PEP483——类型提示理论

1. 摘要

本 PEP 展示了 PEP 484 引用的理论。

2. 简介

本文档展示了 Python 3.5 的新型类型提示建议的理论。因为尚有很多细节需要制定,所以该 PEP 还算不上是一个完整的提案或规范,但是如果没有该 PEP 介绍的这些理论,就很难对更详细的规范进行讨论。我们先回顾类型理论的基本概念,再解释渐进类型(gradual typing),然后声明一些通用规则,并定义可以在注释中使用的新的特殊类型(如 Union);最后,我们定义了泛型类型的方法和类型提示的语用学方面(and finally we define the approach to generic types and pragmatic aspects of type hinting)。

2.1 符号约定

  • t1t2u1u2 等表示类型。有时使用 titj 表示t1t2 等中的任意一个。

  • T, U 等表示类型变量(使用 TypeVar() 定义,参见下文)。

  • 对象、类由 class 语句定义,实例使用标准的 PEP 8 约定来表示。

  • 在此PEP上下文中应用于类型的符号 == 表示两个表达式表示同一类型。

  • 请注意, PEP 484 区分了类型(type)和类(class)(类型是类型检查器的概念,而类是运行时概念)。在此PEP中,我们澄清了这种区别,但避免了不必要的严格性,从而在类型检查器的实现中提供更多的灵活性。

3. 背景

文献中有许多关于类型概念的定义。在这里,我们假设类型是一组值和一组可以应用于这些值的函数。

有几种定义特定类型的方法:

  • 通过显式列出所有值。比如 TrueFalse 形成了 bool 类型。

  • 通过指定可与某种类型的变量一起使用的函数。例如。具有 __len__ 方法的所有对象均形成 Sized 类型。 [1、2、3]'abc' 都属于此类型,因为可以使用这些类型的变量调用 len 函数:

    len([1, 2, 3])  # OK
    len('abc')      # also OK
    len(42)         # not a member of Sized
    
  • 通过简单的类定义,例如,定义如下的一个类:

    class UserID(int):
        pass
    
  • 还有更复杂的类型。例如,可以将类型 FancyList 定义为仅包含 intstr 或其子类实例的所有列表。值[1,'abc',UserID(42)] 具有此类型。

对于用户而言,重要的是能够以类型检查器可以理解的形式定义类型。该PEP的目标是提出一种使用 PEP 3107 语法为变量和函数的类型注释定义类型的系统方法。这些注释可用于避免许多错误、用作文档,甚至可用于提高程序执行的速度。在这里,我们仅专注于通过使用静态类型检查器来避免错误。

3.1 子类型关系

静态类型检查器的关键概念是子类型关系。子类型关系来自于以下问题:如果 first_var 的类型为 first_typesecond_var 的类型为 second_type,那么赋值语句 first_var = second_var 是否安全呢?

关于何时 应该 是安全的的一个严格标准是:

  • second_type 中的每个值也位于 first_type 的值集中;并且
  • first_type 中的每个函数也都在 second_type 的函数集中。

定义的这个关系既是子类型关系。

根据以上定义:

  • 每个类型均为其自身的子类型。
  • 在子类型化过程中,值的集合变小,而函数的集合变大。

一个直观的例子:每个 Dog 都是 Animal,而 Dog 也具有更多功能,例如可以吠叫,因此,DogAnimal 的子类型。相反,Animal 不是 Dog 的子类型。

一个更正式的例子:整数是实数的子类型。实际上,每个整数当然也是一个实数,并且整数支持更多的运算,例如按位移位 <<>>

lucky_number = 3.14    # type: float
lucky_number = 42      # Safe
lucky_number * 2       # This works
lucky_number << 5      # Fails

unlucky_number = 13    # type: int
unlucky_number << 5    # This works
unlucky_number = 2.72  # Unsafe

让我们再考虑一个棘手的示例:如果 List[int] 表示由仅包含整数的所有列表构成的类型,则它 不是 List[float] 的子类型,List[float] 是由只包含实数的所有列表组成的。子类型化的第一个条件成立,但将一个实数追加到列表末尾则仅适用于 List[float],因此第二个条件失败:

def append_pi(lst: List[float]) -> None:
    lst += [3.14]

my_list = [1, 3, 5]  # type: List[int]

append_pi(my_list)   # Naively, this should be safe...

my_list[-1] << 5     # ... but this fails

