VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > Python基础教程 >
  • PEP 484 类型提示 -- Python官方文档译文 [原创](3)

,即等效于 collections.abc.Callable

from typing import Tuple, List, Callable

def check_args(args: Tuple) -> bool:
    ...

check_args(())           # OK
check_args((42, 'abc'))  # Also OK
check_args(3.14)         # Flagged as error by a type checker

# A list of arbitrary callables is accepted by this function
def apply_callbacks(cbs: List[Callable]) -> None:
    ...

NoReturn 类型(The NoReturn type)

typing 模块提供了一种特殊的类型 NoReturn,用于注解一定不会正常返回的函数。例如一个将无条件引发异常的函数:

from typing import NoReturn

def stop() -> NoReturn:
    raise RuntimeError('no way')

类型注解 NoReturn 用于 sys.exit 之类的函数。静态类型检查程序将会确保返回类型注解为 NoReturn 的函数确实不会隐式或显式地返回:

import sys
from typing import NoReturn

  def f(x: int) -> NoReturn:  # Error, f(0) implicitly returns None
      if x != 0:
          sys.exit(1)

类型检查程序还会识别出调用此类函数后面的代码是否可达,并采取相应动作:

# continue from first example
def g(x: int) -> int:
    if x > 0:
        return x
    stop()
    return 'whatever works'  # Error might be not reported by some checkers
                             # that ignore errors in unreachable blocks

NoReturn 类型仅可用于函数的返回类型注解,出现在其他位置则被认为是错误:

from typing import List, NoReturn

# All of the following are errors
def bad1(x: NoReturn) -> int:
    ...
bad2 = None  # type: NoReturn
def bad3() -> List[NoReturn]:
    ...

类对象的类型(The type of class objects)

有时会涉及到类对象,特别是从某个类继承而来的类对象。类对象可被写为 Type[C],这里的 C 是一个类。为了清楚起见,C 在用作类型注解时指的是类 C 的实例,Type[C] 指的是 C 的子类。这类似于对象和类型之间的区别。

例如,假设有以下类:

class User: ...  # Abstract base for User classes
class BasicUser(User): ...
class ProUser(User): ...
class TeamUser(User): ...

假设有一个函数,如果传一个类对象进去,就会创建出该类的一个实例:

def new_user(user_class):
    user = user_class()
    # (Here we could write the user object to a database)
    return user

若不用 Type[],能给 new_user() 加上的最好的类型注解将会是:

def new_user(user_class: type) -> User:
    ...

但采用 Type[] 和带上界的类型变量,就可以注解得更好:

U = TypeVar('U', bound=User)
def new_user(user_class: Type[U]) -> U:
    ...

现在,若用 User 的某个子类做参数调用 new_user(),类型检查程序将能推断出结果的正确类型:

joe = new_user(BasicUser)  # Inferred type is BasicUser

Type[C] 对应的值必须是类型为 C 的子类型的类对象实体,而不是某个具体的类型。换句话说,在上述示例中,new_user(Union[BasicUser, ProUser]) 之类的调用将被类型检查程序拒绝(并且会运行失败,因为 union 无法实例化)。

请注意,用类的 union 作 Type[] 的参数是合法的,如下所示:

def new_non_team_user(user_class: Type[Union[BasicUser, ProUser]]):
    user = new_user(user_class)
    ...

但是,在运行时上例中传入的实际参数仍必须是具体的类对象:

new_non_team_user(ProUser)  # OK
new_non_team_user(TeamUser)  # Disallowed by type checker

Type[Any] 也是支持的,含义参见下文。

为类方法的第一个参数标注类型注解时,允许采用 Type[T],这里的 T 是一个类型变量,具体请参阅相关章节。

任何其他的结构(如 Tuple 或 Callable)均不能用作 Type 的参数。

此特性存在一些问题:比如若 new_user() 要调用 user_class(),就意味着 User 的所有子类都必须在其构造函数的签名中支持该调用。不过并不是只有 Type[] 才会如此,类方法也有类似的问题。类型检查程序应该将违反这种假定的行为标记出来,但与所标明基类(如上例中的 User)的构造函数签名相符的构造函数,应该默认是允许调用的。如果程序中包含了比较复杂的或可扩展的类体系,也可以采用工厂类方法来作处理。本 PEP 的未来修订版本可能会引入更好的方法来解决这些问题。

当 Type 带有参数时,仅要求有一个参数。不带中括号的普通类型等效于 Type[Any],也即等效于 type(Python 元类体系中的根类)。这种等效性也促成了其名称 Type,而没有采用 Class 或 SubType 这种名称,在讨论此特性时这些名称都被提出过,这有点类似 List 和 list 的关系。

关于 Type[Any](或 TypeType)的行为,如果要访问该类型变量的属性,则只提供了 type 定义的属性和方法(如 __repr__() 和 __mro__)。此类变量可以用任意参数进行调用,返回类型则为 Any

Type 的参数是协变的,因为 Type[Derived] 是 Type[Base] 的子类型:

def new_pro_user(pro_user_class: Type[ProUser]):
    user = new_user(pro_user_class)  # OK
    ...

