跳转到内容

用户:Xyy23330121/Python/函数注释

来自维基学院


如同本学习资料第一次提到注释时一样:在编写程序时,程序员会不可避免地忘记之前写的内容。为了让程序员在读已经忘记的函数代码时,可以快速了解函数的内容、函数的使用要点、以及修改函数的方法。需要进行注释。

而本章所述的注释方法,可以由功能比较强劲的文本编辑器读取、并在使用函数时实时地给程序员以提示。这让复杂项目的编程工作变得十分便利。

但如果项目的内容较少,有时是没有作注释的必要的。是否采取注释和如何使用注释,要视读者的情况而定。

之后学习 类(class) 的时候,使用的注释方法和这个是一样的。所以类的注释不再单独列一个章节。

本章一些内容可能较为复杂,而且就算完全不写注释、程序也能运行。因此本章为选学。

文档字符串

[编辑 | 编辑源代码]
在鼠标移动到 "func" 的位置时,VSCode 自动在鼠标上方显示了函数的文档字符串。而且用类似Markdown的方式渲染了。
在鼠标移动到 "func" 的位置时,VSCode 自动在鼠标上方显示了函数的文档字符串。而且用类似Markdown的方式渲染了。

可以通过以下方式,写入和输出文档字符串。一些文本编辑器(比如VSCode)可以在编辑代码时简单地查阅函数文档。利用文本编辑器的这些功能,通过编写详细的文档,在编写大型项目时可以少记忆许多内容。

def func():
    """# 什么都不做

  本函数仅包含一个 pass 语句。
  
  当使用 if / else / def 等语句时,冒号
  的下一行必须是语句组的内容,要进行缩进。
  但单独只有缩进也会导致出错。因此,必须
  要写入至少一个语句。若希望语句组不执行
  任何操作,可以使用 pass 语句来作为语句
  组中唯一的语句。
  
  在使用 pass 语句时,应注意能否有更好的
  方法。比如 if True: pass; else: do so-
  mething的情况。可以直接用 if not True。
  
  使用方法:
  >>> func()
"""
    pass

print(func.__doc__)  #输出文档字符串的内容

输出为:

# 什么都不做

  本函数仅包含一个 pass 语句。
  
  当使用 if / else / def 等语句时,冒号
  的下一行必须是语句组的内容,要进行缩进。
  但单独只有缩进也会导致出错。因此,必须
  要写入至少一个语句。若希望语句组不执行
  任何操作,可以使用 pass 语句来作为语句
  组中唯一的语句。
  
  在使用 pass 语句时,应注意能否有更好的
  方法。比如 if True: pass; else: do so-
  mething的情况。可以直接用 if not True。
  
  使用方法:
  >>> func()

使用文档字符串时,应当注意其可读性。

函数的简单类型注解

[编辑 | 编辑源代码]

除上面的“文档字符串”以外,类型注解也是一种良好的注释方式。

对于参数的类型注解如果有不了解的地方,可以查阅 Python 文档。如果查阅 Python 文档之后还不了解,那就直接省略注解也是可以的。

使用内置类型进行类型注解

[编辑 | 编辑源代码]
类型名称列表
类型名 含义
int 整数
float 浮点数
complex 复数
bool 布尔值
list 列表
tuple 元组
str 字符串
dict 字典
set 集合
frozenset 不可变集合

以下示例展示了对参数的类型进行注解,以及设置默认值的方式。该示例还提示了该函数返回值的类型。

def plus(arg1: int, arg2: int = 0) -> int:
    return arg1 + arg2

这代表着:plus 函数的参数 arg1 应传入一个 int 类型的值;arg2 有默认值 0,如果要传入,应传入一个 int 类型的值。plus 函数应返回一个 int 类型的值。

类型注解仅作为标识,对函数运行无影响。如果我们执行:

print(plus(1.1))

输出为:

1.1

尽管类型注解提示了应当给参数传入整数,但传入浮点数时,函数依旧正常运行。而函数的返回值也并非类型注解提示的整数。

右侧表格简单总结了目前学过的一些类型的名称。读者可以查阅 Python 文档,了解更多的内置类型。

关于类型名称,我们将在“类”章节中得到更深刻的了解。

更多可用于标注的类型

