用户:Xyy23330121/Python/函数注释
如同本学习资料第一次提到注释时一样:在编写程序时,程序员会不可避免地忘记之前写的内容。为了让程序员在读已经忘记的函数代码时,可以快速了解函数的内容、函数的使用要点、以及修改函数的方法。需要进行注释。
而本章所述的注释方法,可以由功能比较强劲的文本编辑器读取、并在使用函数时实时地给程序员以提示。这让复杂项目的编程工作变得十分便利。
但如果项目的内容较少,有时是没有作注释的必要的。是否采取注释和如何使用注释,要视读者的情况而定。
之后学习 类(class) 的时候,使用的注释方法和这个是一样的。所以类的注释不再单独列一个章节。
本章一些内容可能较为复杂,而且就算完全不写注释、程序也能运行。因此本章为选学。

可以通过以下方式,写入和输出文档字符串。一些文本编辑器(比如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)
该类型代表任意类型。它可以说明函数参数可以传入任意类型,也可以说明函数可以返回任意类型。例如:
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.
文本编辑器会自动在后面添加一个下拉的提示框。以提示之后的内容可以写append、clear、copy等。
但这对于函数的参数是没用的,
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] 等方式最多仅支持一个参数。由此,我们可以总结出泛型的一般形式:
- 一般容器类型:
list[int]set[int]等 - 映射容器类型:
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 vector = list[float]
之后,我们就可以使用名称 vector 进行类型注解。
type 语句是 Python 3.12 新增的。为了向下兼容,也可以通过简单赋值语句创建类型别名:
vector = list[float]
有时我们不关注具体的类型是什么,而只在乎两个变量属于同一类型。此时,我们可以用泛型来指代这一类型。
创建泛型的最基本方式,就是用 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]
当标注所需的泛型比较多,或者泛型的个数不定时,可以用泛型元组。以下面的例子为例:
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并不要求如此。
这一泛型是专门用于标注函数参数的。比如:
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__属性用于存储所有使用的泛型。
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 文档:类型形参列表提供了更多种类的额外限制。