美文网首页
Python 函数注解与类型注解

Python 函数注解与类型注解

作者: 水之心 | 来源:发表于2020-12-03 10:42 被阅读0次

    参考: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 ,用户能够定义新的自定义协议来充分享受结构化子类型(后文中有例子)。

    相关文章

      网友评论

          本文标题:Python 函数注解与类型注解

          本文链接:https://www.haomeiwen.com/subject/fosywktx.html