Python 函数注解与类型注解

参考:PEP 3107 -- Function Annotations | Python.orgPEP 484 -- Type Hints | Python.org

1 PEP 3107 -- Function Annotations

该 PEP 引入了用于向 Python 函数添加任意元数据注释的语法。

  1. 参数和返回值的函数注释(function annotations)完全是可选的。函数注释无非是在编译时将任意 Python 表达式与函数的各个部分相关联的一种方式。

  2. 就其本身而言,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:
    ...

但是,第一个示例中的字符串或第二个示例中的类型信息都没有任何意义。含义仅来自第三方库。

  1. 从第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 运行时并不强制标注函数和变量类型。类型标注可被用于第三方工具,比如类型检查器、集成开发环境、静态检查器等。

类型提示最基本的支持由 AnyUnionTupleCallableTypeVarGeneric 类型组成。

2.1 类型别名

要定义一个类型别名,可以将一个类型赋给别名。在本例中,Vectorlist[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 被用作 应急出口

Anyobject 的行为对比。与 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)。即是说,当且仅当 AB 的子类时,可在需要 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 ,用户能够定义新的自定义协议来充分享受结构化子类型(后文中有例子)。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容