有两种广泛的方法可以向类型检查器声明子类型信息。

在名义子类型(nominal subtyping)中,类型树基于类树,即 UserID 被视为 int 的子类型。此方法应在类型检查器的控制下使用,因为在Python中,可以以不兼容的方式覆盖属性:

class Base:
    answer = '42' # type: str

class Derived(Base):
    answer = 5 # should be marked as error by type checker

在结构子类型(structural subtyping)中,子类型关系是从声明的方法中推导出来的,例如,UserIDint 将被视为同一类型。尽管这有时可能会引起混乱,但结构子类型被认为更灵活。我们努力为这两种方法提供支持,这样除了名义子类型之外,还可以使用结构信息。

4. 渐进类型概论

渐进类型(gradual typing)允许仅注释程序的一部分,从而可以利用动态和静态类型的可取方面。

我们定义一个新的关系 is-consistent-with,它与 is-subtype-of 类似,不同之处在于当涉及到新的 Any 类型时,is-consisten-with 关系是不可传递的。 (两种关系都是不对称的。)如果 a_value 的类型与 a_variable 的类型一致,则可以将 a_value 赋值给 a_variable。 (将其与表示面向对象编程的基本原理之一的 “如果 a_value 的类型是 a_variable 的类型的子类型” 进行比较)is-consistent-with 关系由三个规则定义:

  • 如果 t1t2 的子类型,则类型 t1 与 类型t2 一致。(反之则不成立。)
  • Any 与每种类型都一致。(但 Any 并不是每种类型的子类型。)
  • 每种类型都与 Any 一致。(但每种类型都不是 Any 的子类型)

以上就是 is-consistent-with 关系的定义!有关更长的解释和动机,请参见 Jeremy Siek 的博客文章 “什么是渐进类型”。Any类型可以视为具有所有值和所有方法的类型。结合上面的子类型定义,这将 Any 局部地放在类型层次结构的顶部(具有所有值)和底部(具有所有方法)。将此与 object 进行对比——与大多数类型不一致(例如,您不能在需要 int 的地方使用object() 实例)。 换句话说, Anyobject 在用于注解参数时均表示 “允许任何类型”,但是,无论预期的类型是什么,都只能传递 Any(本质上,Any 声明回退到动态类型并关闭来自静态检查器的抱怨)。

下面这些示例显示了上述规则在实践中如何工作:

假定有 Employee 类及其子类 Manager

class Employee: ...
class Manager(Employee): ...

假定变量 worker 声明为 Employee 类型:

worker = Employee()  # type: Employee

现在将 Manager 实例赋值给 worker 是可以的(规则1):

worker = Manager()

但是将 Employee 类型的实例赋值给声明为 Manager 类型的变量则是不行的:

boss = Manager()  # type: Manager
boss = Employee()  # Fails static check

然而,假设有一个类型为 Any 的变量:

something = some_func()  # type: Any

则将 something 赋值给 worker 是没问题的(规则2):

worker = something  # OK

当然将 worker 赋值给 something 也没问题(规则3),但这不需要用到一致性的概念。

something = worker  # OK

4.1 类型 vs 类

在Python中,类是由 class 语句定义的对象工厂,并由 type(obj) 内置函数返回。类是一个动态的运行时概念。

类型概念已在上面进行了描述,类型出现在变量和函数类型注解中,可以由下面描述的构造块构造,并由静态类型检查器使用。

每个类都是如上所述的类型。但是,要实现一个类来精确表示给定类型的语义是棘手且容易出错的,这不是 PEP 484 的目标。不应将 PEP 484 中描述的静态类型与运行时类混淆。例如:

  • int 是类,同时也是类型。

  • UserID 是类,同时也是类型。

  • Union[str, int] 是类型,但不是一个合适的类:

    class MyUnion(Union[str, int]): ...  # raises TypeError
    
    Union[str, int]()  # raises TypeError
    

类型接口是通过类实现的,即在运行时可以进行诸如 Generic[T].__bases__ 这样的计算。但是要强调类和类型之间的区别,请遵循以下一般规则:

  • 下面定义的任何类型(即 AnyUnion 等)都不能被实例化,尝试这样做将引发 TypeError。 (但可以子类化 Generic 的非抽象子类。)
  • 除了 Generic 和从其派生的类之外,下面定义的任何类型都不能被子类化。
  • 如果它们中的任何一个出现在 isinstanceissubclass 中(未参数化的泛型除外),都会引发TypeError