[编辑 | 编辑源代码]

除上述类型之外,还有一些特殊类型。特别的,typing 模块提供了许多特殊类型。为篇幅起见,这里只列出两个最常用的特殊类型:

我们之前提到过,None 是 Python 中代表“无”的量。实际上,它同时也是一个类型。我们可以用 None 来标记函数没有返回值。例如:

def prtplus(arg1: int, arg2: int = 0) -> None:
    print(arg1 + arg2)

typing.Any

[编辑 | 编辑源代码]

该类型代表任意类型。它可以说明函数参数可以传入任意类型,也可以说明函数可以返回任意类型。例如:

from typing import Any

def prttype(arg: Any) -> None:
    print(type(arg))

特别的,对于未进行类型注解的参数或返回值,默认使用 typing.Any 作为其参数类型和返回值类型。

使用抽象基类进行类型注解

[编辑 | 编辑源代码]

抽象基类 也可以用于类型注解。

比如,collections.abc.Mapping 表示映射类型,任何具有映射功能的类型,比如字典,都会被判断为 collections.abc.Mapping 的子类型,也就可以用该类型作为标注:

from collections.abc import Mapping

def func(arg: Mapping) -> None: pass

为了让代码的类型在进行检查时尽可能地适用广泛,可以使用抽象基类。

对于 collections.abc 提到的“抽象方法”等内容,可能要到类的方法与Python的计算章节才会详细讲解。

类型注解的作用

[编辑 | 编辑源代码]

静态类型检查器

[编辑 | 编辑源代码]

类型注解可以用于静态类型检查器。静态类型检查器会检查 Python 代码中各元素的类型,并对于其中类型不正确的内容进行提示和报错。比如:

def func(s: int) -> None:
    s[index]

在静态类型检查器中会提示报错,因为整数类型不支持索引操作。

文本编辑器

[编辑 | 编辑源代码]

一些功能强大的文本编辑器会按照类型来提供提示,比如,输入以下内容后:

a = [0, 1]  #文本编辑器检查这一行,得知 a 的类型
a.

文本编辑器会自动在后面添加一个下拉的提示框。以提示之后的内容可以写appendclearcopy等。

但这对于函数的参数是没用的,

def func(a):
    a.

此时,文本编辑器不会提供任何提示。这是因为文本编辑器不能通过赋值表达式知道函数参数的类型。

但如果我们使用:

def func(a:list):  #文本编辑器检查这一行,得知 a 的类型
    a.

文本编辑器就能够提供提示了。这在许多情况下很有帮助。

从函数属性得到类型注解

[编辑 | 编辑源代码]

类型注解的内容会被作为函数的属性存储起来。我们可以用以下方式输出所存储的注解:

print(plus.__annotations__)

注解的输出为一个字典:

{'arg1': <class 'int'>, 'arg2': <class 'int'>, 'return': <class 'int'>}

使用Python交互模式、查文档又比较麻烦时。可以用这种方式查询类型注解。

特别说明

[编辑 | 编辑源代码]

参见《Python指南:注解最佳实践》:

复合类型

[编辑 | 编辑源代码]

有时我们希望说明一项参数的类型“不是int就是float”,或者希望说明一项参数是“由int构成的列表”。此时就需要对类型进行某种运算。

或类型

[编辑 | 编辑源代码]

可以用|运算符,表示类型是左右两者之一。

R = int | float           #整数或浮点数
C = int | float | complex #整数或浮点数,或复数

也可以用泛型的方法来作标注:

from typing import Union
R = Union[int, float]           #整数或浮点数
C = Union[int, float, complex]  #整数或浮点数,或复数

标注容器中元素的类型

[编辑 | 编辑源代码]

序列类型、字典、集合等被称之为容器类型。许多容器类型都支持下标操作,以表示其内部元素的类型。

我们可以用以下方式标注“由某个类型的元素”组成的列表:

a = list[int]  #以多个整数组成的类型。
b = list[int | None]  #以多个“整数或None”组成的类型

类似,可以用 set[int] 来表示由多个整数组成的列表等。

而对于映射结构,比如 dict ,则是用 dict[str, int] 表示“用字符串作索引,得到整数”的字典。


对于 Python 中的大多数容器,类型系统会假定容器中的所有元素都是相同类型的。这表现在代码上,就是 list[ClassName] 等方式最多仅支持一个参数。由此,我们可以总结出泛型的一般形式:

  1. 一般容器类型:list[int] set[int]
  2. 映射容器类型:dict[str, int] collections.abc.Mapping[str, int]

但元组是一个例外。元组中的元素,在许多代码中并非相同类型,且其位置很关键。所以,我们有以下特例:

特例:元组类型

[编辑 | 编辑源代码]

可以用以下方式标注“由特定类型元素”组成的元组:

a = tuple[int, str]  #有二个元素的元组,第一个元素是整数,第二个元素是字符串。
b = tuple[int, ...]  #不定长的、由int构成的元组。

容器类型的使用

[编辑 | 编辑源代码]

被标注了容器内容的容器类型,在使用时和原类型是一致的。

a = list[int]('Python')  #等同于 a = list('Python')
print(a, type(a)) #输出:['P', 'y', 't', 'h', 'o', 'n'] <class 'list'>

可调用类型

[编辑 | 编辑源代码]

函数就是可调用的类型。我们可以通过以下方式标注可调用的类型。

from typing import Callable

a = Callable[[int], str]       #输入整数参数,返回字符串的可调用类型
b = Callable[[int, int], None] #输入两个整数参数,不返回任何值的可调用类型

特别的,用省略号(三个小数点)可以表示任意参数列表。比如:

from typing import Callable

a = Callable[..., str]       #输入任意参数列表,返回字符串的可调用类型

类对象的类型

[编辑 | 编辑源代码]

利用 type[ClassName] 的方式,可以要求传入的参数或返回值的类型是“类”本身或其子类本身。我们在之前的学习中,已经遇到过“类”对象了。比如:

1       #类型为:int
type(1) #类型为:type[int]
int     #类型为:type[int]

在使用时,可以用以下方式:

a = type[int]    #类 int 或其子类
b = type[object] #类 object 或其子类,即任意类型

类型别名

[编辑 | 编辑源代码]

有些类型,尤其是复合类型写起来比较麻烦。此时,我们可以用以下几种方式创建类型别名:

type 语句

[编辑 | 编辑源代码]
type vector = list[float]

之后,我们就可以使用名称 vector 进行类型注解。

简单赋值语句

[编辑 | 编辑源代码]

type 语句是 Python 3.12 新增的。为了向下兼容,也可以通过简单赋值语句创建类型别名:

vector = list[float]

有时我们不关注具体的类型是什么,而只在乎两个变量属于同一类型。此时,我们可以用泛型来指代这一类型。

基本方式:typing.TypeVar

[编辑 | 编辑源代码]

创建泛型的最基本方式,就是用 typing.TypeVar。

from typing import TypeVar
T = TypeVar("T")
c = list[T]   #以多个“待定类型T的实例”组成的类型。

此时,如果有一个函数被标注为:

def func(arg: c):
    ...

那么向它传入

arg = [0, 1, 2]    #以整数作为待定类型T
arg = ["0", "1", "2"] #以字符串作为待定类型T

都不会被类型检查器报错。

对泛型进行额外限制

[编辑 | 编辑源代码]

我们可以对泛型进行一些额外的限制。

S = TypeVar("S", bound=str)
d = list[S]   #以多个“待定类型S的实例”组成的类型。要求S是str的子类。

N = TypeVar("N", int, float, complex)
e = list[N]   #以多个“待定类型N的实例”组成的类型。N或者是int,或者是float,或者是complex。
            #等同于 e = list[int] | list[float] | list[complex]

泛型元组:typing.TypeVarTuple

[编辑 | 编辑源代码]

当标注所需的泛型比较多,或者泛型的个数不定时,可以用泛型元组。以下面的例子为例:

from typing import TypeVar, TypeVarTuple
T = TypeVar("T")
Ts = TypeVarTuple("Ts")

def move_first_element_to_last(tup: tuple[T, *Ts]) -> tuple[*Ts, T]:
    return (*tup[1:], tup[0])

在刚才的例子中,我们并不在乎参数元组的长度和元组中元素的具体类型。我们只希望备注“返回值中各元素类型”和“参数中各元素类型”的对应关系。此时可以使用泛型元组。