为实例和类方法加类型注解(Annotating instance and class methods)

大多数情况下,类和实例方法的第一个参数不需要加类型注解,对实例方法而言假定它的类型就是所在类(的类型),对类方法而言它则是所在类对象对应的类型对​​象(的类型)。另外,实例方法的第一个参数加类型注解时可以带有一个类型变量。这时返回类型可以采用相同的类型变量,从而使该方法成为泛型函数。例如:

T = TypeVar('T', bound='Copyable')
class Copyable:
    def copy(self: T) -> T:
        # return a copy of self

class C(Copyable): ...
c = C()
c2 = c.copy()  # type here should be C

同样,可以对类方法第一个参数的类型注解中使用 Type[]

T = TypeVar('T', bound='C')
class C:
    @classmethod
    def factory(cls: Type[T]) -> T:
        # make a new instance of cls

class D(C): ...
d = D.factory()  # type here should be D

请注意,某些类型检查程序可能对以上用法施加限制,比如要求所用类型变量具备合适的类型上界(参见示例)。

版本和平台检查(Version and platform checking)

类型检查程序应该能理解简单的版本和平台检查语句,例如:

import sys

if sys.version_info[0] >= 3:
    # Python 3 specific definitions
else:
    # Python 2 specific definitions

if sys.platform == 'win32':
    # Windows specific definitions
else:
    # Posix specific definitions

请别指望类型检查程序能理解诸如 "".join(reversed(sys.platform)) == "xunil" 这种晦涩语句。

运行时检查还是类型检查?(Runtime or type checking?)

有时候,有些代码必须由类型检查程序(或其他静态分析工具)进行检查,而不应拿去运行。typing 模块为这种情况定义了一个常量 TYPE_CHECKING,在类型检查(或其他静态分析)期间视其为 True,在运行时视其为 False。例如:

import typing

if typing.TYPE_CHECKING:
    import expensive_mod

def a_func(arg: 'expensive_mod.SomeClass') -> None:
    a_var = arg  # type: expensive_mod.SomeClass
    ...

注意,这里的类型注解必须用引号引起来,使其成为“向前引用”,以便向解释器隐藏 expensive_mod 引用。在 # type 注释中无需加引号。

这种做法对于处理循环导入也会比较有用。

可变参数列表和默认参数值(Arbitrary argument lists and default argument values)

可变参数列表也可以加注类型注解,以下定义是可行的:

def foo(*args: str, **kwds: int): ...

这表示以下函数调用的参数类型都是合法的:

foo('a', 'b', 'c')
foo(x=1, y=2)
foo('', z=0)

在 foo 函数体中,变量 args 的类型被推导为 Tuple[str, ...],变量 kwds 的类型被推导为 Dict [str, int]

在存根(stub)文件中,将参数声明为带有默认值,但不指定实际的默认值,这会很有用。例如:

def foo(x: AnyStr, y: AnyStr = ...) -> AnyStr: ...

默认值应该是如何的?""b"" 或 None 都不符合类型约束。

这时可将默认值指定为省略号,其实就是以上示例。

只采用位置参数(Positional-only arguments)

有一些函数被设计成只能按位置接收参数,并希望调用者不要使用参数名称,不通过关键字给出参数。名称以__开头的参数均被假定为只按位置访问,除非同时以__结尾:

def quux(__x: int, __y__: int = 0) -> None: ...

quux(3, __y__=1)  # This call is fine.

quux(__x=3)  # This call is an error.

为生成器函数和协程加类型注解(Annotating generator functions and coroutines)

生成器函数的返回类型可以用 type.py 模块提供的泛型 Generator[yield_type, send_type, return_type] 进行类型注解:

def echo_round() -> Generator[int, float, str]:
    res = yield
    while res:
        res = yield round(res)
    return 'OK'

PEP 492 中引入的协程(coroutine)可用与普通函数相同的语法进行类型注解。但是,返回类型的类型注解对应的是 await 表达式的类型,而不是协程的类型:

async def spam(ignored: int) -> str:
    return 'spam'

async def foo() -> None:
    bar = await spam(42)  # type: str

type.py 模块提供了一个抽象基类 collections.abc.Coroutine 的泛型版本,以支持可异步调用(awaitable)特性,同时支持 send() 和 throw() 方法。类型变量定义及其顺序与 Generator 的相对应,即 Coroutine[T_co, T_contra, V_co],例如:

from typing import List, Coroutine
c = None  # type: Coroutine[List[str], str, int]
...
x = c.send('hi')  # type: List[str]
async def bar() -> None:
    x = await c  # type: int

该模块还为无法指定更精确类型的情况提供了泛型抽象基类 AwaitableAsyncIterable 和 AsyncIterator

def op() -> typing.Awaitable[str]:
    if cond:
        return spam(42)
    else:
        return asyncio.Future(...)

与函数注解其他用法的兼容性(Compatibility with other uses of function annotations)

有一些函数注解的使用场景,与类型提示是不兼容的。这些用法可能会引起静态类型检查程序的混乱。但因为类型提示的注解在运行时不起作用(计算注解表达式、将注解存储在函数对象的 __annotations__ 属性中除外),所以不会让程序报错,只是可能会让类型检查程序发出虚报警告或错误。