4.2 基本构件块

  • Any

    任何类型均与 Any 一致,Any 也与任何类型一致(参见上文)。

  • Union[t1, t2, …]

    属于 t1 等中 至少一个 的子类型的类型是该类型的子类型。

    • 成员都是 t1 等的子类型的 Union 是该类型的子类型。比如 Union[int, str] 就是 Union[int, float, str] 的子类型。
    • 参数的顺序无关紧要。例如 Union[int, str] == Union[str, int]
    • 如果 ti 本身是 Union,则结果将进行扁平化处理。例如:Union[int, Union[float, str]] == Union[int, float, str]
    • 如果 titj 具有子类型关系,则保留更泛化的类型。例如:Union[Employee, Manager] == Union[Employee]
    • Union[t1] 仅返回 t1Union[]Union[()] 都是非法的。
    • 很显然,Union[..., object, ...] 将返回 object
  • Optional[t1]

    Union[t1, None] 的别名,例如 Union[t1, type(None)]

  • Tuple[t1, t2, …, tn]

    t1 等类型的实例构成的元组。比如 Tuple[int, float] 表示包含两个项的元组,第一项是 int 类型,第二项是 float 类型,例如 (42, 3.14)

    • 如果 Tuple[u1, u2, ..., um]Tuple[t1, t2, ..., tn] 具有相同的长度,即 n == m,并且对于每一个 uiti ,前者都是后者的子类型,那么 Tuple[u1, u2, ..., um]Tuple[t1, t2, ..., tn] 的子类型。
    • 空元组的类型使用 Tuple[()] 表示。
    • 可变的齐次元组类型可以写为 Tuple[t1, ...]。 (其中的三个点是 Python 中的 ellipse 类的单例对象)
  • Callable[[t1, t2, …, tn], tr]

    位置参数类型为 t1等、返回类型为 tr 的函数。参数列表可以为空,即 n == 0。无法指示可选参数或关键字参数,也无法指示可变参数,但是可以通过 Callable[..., tr] 这样的形式来表示完全不检查参数列表。