泛型元组不能直接用于标记某个对象是元组类型,而是必须被解包后使用:

x: Ts          # 不可用
x: tuple[Ts]   # 不可用
x: tuple[*Ts]  # 正确的做法
x: Callable[[*Ts], T] # 正确的做法

同一级别只能存在一个被解包的泛型元组,比如:

Ts = TypeVarTuple("Ts")
Ts2= TypeVarTuple("Ts2")

x: tuple[*Ts, *Ts2]  # Ts 和 Ts2 在同一级被解包,不可用
x: tuple[*Ts, tuple[*Ts2]] # 可用

泛型元组可以用于标记函数的打包参数,比如:

T = TypeVar("T")
Ts = TypeVarTuple("Ts")

def func1(*args: T) -> None:
    pass

def func2(*args: *Ts) -> None:
    pass

func1在类型标记上,希望被打包的参数属于同一类型。而func2并不要求如此。

作为函数参数的泛型:typing.ParamSpec

[编辑 | 编辑源代码]

这一泛型是专门用于标注函数参数的。比如:

from typing import Callable, ParamSpec

T = TypeVar("T")
P = ParamSpec('P')

def print_when_run(f: Callable[P, T]) -> Callable[P, T]:
    def decorated_f(*args: P.args, **kwargs: P.kwargs) -> T:
        print("Function called.")
        return f(*args, **kwargs)
    return decorated_f

在上面的例子中,我们实现了一个装饰器,并对装饰器所装饰的函数进行了类型标注。

与泛型元组不同,此时还支持 对关键字参数的打包

对于函数和类型的简便写法

[编辑 | 编辑源代码]

上面列出的基本方式,主要是为了方便读者理解。在实际的泛型使用中,一般会使用更为简便的写法。

基本规则

[编辑 | 编辑源代码]
def func[T](arg: T) -> T:
    ...

这可被认为是等同于

def TYPE_PARAMS_OF_func():
    T = typing.TypeVar("T")    #泛型“T”在局部作用域里面被自动设置
    def func(arg: T) -> T: ...
    func.__type_params__ = (T,)
    return func
func = TYPE_PARAMS_OF_func()
del TYPE_PARAMS_OF_func

类似的,有

class Bag[T]: ...
# 等同于:
def TYPE_PARAMS_OF_Bag():
    T = typing.TypeVar("T")
    class Bag(typing.Generic[T]):
        __type_params__ = (T,)
        ...
    return Bag
Bag = TYPE_PARAMS_OF_Bag()
del TYPE_PARAMS_OF_Bag

泛型函数、泛型类与__type_params__属性

[编辑 | 编辑源代码]

参数或其它相关内容被用这种方式标记的函数和类型,被称为泛型函数和泛型类。称用于标注的泛型为函数或类的类型形参。

泛型函数在使用上和一般的函数是一样的。但它会额外提供一个__type_params__属性用于存储所有使用的泛型。

def func[T](arg:T) -> T:
    return arg

print(func.__type_params__)  #输出:(T,)

__type_params__是一个包含了函数所使用的所有泛型的元组。上面的函数只使用了T作为泛型,所以元组中只包含T。对于未通过上面方法设置泛型的一般函数而言,__type_params__是空元组。

泛型元组与专用于参数的泛型

[编辑 | 编辑源代码]

在使用简便写法时,标注泛型元组的方法为:

def func[*Ts](*args:*Ts) -> tuple[*Ts]:
    return args

print(func.__type_params__)  #输出:(Ts,)

标注专用于参数的泛型时,则是用:

def func[**Params](f:Callable[Params, None]) -> Callable[Params, None]:
    return f

print(func.__type_params__)  #输出:(Params,)

对泛型进行额外限制

[编辑 | 编辑源代码]
def func[A: int, B: (str, bytes)](a: A, b: B):
    pass

这可被认为是等同于

def TYPE_PARAMS_OF_func():
    A = typing.TypeVar("A", bound=int)
    B = typing.TypeVar("B", str, bytes)
    def func(a: A, b: B): ...
    func.__type_params__ = (A, B)
    return func
func = TYPE_PARAMS_OF_func()
del TYPE_PARAMS_OF_func

实际上,Python 文档:类型形参列表提供了更多种类的额外限制。