参考:PEP 3107 -- Function Annotations | Python.org 与 PEP 484 -- Type Hints | Python.org
1 PEP 3107 -- Function Annotations
该 PEP 引入了用于向 Python 函数添加任意元数据注释的语法。
参数和返回值的函数注释(function annotations)完全是可选的。函数注释无非是在编译时将任意 Python 表达式与函数的各个部分相关联的一种方式。
就其本身而言,Python 不会对注释赋予任何特定的含义或意义。单独使用,Python 只需按照下面的“Accessing Function Annotations”中的描述使这些表达式可用。
注释具有意义的唯一方法是当它们由第三方库解释时。这些批注使用者可以使用函数的批注做他们想做的任何事情。例如,一个库可能使用基于字符串的注释来提供改进的帮助消息,如下所示:
def compile(source: "something compilable",
filename: "where the compilable thing comes from",
mode: "is this a single statement or a suite?"):
...
可以使用另一个库为 Python 函数和方法提供类型检查。该库可以使用注释来指示函数的预期输入和返回类型,可能类似于:
def haul(item: Haulable, *vargs: PackAnimal) -> Distance:
...
但是,第一个示例中的字符串或第二个示例中的类型信息都没有任何意义。含义仅来自第三方库。
- 从第2点开始,即使是对于内置类型,该PEP也没有尝试引入任何类型的标准语义。 这项工作将留给第三方库。
1.1 参数注解
参数注释采用参数名称后面的可选表达式的形式:
def foo(a: expression, b: expression = 5):
...
在伪语法中,参数现在看起来像 identifier [: expression] [= expression]
。也就是说,注释始终在参数的默认值之前,并且注释和默认值都是可选的。就像使用等号表示默认值一样,冒号用于标记注释。就像默认值一样,在执行函数定义时将评估所有注释表达式。
多余参数(即*args
和**kwargs
)的注释类似地表示为:
def foo(*args: expression, **kwargs: expression):
...
嵌套参数的注释始终跟随参数的名称,而不是最后的括号。不需要注释嵌套参数的所有参数:
def foo((x1, y1: expression),
(x2: expression, y2: expression)=(None, None)):
...
1.2 return 注解
到目前为止,这些示例都省略了有关如何注释函数的返回值类型的示例。 这样做是这样的:
def sum() -> expression:
...
也就是说,参数列表现在可以跟随一个字面量 ->
和一个 Python 表达式。像参数注释一样,执行函数定义时将评估此表达式。
现在,函数定义的语法为:
decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE
decorators: decorator+
funcdef: [decorators] 'def' NAME parameters ['->' test] ':' suite
parameters: '(' [typedargslist] ')'
typedargslist: ((tfpdef ['=' test] ',')*
('*' [tname] (',' tname ['=' test])* [',' '**' tname]
| '**' tname)
| tfpdef ['=' test] (',' tfpdef ['=' test])* [','])
tname: NAME [':' test]
tfpdef: tname | '(' tfplist ')'
tfplist: tfpdef (',' tfpdef)* [',']
1.3 Accessing Function Annotations
编译后,可通过函数的 __annotations__
属性获得函数的注释。此属性是可变的字典,将参数名称映射到表示所评估的注释表达式的对象。__annotations__
映射中有一个特殊的键“return
”。仅当为函数的返回值被提供注释时,此键才存在。
例如,以下注释:
def foo(a: 'x', b: 5 + 6, c: list) -> max(2, 9):
...
会导致 __annotations__
映射:
{'a': 'x',
'b': 11,
'c': list,
'return': 9}
选择 return
键是因为它不能与参数名称冲突。任何使用 return
作为参数名称的尝试都将导致SyntaxError。
如果该函数上没有注释,或者该函数是从 lambda 表达式创建的,则 __annotations__
是一个空的可变字典。
2 typing
--- 类型标注支持¶
Python 运行时并不强制标注函数和变量类型。类型标注可被用于第三方工具,比如类型检查器、集成开发环境、静态检查器等。
类型提示最基本的支持由 Any
,Union
,Tuple
,Callable
,TypeVar
和 Generic
类型组成。
2.1 类型别名
要定义一个类型别名,可以将一个类型赋给别名。在本例中,Vector
和 list[float]
将被视为可互换的同义词:
from typing import List
Vector = List[float]
def scale(scalar: float, vector: Vector) -> Vector:
return [scalar * num for num in vector]
# typechecks; a list of floats qualifies as a Vector.
new_vector = scale(2.0, [1.0, -4.2, 5.4])
类型别名可用于简化复杂类型签名。例如:
from typing import Dict, Tuple, Sequence
ConnectionOptions = Dict[str, str]
Address = Tuple[str, int]
Server = Tuple[Address, ConnectionOptions]
def broadcast_message(message: str, servers: Sequence[Server]) -> None:
...
# The static type checker will treat the previous type signature as
# being exactly equivalent to this one.
def broadcast_message(
message: str,
servers: Sequence[Tuple[Tuple[str, int], Dict[str, str]]]) -> None:
...
请注意,None
作为类型提示是一种特殊情况,并且由 type(None)
取代。
2.2 NewType
使用 NewType()
辅助函数创建不同的类型:
from typing import NewType
UserId = NewType('UserId', int)
some_id = UserId(524313)
静态类型检查器会将新类型视为它是原始类型的子类。这对于帮助捕捉逻辑错误非常有用:
def get_user_name(user_id: UserId) -> str:
...
# typechecks
user_a = get_user_name(UserId(42351))
# does not typecheck; an int is not a UserId
user_b = get_user_name(-1)
您仍然可以对 UserId
类型的变量执行所有的 int
支持的操作,但结果将始终为 int
类型。这可以让你在需要 int
的地方传入 UserId
,但会阻止你以无效的方式无意中创建 UserId
:
# 'output' is of type 'int', not 'UserId'
output = UserId(23413) + UserId(54341)
请注意,这些检查仅通过静态类型检查程序来强制。在运行时,语句 Derived = NewType('Derived',Base)
将 Derived
设为一个函数,该函数立即返回您传递它的任何参数。这意味着表达式 Derived(some_value)
不会创建一个新的类或引入任何超出常规函数调用的开销。更确切地说,表达式 some_value is Derived(some_value)
在运行时总是为真。
这也意味着无法创建 Derived
的子类型,因为它是运行时的标识函数,而不是实际的类型:
from typing import NewType
UserId = NewType('UserId', int)
# Fails at runtime and does not typecheck
class AdminUserId(UserId): pass
但是,可以基于'derived' NewType
创建 NewType()
from typing import NewType
UserId = NewType('UserId', int)
ProUserId = NewType('ProUserId', UserId)
并且 ProUserId
的类型检查将按预期工作。有关更多详细信息,请参阅 PEP 484。
NewType
声明一种类型是另一种类型的子类型。Derived = NewType('Derived', Original)
将使静态类型检查器将 Derived
当作 Original
的 子类 ,这意味着 Original
类型的值不能用于 Derived
类型的值需要的地方。当您想以最小的运行时间成本防止逻辑错误时,这非常有用。
2.3 Callable
期望特定签名的回调函数的框架可以将类型标注为 Callable[[Arg1Type, Arg2Type], ReturnType]
。
例如:
from typing import Callable
def feeder(get_next_item: Callable[[], str]) -> None:
# Body
...
def async_query(on_success: Callable[[int], None],
on_error: Callable[[int, Exception], None]) -> None:
# Body
...
通过用字面量省略号替换类型提示中的参数列表:Callable[...,ReturnType]
,可以声明可调用的返回类型,而无需指定调用签名。
2.4 泛型(Generic)
由于无法以通用方式静态推断有关保存在容器中的对象的类型信息,因此抽象基类已扩展为支持订阅以表示容器元素的预期类型。
from typing import Mapping, Sequence
class Employee:
...
def notify_by_email(employees: Sequence[Employee],
overrides: Mapping[str, str]) -> None:
...
泛型可以通过使用typing模块中名为 TypeVar
的新工厂进行参数化。
from typing import Sequence, TypeVar
T = TypeVar('T') # Declare type variable
def first(l: Sequence[T]) -> T: # Generic function
return l[0]
TypeVar
支持将参数类型限制为一组固定的可能类型(注意:这些类型不能由类型变量进行参数化)。例如,我们可以定义一个类型变量,其范围仅在str和字节范围内。默认情况下,类型变量覆盖所有可能的类型。 约束类型变量的示例:
from typing import TypeVar, Text
AnyStr = TypeVar('AnyStr', Text, bytes)
def concat(x: AnyStr, y: AnyStr) -> AnyStr:
return x + y
2.5 用户定义的泛型类型
用户定义的类可以定义为泛型类。
from typing import TypeVar, Generic
from logging import Logger
T = TypeVar('T')
class LoggedVar(Generic[T]):
def __init__(self, value: T, name: str, logger: Logger) -> None:
self.name = name
self.logger = logger
self.value = value
def set(self, new: T) -> None:
self.log('Set ' + repr(self.value))
self.value = new
def get(self) -> T:
self.log('Get ' + repr(self.value))
return self.value
def log(self, message: str) -> None:
self.logger.info(f'{self.name}, {message}', )
Generic[T]
作为基类定义了类 LoggedVar
采用单个类型参数 T
。这也使得 T
作为类体内的一个类型有效。
Generic
基类定义了 _getitem__()
,使得 LoggedVar[t]
作为类型有效:
from typing import Iterable
def zero_all_vars(vars: Iterable[LoggedVar[int]]) -> None:
for var in vars:
var.set(0)
泛型类型可以有任意数量的类型变量,并且类型变量可能会受到限制:
from typing import TypeVar, Generic
T = TypeVar('T')
S = TypeVar('S', int, str)
class StrangePair(Generic[T, S]):
...
Generic
每个参数的类型变量必须是不同的。这是无效的:
from typing import TypeVar, Generic
...
T = TypeVar('T')
class Pair(Generic[T, T]): # INVALID
...
您可以对 Generic
使用多重继承:
from typing import TypeVar, Generic, Sized, Iterable, Container, Tuple
T = TypeVar('T')
class LinkedList(Sized, Generic[T]):
...
K = TypeVar('K')
V = TypeVar('V')
class MyMapping(Iterable[Tuple[K, V]],
Container[Tuple[K, V]],
Generic[K, V]):
...
从泛型类继承时,某些类型变量可能是固定的:
from typing import TypeVar, Mapping
T = TypeVar('T')
class MyDict(Mapping[str, T]):
...
在这种情况下,MyDict
只有一个参数,T
。
在不指定类型参数的情况下使用泛型类别会为每个位置假设 Any
。在下面的例子中,MyIterable
不是泛型,但是隐式继承自 Iterable[Any]
:
from typing import Iterable
class MyIterable(Iterable):
# Same as Iterable[Any]
...
用户定义的通用类型别名也受支持。例子:
from typing import TypeVar, Union, Iterable, Tuple
S = TypeVar('S')
Response = Union[Iterable[S], int]
# Return type here is same as Union[Iterable[str], int]
def response(query: str) -> Response[str]:
...
T = TypeVar('T', int, float, complex)
Vec = Iterable[Tuple[T, T]]
def inproduct(v: Vec[T]) -> T: # Same as Iterable[tuple[T, T]]
return sum(x*y for x, y in v)
一个用户定义的泛型类能够使用抽象基本类作为基类,而不会发生元类冲突。泛型元类不再被支持。参数化泛型的结果会被缓存,并且在 typing 模块中的大部分类型是可哈希且可比较相等性的。
2.6 Any
类型
Any
是一种特殊的类型。静态类型检查器将所有类型视为与 Any
兼容,反之亦然, Any
也与所有类型相兼容。
这意味着可对类型为 Any
的值执行任何操作或者方法调用并将其赋值给任意变量:
from typing import Any
a = None # type: Any
a = [] # OK
a = 2 # OK
s = '' # type: str
s = a # OK
def foo(item: Any) -> int:
# Typechecks; 'item' could be any type,
# and that type might have a 'bar' method
item.bar()
...
需要注意的是,将 Any
类型的值赋值给另一个更具体的类型时,Python不会执行类型检查。例如,当把 a
赋值给 s
时,即使 s
被声明为 str
类型,在运行时接收到的是 int
值,静态类型检查器也不会报错。
此外,所有返回值无类型或形参无类型的函数将隐式地默认使用 Any
类型:
def legacy_parser(text):
...
return data
# A static type checker will treat the above
# as having the same signature as:
def legacy_parser(text: Any) -> Any:
...
return data
当需要混用动态类型和静态类型的代码时,上述行为可以让 Any
被用作 应急出口 。
Any
和 object
的行为对比。与 Any
相似,所有的类型都是 object
的子类型。然而不同于 Any
,反之并不成立: object
不是 其他所有类型的子类型。
这意味着当一个值的类型是 object
的时候,类型检查器会拒绝对它的几乎所有的操作。把它赋值给一个指定了类型的变量(或者当作返回值)是一个类型错误。比如说:
def hash_a(item: object) -> int:
# Fails; an object does not have a 'magic' method.
item.magic()
...
def hash_b(item: Any) -> int:
# Typechecks
item.magic()
...
# Typechecks, since ints and strs are subclasses of object
hash_a(42)
hash_a("foo")
# Typechecks, since Any is compatible with all types
hash_b(42)
hash_b("foo")
使用 object
示意一个值可以类型安全地兼容任何类型。使用 Any
示意一个值地类型是动态定义的。
2.7 名义性子类型 区别于 结构性子类型
最初 PEP 484 将 Python 的静态类型系统定义为使用 名义性子类型(nominal subtyping)。即是说,当且仅当 A
是 B
的子类时,可在需要 B
类时提供 A
类。
这一要求之前也适用于抽象基类,比如 Iterable
。这一做法的问题在于,一个类必须显式地标注为支持他们,这即不 Pythonic,也不太可能在惯用动态类型的 Python 代码中会有人正常地去用。举例来说,这符合 PEP 484:
from typing import Sized, Iterable, Iterator
class Bucket(Sized, Iterable[int]):
...
def __len__(self) -> int: ...
def __iter__(self) -> Iterator[int]: ...
PEP 544 通过允许用户不必在类定义中显式地标注基类来解决这一问题,允许静态类型检查器隐含地认为 Bucket
既是 Sized
的子类型又是 Iterable[int]
的子类型。这被称为 结构性子类型 (structural subtyping,或者静态鸭子类型(duck-typing)):
from typing import Iterator, Iterable
class Bucket: # Note: no base classes
...
def __len__(self) -> int: ...
def __iter__(self) -> Iterator[int]: ...
def collect(items: Iterable[int]) -> int: ...
result = collect(Bucket()) # Passes type check
此外,通过继承一个特殊的类 Protocol
,用户能够定义新的自定义协议来充分享受结构化子类型(后文中有例子)。