也许还会加入如下一些类型:

  • Intersection[t1, t2, …]

    属于 t1每一个 的子类型的类型是该类型的子类型。 (与 Union 相比,Union 在其定义中使用了 至少一个 ,而 Intersection 使用了 每一个

    • 参数的顺序无关紧要。嵌套的 Intersection 将被扁平化,例如, Intersection[int, Intersection[float, str]] == Intersection[int, float, str]
    • 包含的类型较少的 Intersection 是包含的类型较多的 Intersection 的超类型。比如, Intersection[int, str]Intersection[int, float, str] 的超类型。
    • 只有一个参数的 Intersection 就是参数本身的类型。比如 Intersection[int] 就是 int
    • 如果参数包含子类型关系,则保留更具体的类型,比如 Intersection[str, Employee, Manager] 就是 Intersection[str, Manager]
    • Intersection[]Intersection[()] 都是非法的。
    • 很显然,参数列表中的 Any 将会消失,例如 Intersection[int, str, Any] == Intersection[int, str]Intersection[Any, object] 就是 object
    • IntersectionUnion 的关系是比较复杂的,但如果理解了普通集合的交集和并集,就比较容易理解它们之间的关系了(注意,类型的集合的大小可以是无限的,因为新的子类的数量是没有限制的。)

5. 泛型类型

上面定义的基本构建块允许以泛型方式构造新类型。例如,Tuple 可以使用具体类型 float 创建一个新的具体类型 Vector = Tuple[float, ...],或者可以使用另一种类型 UserID 创建另一种新的具体类型 Registry = Tuple[UserID, ...]。这种语义称为泛型类型构造器,它类似于函数的语义,但是函数接受值并返回值,而泛型类型构造器接受类型并"返回"类型。

特定的类或函数以这种类型的泛型方式运行是很常见的。考虑两个例子:

  • 容器类,比如 listdict,通常只包含特定类型的值。因此用户可能想使用如下的方式进行注解:

    users = [] # type: List[UserID]
    users.append(UserID(42)) # OK
    users.append('Some guy') # 应该被类型检查器拒绝
    
    examples = {} # type: Dict[str, Any]
    examples['first example'] = object() # OK
    examples[2] = None                   # 被类型检查器拒绝
    
  • 下面的函数可以接受两个 int 类型的参数并返回一个 int 类型的值,也可以接受两个 float 类型的参数并返回一个 float 类型的值,等等:

    def add(x, y):
        return x + y
    
    add(1, 2) == 3
    add('1', '2') == '12'
    add(2.7, 3.5) == 6.2
    

为了能够在第一个示例所示的情况下进行类型注解,内置容器和容器抽象基类使用类型参数进行了扩展,从而它们的行为就像泛型类型构造器一样。作为泛型类型构造器的类称为 泛型类型。例如:

from typing import Iterable

class Task:
    ...

def work(todo_list: Iterable[Task]) -> None:
    ...

此处的 Iterable 就是一个泛型类型,它接受具体类型 Task ,并返回具体类型 Iterable[Task]

表现为类型泛型方式的函数(如第二个示例所示)称为 泛型函数。泛型函数的类型注解允许使用类型变量 。带有遵循泛型类型的类型变量的语义与函数中参数的语义有些相似。但是没有为类型变量分配具体的类型,静态类型检查器的任务是找到它们的可能值,并在找不到时警告用户。例如:

def take_first(seq: Sequence[T]) -> T: # a generic function
    return seq[0]

accumulator = 0 # type: int

accumulator += take_first([1, 2, 3])   # 安全,T 推断为 int
accumulator += take_first((2.7, 3.5))  # 不安全

类型变量广泛应用于类型注解中,类型检查器中类型推断的内部机制通常也建立在类型变量之上。因此,让我们详细介绍一下它们。

5.1 类型变量

X = TypeVar('X') 声明一个唯一的类型变量。名称必须与变量名称匹配。默认情况下,类型变量覆盖所有可能的类型。例:

def do_nothing(one_arg: T, other_arg: T) -> None:
    pass

do_nothing(1, 2)               # OK, T is int
do_nothing('abc', UserID(42))  # also OK, T is object

Y = TypeVar('Y', t1, t2, ...) 同上,约束为 t1 等类型。其行为与Union[t1, t2, ...] 类似。受约束的类型变量仅在约束 t1 等范围内精确地变化;约束的子类被 t1 等中最底层(most-derived)的基类替换。示例:

  • 具有受限类型变量的函数类型注解:

    S = TypeVar('S', str, bytes)
    
    def longest(first: S, second: S) -> S:
        return first if len(first) >= len(second) else second
    
    result = longest('a', 'abc')  #推断出的返回值类型为 str
    
    result = longest('a', b'abc')  # 静态类型检查失败
    

    上述例子中,longest() 两个参数的类型必须相同(strbytes),而且,即便参数是 str 的子类,longest() 函数的返回值的类型依然会是 str,而不是 str 的那个子类(参见以下例子)。

  • 以下例子作为对照,如果类型变量是非受限的,将会把子类作为返回值的类型,比如:

    S = TypeVar('S')
    
    def longest(first: S, second: S) -> S:
        return first if len(first) >= len(second) else second
    
    class MyStr(str): ...
    
    result = longest(MyStr('a'), MyStr('abc'))
    

    result 的推断类型为 MyStr(而在 AnyStr 示例中为 str)。

  • 以下仍然作为对照,如果使用 Union,则返回类型必须为 Union

    U = Union[str, bytes]
    
    def longest(first: U, second: U) -> U:
        return first if len(first) >= len(second) else second
    
    result = longest('a', 'abc')
    

    即使两个参数均为 str,上述 result 的推断类型仍为 Union[str, bytes]

    请注意,类型检查器将拒绝此函数:

    def concat(first: U, second: U) -> U:
        return x + y  # 错误:  str and bytes
    

    对于此类参数只能同时更改其类型的情况,应使用受约束的类型变量。

5.2 泛型类型的定义和使用

可以使用特殊的构造块 Generic 将类声明为泛型类型。定义 class MyGeneric(Generic[X, Y, ...]): ... 定义了类型变量 X 等的泛型类型 MyGenericMyGeneric 本身成为可参数化的,例如, MyGeneric[int, str, ...] 是一种带有替换 X -> int 等的特殊类型。例如:

class CustomQueue(Generic[T]):

    def put(self, task: T) -> None:
        ...
    def get(self) -> T:
        ...

def communicate(queue: CustomQueue[str]) -> Optional[str]:
    ...

由泛型类型派生的类就成了泛型。一个类可以子类化多个泛型类型。但是泛型返回的特定类型的派生类则不是泛型。例如:

class TodoList(Iterable[T], Container[T]):
    def check(self, item: T) -> None:
        ...

def check_all(todo: TodoList[T]) -> None:  # TodoList is generic
    ...

class URLList(Iterable[bytes]):
    def scrape_all(self) -> None:
        ...

def search(urls: URLList) -> Optional[bytes]  # URLList is not generic
    ...

对泛型类型进行子类化会将子类型关系强加到对应的特定类型上,因此在上述例子中, TodoList[t1]Iterable[t1] 的子类型。

泛型类型可以通过几个步骤进行专门化(索引化)。每个类型变量都可以由特定类型或其他泛型类型替换。如果 Generic 出现在基类列表中,则它应包含所有类型变量,并且类型参数的顺序由它们在 Generic 中出现的顺序决定。例子:

Table = Dict[int, T]     # Table is generic
Messages = Table[bytes]  # Same as Dict[int, bytes]

class BaseGeneric(Generic[T, S]):
    ...

class DerivedGeneric(BaseGeneric[int, T]): # DerivedGeneric has one parameter
    ...

SpecificType = DerivedGeneric[int]         # OK

class MyDictView(Generic[S, T, U], Iterable[Tuple[U, T]]):
    ...

Example = MyDictView[list, int, str]       # S -> list, T -> int, U -> str

如果泛型类型出现在类型注解中,并且省略了类型变量,则类型变量将被假设为 Any。这种形式可以用作动态类型的后备,并且可以与 issubclassisinstance 一起使用。实例中的所有类型信息都会在运行时擦除。例如:

def count(seq: Sequence) -> int:      # Same as Sequence[Any]
    ...

class FrameworkBase(Generic[S, T]):
    ...

class UserClass:
    ...

issubclass(UserClass, FrameworkBase)  # This is OK

class Node(Generic[T]):
   ...

IntNode = Node[int]
my_node = IntNode()  # at runtime my_node.__class__ is Node
                     # inferred static type of my_node is Node[int]

5.3 协变和逆变(Covariance and Contravariance)


假设 t2t1 的子类型,那么泛型类型构造器 GenType 被称为:

  • 协变(Covariant):如果对所有这样的 t1t2GenType[t2]GenType[t1] 的子类型。
  • 逆变(Contravariant):如果对所有这样的 t1t2GenType[t1]GenType[t2] 的子类型。
  • 不变(invariant):如果上述两条均不成立。

为了更好地理解以上定义,让我们用普通函数来进行分析。假定有以下函数:

def cov(x: float) -> float:
    return 2*x

def contra(x: float) -> float:
    return -x

def inv(x: float) -> float:
    return x*x

如果 x1 < x2,则 总是cov(x1) < cov(x2)contra(x2) < contra(x1),但对于 inv 则无法确定。将 < 替换为 “是…的子类型”(is-subtype-of),将函数替换为泛型类型构造器,就得到了协变、逆变和不变行为的例子。

下面看几个示例:

  • Union 会对所有参数表现为协变。实际上,如上所述,如果 t1 等是 u1 等类型的子类型,则 Union[t1, t2, ...] 就是 Union[u1, u2, ...] 的子类型。
  • FrozenSet[T] 也是协变的。不妨将 T 替换为 intfloat。首先 intfloat 的子类型;其次 FrozenSet[int] 的值集显然是 FrozenSet[float] 的值集的子集,而 FrozenSet[float] 的函数集则是FrozenSet[int] 的函数集的子集。因此,根据定义,FrozenSet[int]FrozenSet[float] 的子类型。
  • List[T] 是不变的。实际上,尽管 List[int] 的值集是 List[float] 的值集的子集,但只有 int 能够附加到 List[int] 中去。因此, List[int] 不是 List[float] 的子类型。这是可变类型的典型情况,他们通常是不变的。

可调用类型是说明(有点违反直觉)逆变行为的最佳示例之一。它在返回类型上是协变的,但参数类型则是逆变的。对于仅仅是返回类型不同的两个可调用类型,可调用类型的子类型关系遵循返回类型的子类型关系。例如:

  • Callable[[], int]Callable[[], float] 的子类型。
  • Callable[[], Manager]Callable[[], Employee] 的子类型。

而对于仅有一个参数类型不同的两个可调用类型,则其子类型关系与参数类型的子类型关系是 相反的。例如:

  • Callable[[float], None]Callable[[int], None] 的子类型。
  • Callable[[Employee], None]Callable[[Manager], None] 的子类型。

是的,你没有看错。确实,如果期望计算经理薪资的函数为:

def calculate_all(lst: List[Manager], salary: Callable[[Manager], Decimal]):
    ...

那么可以为任何雇员计算薪水的 Callable[[Employee], Decimal] 也是可以接受的。

Callable 的示例说明了如何为函数进行更精确的类型注解:为每个参数选择最通用的类型,为返回值选择最特定的类型

通过使用特殊的关键字 covariantcontravariant 来定义作为参数的类型变量,可以声明用户定义的泛型类型的协变性和逆变性。默认情况下,类型是不变的。例如:

T = TypeVar('T')
T_co = TypeVar('T_co', covariant=True)
T_contra = TypeVar('T_contra', contravariant=True)

class LinkedList(Generic[T]):  # invariant by default
    ...
    def append(self, element: T) -> None:
        ...

class Box(Generic[T_co]):      #  this type is declared covariant
    def __init__(self, content: T_co) -> None:
        self._content = content
    def get_content(self) -> T_co:
        return self._content

class Sink(Generic[T_contra]): # this type is declared contravariant
    def send_to_nowhere(self, data: T_contra) -> None:
        with open(os.devnull, 'w') as devnull:
            print(data, file=devnull)

注意,尽管协变性和逆变性是通过类型变量定义的,但它不是类型变量的属性,而是泛型类型的属性。在派生的泛型的复杂定义中,协变性和逆变性仅由使用的类型变量确定。下面是一个复杂的例子:

T_co = TypeVar('T_co', Employee, Manager, covariant=True)
T_contra = TypeVar('T_contra', Employee, Manager, contravariant=True)

class Base(Generic[T_contra]):
    ...

class Derived(Base[T_co]):
    ...

类型检查器从第二个声明中发现 Derived[Manager]Derived[Employee] 的子类型,而 Derived[t1]Base[t1] 的子类型。如果我们用 < 表示 “is-subtype-of” 关系,那么这种情况下的子类型的完整图将是:

  Base[Manager]    >  Base[Employee]
      v                   v
  Derived[Manager] <  Derived[Employee]

因此类型检查器也将发现这些关系,比如 Derived[Manager] 就是 Base[Employee] 的子类型。

关于类型变量、泛型、协变性、逆变性的更多信息,请参阅 PEP 484mypy 中有关泛型的文档Wikipedia

6. 语义学

有些事情与理论无关,但实用起来更方便。 (这不是完整的列表;我可能漏掉了一些,有些仍然有争议或未完全说明。)

  • 在需要类型的地方,None 可以代替 type(None)。例如: Union[t1, None] == Union[t1, type(None)]

  • 类型别名,例如:

    Point = Tuple[float, float]
    def distance(point: Point) -> float: ...
    
  • 通过字符串进行前向引用(Forward references),例如:

    class MyComparable:
        def compare(self, other: 'MyComparable') -> int: ...
    
  • 类型变量可以以不受约束(unconstrained)、受约束(constrained)或有界(bounded)的形式声明。泛型类型的变化也可以使用使用特殊关键字参数声明的类型变量来表示,从而避免使用任何特殊语法,例如:

    T = TypeVar('T', bound=complex)
    
    def add(x: T, y: T) -> T:
        return x + y
    
    T_co = TypeVar('T_co', covariant=True)
    
    class ImmutableList(Generic[T_co]): ...
    
  • 注释中的类型声明,例如:

    lst = []  # type: Sequence[int]
    
  • 使用 cast(T, obj) 进行转换,例如:

    zork = cast(Any, frobozz())
    
  • 其他内容,例如重载和存根模块,请参见 PEP 484

7. typing 模块中的预定义泛型类型和协议

(也可查看 typing.py 模块

  • 来自 collections.abc 中的所有类型(Set 重命名为 AbstractSet

  • DictListSetFrozenSet 和其他一些

  • re.Pattern[AnyStr]re.Match[AnyStr]

  • io.IO[AnyStr]io.TextIO ~ io.IO[str]io.BinaryIO ~ io.IO[bytes]

  • collections.abc 中的所有类型(但 Set 重命名为 AbstractSet)。

  • DictListSetFrozenSet 等。

  • re.Pattern[AnyStr]re.Match[AnyStr]

  • io.IO[AnyStr]io.TextIO ~ io.IO[str]io.BinaryIO ~ io.IO[bytes]

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值