Python 类型注解规范 | Google 官方 PEP-484/585 速查
类型注解 (type annotation)
通用规则
- 熟读 PEP-484。
-
仅在有额外类型信息时才需要注解方法中
self
或cls
的类型。例如:@classmethod def create(cls: Type[_T]) -> _T: return cls()
- 类似地,不需要注解
__init__
的返回值(只能返回None
)。 - 对于其他不需要限制变量类型或返回类型的情况,应该使用
Any
。 - 无需注解模块中的所有函数。
- 至少需要注解你的公开 API。
- 你可以自行权衡,一方面要保证代码的安全性和清晰性,另一方面要兼顾灵活性。
- 应该注解那些容易出现类型错误的代码(比如曾经出现过错误或疑难杂症)。
- 应该注解晦涩难懂的代码。
- 应该注解那些类型已经确定的代码。多数情况下,即使注解了成熟的代码中所有的函数,也不会丧失太多灵活性。
换行
尽量遵守前文所述的缩进规则。 添加类型注解后,很多函数签名(signature)会变成每行一个参数的形式。若要让返回值单独成行,可以在最后一个参数尾部添加逗号。
def my_method(
self,
first_var: int,
second_var: Foo,
third_var: Bar | None,
) -> int:
...
尽量在变量之间换行,避免在变量和类型注解之间换行。当然,若所有东西可以挤进一行,也可以接受。
def my_method(self, first_var: int) -> int:
...
若最后一个参数加上返回值的类型注解太长,也可以换行并添加4格缩进。添加换行符时,建议每个参数和返回值都在单独的一行里, 并且右括号和 def
对齐。
正确:
def my_method(
self,
other_arg: MyLongType | None,
) -> tuple[MyLongType1, MyLongType1]:
...
返回值类型和最后一个参数也可以放在同一行。
可以接受:
def my_method(
self,
first_var: int,
second_var: int) -> dict[OtherLongType, MyLongType]:
...
pylint
也允许你把右括号放在新行上,与左括号对齐,但相较而言可读性更差。
错误:
def my_method(self,
other_arg: MyLongType | None,
) -> dict[OtherLongType, MyLongType]:
...
正如上面所有的例子,尽量不要在类型注解中间换行。但是有时注解过长以至于一行放不下。此时尽量保持子类型中间不换行。
def my_method(
self,
first_var: tuple[list[MyLongType1],
list[MyLongType2]],
second_var: list[dict[
MyLongType3, MyLongType4]],
) -> None:
...
若某个名称和对应的类型注解过长,可以考虑用 别名(alias)代表类型。下策是在冒号后换行并添加4格缩进。
正确:
def my_function(
long_variable_name:
long_module_name.LongTypeName,
) -> None:
...
错误:
def my_function(
long_variable_name: long_module_name.
LongTypeName,
) -> None:
...
前向声明 (foward declaration)
若需要使用一个尚未定义的类名( 比如想在声明一个类时使用自身的类名),可以使用 from __future__ import annotations
或者字符串来代表类名。
正确:
from __future__ import annotations
class MyClass:
def __init__(self, stack: Sequence[MyClass], item: OtherClass) -> None:
class OtherClass:
...
class MyClass:
def __init__(self, stack: Sequence['MyClass'], item: 'OtherClass') -> None:
class OtherClass:
...
默认值
根据 PEP-008,只有 对于同时拥有类型注解和默认值的参数,=
的周围应该加空格。
正确:
def func(a: int = 0) -> int:
...
错误:
def func(a:int=0) -> int:
...
NoneType
在 Python 的类型系统中,NoneType
是“一等”类型。在类型注解中,None
是 NoneType
的别名。如果一个变量可能为 None
,则必须声明这种情况! 你可以使用 |
这样的并集(union)类型表达式(推荐在新的 Python 3.10+ 代码中使用)或者老的 Optional
和 Union
语法。
应该用显式的 X | None
替代隐式声明。早期的 PEP 484 允许将 a: str = None
解释为 a: str | None = None
,但这不再是推荐的行为。
正确:
## 现代的并集写法.
def modern_or_union(a: str | int | None, b: str | None = None) -> str:
...
## 采用 Union / Optional.
def union_optional(a: Union[str, int, None], b: Optional[str] = None) -> str:
...
错误:
## 用 Union 代替 Optional.
def nullable_union(a: Union[None, str]) -> str:
...
## 隐式 Optional.
def implicit_optional(a: str = None) -> str:
...
类型别名 (alias)
你可以为复杂的类型声明一个别名。别名的命名应该采用大驼峰(例如 CapWorded
)。 若别名仅在当前模块使用,应在名称前加 _
代表私有(例如 _Private
)。
注意下面的 : TypeAlias
类型注解只能在 3.10 以后的版本使用。
from typing import TypeAlias
_LossAndGradient: TypeAlias = tuple[tf.Tensor, tf.Tensor]
ComplexTFMap: TypeAlias = Mapping[str, _LossAndGradient]
忽略类型
你可以使用特殊的注释 # type: ignore
禁用某一行的类型检查。
pytype
有针对特定错误的禁用选项(类似格式检查器):
## pytype: disable=attribute-error
标注变量的类型
带类型注解的赋值
如果难以自动推理某个内部变量的类型,可以用带类型注解的赋值操作来指定类型:在变量名和值的中间添加冒号和类型,类似于有默认值的函数参数。
a: Foo = SomeUndecoratedFunction()
类型注释
你可能在代码仓库中看到这种残留的注释(在 Python 3.6 之前必须这样写注释),但是不要再添加 # type: <类型>
这样的行尾注释了:
a = SomeUndecoratedFunction() # type: Foo
元组还是列表
有类型的列表中只能有一种类型的元素。有类型的元组可以有相同类型的元素或者若干个不同类型的元素。后面这种情况多用于注解返回值的类型。 (译者注:注意这里是指的类型注解中的写法,实际python中,list和tuple都是可以在一个序列中包含不同类型元素的,当然,本质其实list和tuple中放的是元素的引用)
a: list[int] = [1, 2, 3]
b: tuple[int, ...] = (1, 2, 3)
c: tuple[int, str, float] = (1, "2", 3.5)
类型变量 (type variable)
Python 的类型系统支持 泛型 (generics)。使用泛型的常见方式是利用类型变量,例如 TypeVar
和 ParamSpec
。
例如:
from collections.abc import Callable
from typing import ParamSpec, TypeVar
_P = ParamSpec("_P")
_T = TypeVar("_T")
...
def next(l: list[_T]) -> _T:
return l.pop()
def print_when_called(f: Callable[_P, _T]) -> Callable[_P, _T]:
def inner(*args: P.args, **kwargs: P.kwargs) -> R:
print('函数被调用')
return f(*args, **kwargs)
return inner
TypeVar
可以有约束条件。
AddableType = TypeVar("AddableType", int, float, str)
def add(a: AddableType, b: AddableType) -> AddableType:
return a + b
AnyStr
是 typing
模块中常用的预定义类型变量。可以用它注解那些接受 bytes
或 str
但是必须保持一致的类型。
from typing import AnyStr
def check_length(x: AnyStr) -> AnyStr:
if len(x) <= 42:
return x
raise ValueError()
(译者注:这个例子中,x 和返回值必须同时是 bytes
或者同时是 str
。)
类型变量必须有描述性的名称,除非满足以下所有标准:
- 外部不可见
- 没有约束条件
正确:
_T = TypeVar("_T")
_P = ParamSpec("_P")
AddableType = TypeVar("AddableType", int, float, str)
AnyFunction = TypeVar("AnyFunction", bound=Callable)
错误:
T = TypeVar("T")
P = ParamSpec("P")
_T = TypeVar("_T", int, float, str)
_F = TypeVar("_F", bound=Callable)
字符串类型
不要在新代码中使用 typing.Text
。这种写法只能用于处理 Python 2/3 的兼容问题。
用 str
表示字符串/文本数据。用 bytes
处理二进制数据。
## 处理文本数据
def deals_with_text_data(x: str) -> str:
...
## 处理二进制数据
def deals_with_binary_data(x: bytes) -> bytes:
...
若一个函数中的字串类型始终一致,比如上述代码中返回值类型和参数类型相同,应该使用 AnyStr。
导入类型
为了静态分析和类型检查而导入 typing
和 collections.abc
模块中的符号时, 一定要导入符号本身。这样常用的类型注解更简洁,也符合全世界的习惯。特别地,你可以在一行内从 typing
和 collections.abc
模块中导入多个特定的类,例如:
from collections.abc import Mapping, Sequence
from typing import Any, Generic
采用这种方法时,导入的类会进入本地命名空间,因此所有 typing
和 collections.abc
模块中的名称都应该和关键词 (keyword) 同等对待。你不能在自己的代码中定义相同的名字,无论你是否采用类型注解。若类型名和某模块中已有的名称出现冲突,可以用 import x as y
的导入形式:
from typing import Any as AnyType
只要可行,就使用内置类型。利用 Python 3.9 引入的 PEP-585,可以在类型注解中使用参数化的容器类型。
def generate_foo_scores(foo: set[str]) -> list[float]:
...
注意: Apache Beam 的用户应该继续导入 typing
模块提供的参数化容器类型。
from typing import Set, List
## 只有在你使用了 Apache Beam 这样没有为 PEP 585 更新的代码, 或者你的
## 代码需要在 Python 3.9 以下版本中运行时, 才能使用这种旧风格.
def generate_foo_scores(foo: Set[str]) -> List[float]:
...
有条件的导入
仅在一些特殊情况下,比如在运行时必须避免导入类型检查所需的模块,才能有条件地导入。不推荐这种写法。替代方案是重构代码,使类型检查所需的模块可以在顶层导入。
可以把仅用于类型注解的导入放在 if TYPE_CHECKING:
语句块内。
- 在类型注解中,有条件地导入的类型必须用字符串表示,这样才能和 Python 3.6 之前的代码兼容。因为 Python 3.6 之前真的会对类型注解求值。
- 只有那些仅仅用于类型注解的实例才能有条件地导入,别名也是如此。否则会引发运行时错误,因为运行时不会导入这些模块。
- 有条件的导入语句应紧随所有常规导入语句之后。
- 有条件的导入语句之间不能有空行。
- 和常规导入一样,请对有条件的导入语句排序。
import typing
if typing.TYPE_CHECKING:
import sketch
def f(x: "sketch.Sketch"): ...
循环依赖
若类型注解引发了循环依赖,说明代码可能存在问题。这样的代码适合重构。虽然技术上我们可以支持循环依赖,但是很多构建系统(build system)不支持。
可以用 Any
替换引起循环依赖的模块。起一个有意义的别名,然后使用模块中的真实类型名(Any 的任何属性依然是 Any)。定义别名的语句应该和最后一行导入语句之间间隔一行。
from typing import Any
some_mod = Any # 因为 some_mod.py 导入了我们的模块.
...
def my_method(self, var: "some_mod.SomeType") -> None:
...
泛型 (generics)
在注解类型时,尽量为泛型类型填入类型参数。否则,泛型参数默认为 Any 。
正确:
def get_names(employee_ids: Sequence[int]) -> Mapping[int, str]:
...
错误:
## 这表示 get_names(employee_ids: Sequence[Any]) -> Mapping[Any, Any]
def get_names(employee_ids: Sequence) -> Mapping:
...
如果泛型类型的参数的确应该是 Any
,请显式地标注,不过注意 TypeVar
很可能更合适。
错误:
def get_names(employee_ids: Sequence[Any]) -> Mapping[Any, str]:
"""返回员工ID到员工名的映射."""
正确:
_T = TypeVar('_T')
def get_names(employee_ids: Sequence[_T]) -> Mapping[_T, str]:
"""返回员工ID到员工名的映射."""
更多建议: