Python 类型注解规范 | Google 官方 PEP-484/585 速查

2025-07-31 11:37 更新

类型注解 (type annotation)

通用规则

  1. 熟读 PEP-484
  2. 仅在有额外类型信息时才需要注解方法中 self 或 cls 的类型。例如:

    @classmethod
    def create(cls: Type[_T]) -> _T:
    return cls()

  3. 类似地,不需要注解 __init__ 的返回值(只能返回 None)。
  4. 对于其他不需要限制变量类型或返回类型的情况,应该使用 Any
  5. 无需注解模块中的所有函数。
    1. 至少需要注解你的公开 API。
    2. 你可以自行权衡,一方面要保证代码的安全性和清晰性,另一方面要兼顾灵活性。
    3. 应该注解那些容易出现类型错误的代码(比如曾经出现过错误或疑难杂症)。
    4. 应该注解晦涩难懂的代码。
    5. 应该注解那些类型已经确定的代码。多数情况下,即使注解了成熟的代码中所有的函数,也不会丧失太多灵活性。

换行

尽量遵守前文所述的缩进规则。 添加类型注解后,很多函数签名(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。) 类型变量必须有描述性的名称,除非满足以下所有标准:

  1. 外部不可见
  2. 没有约束条件

正确:

_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: 语句块内。

  1. 在类型注解中,有条件地导入的类型必须用字符串表示,这样才能和 Python 3.6 之前的代码兼容。因为 Python 3.6 之前真的会对类型注解求值。
  2. 只有那些仅仅用于类型注解的实例才能有条件地导入,别名也是如此。否则会引发运行时错误,因为运行时不会导入这些模块。
  3. 有条件的导入语句应紧随所有常规导入语句之后。
  4. 有条件的导入语句之间不能有空行。
  5. 和常规导入一样,请对有条件的导入语句排序。

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到员工名的映射."""
以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号