如果要让某部分程序不受类型提示的影响,可以用以下一种或几种方法进行标记:

  • 用 # type: ignore 加注释(comment);
  • 为类或函数加上 @no_type_check 装饰符(decorator);
  • 为自定义类或函数装饰符加上 @no_type_check_decorator 标记。

更多详情,请参见后续章节。

为了最大程度与脱机类型检查过程保持兼容,将依赖于类型注解的接口改成其他机制(例如装饰器)可能比较合适些。不过这在 Python 3.5 中没什么关系。更多讨论请参见后续的“未被采纳的其他方案”。

类型注释(Type comments)

本 PEP 并未将变量明确标为某类型提供一等语法支持。为了有助于在复杂情况下进行类型推断,可以采用以下格式的注释:

x = []                # type: List[Employee]
x, y, z = [], [], []  # type: List[int], List[int], List[str]
x, y, z = [], [], []  # type: (List[int], List[int], List[str])
a, b, *c = range(5)   # type: float, float, List[float]
x = [1, 2]            # type: List[int]

类型注释应放在变量定义语句的最后一行,还可以紧挨着冒号放在 with 和 for 语句后面。

以下是with 和 for 语句的类型注解示例:

with frobnicate() as foo:  # type: int
    # Here foo is an int
    ...

for x, y in points:  # type: float, float
    # Here x and y are floats
    ...

在存根(stub)文件中,只声明变量的存在但不给出初值可能会比较有用。这用 PEP 526 的变量注解语法即可实现:

from typing import IO

stream: IO[str]

上述语法在所有版本的 Python 的存根文件中均可接受。但在 Python 3.5 以前版本的非存根文件代码中,存在一种特殊情况:

from typing import IO

stream = None  # type: IO[str]

尽管 None 与给定类型不符,类型检查程序不应对上述语句报错,也不应将类型推断结果更改为 Optional[...](虽然规则要求对注解默认值为 None 的参数如此操作)。这里假定将由其他代码保证赋予变量类型合适的值,并且所有调用都可假定该变量具有给定类型。

注释 # type: ignore 应该放在错误信息所在行上:

import http.client
errors = {
    'not_found': http.client.NOT_FOUND  # type: ignore
}

如果注释 # type: ignore 位于文件的开头、单独占一行、在所有文档字符串(docstring)、import 语句或其他可执行代码之前,则会让文件中所有错误都不报错。空行和其他注释(如 shebang 代码行和编码 cookie)可以出现在 # type: ignore 之前。

某些时候,类型注释可能需要与查错(lint)工具或其他注释同处一行。此时类型注释应位于其他注释和 lint 标记之前:

# type: ignore # <comment or other marker>

如果大多时候类型提示能被证明有用,那么将来版本的 Python 可能会为 typing 变量提供语法。

更新:该语法已通过 PEP 526 在 Python 3.6 加入。

指定类型(Cast)

偶尔,类型检查程序可能需要另一种类型提示:程序员可能知道,某个表达式的类型比类型检查程序能够推断出来的更为准确。例如:

from typing import List, cast

def find_first_str(a: List[object]) -> str:
    index = next(i for i, x in enumerate(a) if isinstance(x, str))
    # We only get here if there's at least one string in a
    return cast(str, a[index])

某些类型检查程序可能无法推断出 a[index] 的类型为 str,而只能推断出是个对象或 Any,但大家都知道(如果代码能够运行到该点)它必须是个字符串。ast(t, x) 调用会通知类型检查程序,确信 x 的类型就是 t。在运行时,cast 始终会原封不动地返回表达式,不作类型检查,也不对值作任何转换或强制转换。

cast 与类型注释(参见上一节)不同。用了类型注释,类型检查程序仍应验证推断出的类型是否与声明的类型一致。若用了 cast ,类型检查程序就会完全信任程序员。cast 还可以在表达式中使用,而类型注释则只能在赋值时使用。

NewType 工具函数(NewType helper function)

还有些时候,为了避免逻辑错误,程序员可能会创建简单的类。例如:

class UserId(int):
    pass

get_by_user_id(user_id: UserId):
    ...

但创建类会引入运行时的开销。为了避免这种情况,typeing.py 提供了一个工具函数 NewType,该函数能够创建运行开销几乎为零的唯一简单类型。对于静态类型检查程序而言,Derived = NewType('Derived', Base) 大致等同于以下定义:

class Derived(Base):
    def __init__(self, _x: Base) -> None:
        ...

在运行时,NewType('Derived', Base) 将返回一个伪(dummy)函数,该伪函数只是简单地将参数返回。类型检查程序在用到 UserId 时要求由 int 显式转换(cast)而来,而用到 int 时要求由 UserId 显式转换而来。例如:

UserId = NewType('UserId', int)

def name_by_id(user_id: UserId) -> str:
    ...

UserId('user')          # Fails type check

name_by_id(42)          # Fails type check
name_by_id(UserId(42))  # OK