Python-技术手册第三版-二-

127 阅读1小时+

Python 技术手册第三版(二)

原文:annas-archive.org/md5/9e375b08cb0be52e8b7c2a9eba6f5313

译者:飞龙

协议:CC BY-NC-SA 4.0

第四章:面向对象的 Python

Python 是一种面向对象(OO)编程语言。然而,与一些其他面向对象语言不同,Python 不强制您专门使用面向对象范式:它还支持过程式编程,具有模块和函数,因此您可以为程序的每个部分选择最佳范式。面向对象范式帮助您将状态(数据)和行为(代码)组合在方便的功能包中。此外,它提供了一些有用的专门机制,如继承特殊方法。更简单的过程式方法,基于模块和函数,当您不需要面向对象编程的优点时可能更合适。使用 Python,您可以混合和匹配范式。

除了核心面向对象概念外,本章还涵盖了抽象基类装饰器元类

类和实例

如果你熟悉其他面向对象编程语言(如 C++或 Java)中的面向对象编程,你可能对类和实例有很好的理解:是一种用户定义的类型,你可以实例化它以构建实例,即该类型的对象。Python 通过其类和实例对象支持此功能。

Python 类

一个是具有以下特征的 Python 对象:

  • 你可以像调用函数一样调用类对象。这种调用称为实例化,返回一个称为类的实例的对象;类也称为实例的类型

  • 一个类具有任意命名的属性,您可以绑定和引用。

  • 类属性的值可以是描述符(包括函数),在“描述符”中有介绍,也可以是普通数据对象。

  • 绑定到函数的类属性也称为类的方法

  • 一个方法可以有许多 Python 定义的名称之一,其名称前后有两个下划线(称为双下划线名称,简称“双下划线名称”——例如,名称 init,读作“dunder init”)。当类提供这些特殊方法时,Python 隐式调用它们,当发生类或其实例的各种操作时。

  • 一个类可以继承自一个或多个类,这意味着它将一些属性的查找委托给其他类对象(包括常规和双下划线方法),这些属性不在类本身中。

类的一个实例是一个 Python 对象,具有任意命名的属性,您可以绑定和引用。对于实例本身没有找到的任何属性,每个实例对象都将属性查找委托给其类。类反过来可能会将查找委托给它继承的类(如果有的话)。

在 Python 中,类是对象(值),与其他对象一样处理。你可以将一个类作为参数传递给函数调用,并且函数可以将一个类作为调用的结果返回。你可以将一个类绑定到一个变量,一个容器中的项,或者一个对象的属性。类也可以作为字典的键。由于在 Python 中类是完全普通的对象,我们经常说类是一等对象。

类语句

class 语句是创建类对象的最常见方式。class 是一个单子句复合语句,具有以下语法:

`class` *`Classname`*(*`base``-``classes`*, *, ***`kw`*):
    statement(s)

Classname 是一个标识符:当类语句完成时,它绑定(或重新绑定)到刚刚创建的类对象。Python 命名约定 建议对类名使用大写字母开头,例如 Item、PrivilegedUser、MultiUseFacility 等。

base-classes 是一个以逗号分隔的表达式序列,其值是类对象。各种编程语言对这些类对象使用不同的名称:你可以称它们为类的 bases, superclasses,或者 parents。你可以说创建的类从其基类 继承派生扩展,或者 子类化;在本书中,我们通常使用 extend。这个类是其基类的 直接子类 或者 后代。**kw 可以包括一个命名参数 metaclass= 来建立类的 元类,如 “Python 如何确定类的元类” 中所述。

从语法上讲,包括 base-classes 是可选的:要指示你正在创建一个没有基类的类,只需省略 base-classes(并且可选地也省略括号,将冒号直接放在类名后面)。每个类都继承自 object,无论你是否指定了显式基类。

类之间的子类关系是传递的:如果 C1 扩展了 C2,而 C2 扩展了 C3,那么 C1 扩展了 C3。内置函数 issubclass(*C1*, *C2*) 接受两个类对象:当 C1 扩展了 C2 时返回 True,否则返回 False。任何类都是其自身的子类;因此,对于任何类 Cissubclass(*C*, *C*) 返回 True。我们在 “继承” 中讨论了基类如何影响类的功能。

跟随class语句的缩进语句的非空序列是class body。类主体作为class语句的一部分立即执行。直到主体执行完毕,新的类对象才存在,并且Classname标识符尚未绑定(或重新绑定)。“元类如何创建类”提供了关于class语句执行时发生的详细信息。请注意,class语句不会立即创建任何新类的实例,而是定义了稍后通过调用该类创建实例时所有实例共享的属性集合。

类主体

类的主体通常是您指定类属性的地方;这些属性可以是描述符对象(包括函数)或任何类型的普通数据对象。类的一个属性可以是另一个类,所以,例如,您可以在另一个class语句内“嵌套”一个class语句。

类对象的属性

您通常通过在类主体内将值绑定到标识符来指定类对象的属性。例如:

`class` C1:
    x = 23
print(C1.x)                      *`# prints:`* *`23`*

在这里,类对象 C1 有一个名为 x 的属性,绑定到值 23,并且 C1.x 引用该属性。这样的属性也可以通过实例访问:c = C1(); print(c.x)。然而,在实践中,这并不总是可靠的。例如,当类实例 c 有一个 x 属性时,c.x 访问的是该属性,而不是类级别的属性。因此,要从实例中访问类级别的属性,例如,使用 print(c.class.x)可能是最好的选择。

您还可以在类主体外绑定或解绑类属性。例如:

`class` C2:
    `pass`
C2.x = 23
print(C2.x)                      *`# prints:`* *`23`*

如果您只在类主体内部的语句中绑定类属性,您的程序通常会更易读。但是,如果您希望在类级别而不是实例级别传递状态信息,则可能需要在其他地方重新绑定它们;如果您愿意,Python 允许您这样做。通过将属性赋值给属性,在类主体内部绑定的类属性与在类主体外部绑定或重新绑定的属性之间没有区别。

正如我们将很快讨论的那样,所有类实例共享类的所有属性。

class语句隐含地设置了一些类属性。属性 name 是在class语句中使用的Classname标识符字符串。属性 bases 是作为class语句中基类给定(或隐含)的类对象元组。例如,使用我们刚刚创建的 C1 类:

print(C1.__name__, C1.__bases__) *`# prints:`* *`C1 (<class 'object'>,)`*

类还有一个名为 dict 的属性,它是类使用的只读映射,用于保存其他属性(也可以非正式地称为类的命名空间)。

直接在类主体中的语句中,对类属性的引用必须使用简单名称,而不是完全限定名称。例如:

`class` C3:
    x = 23
    y = x + 22                   *`# must use just x`*, *`not`* *`C3.x`*

但是,在类主体中定义的方法中的语句中,对类属性的引用必须使用完全限定名称,而不是简单名称。例如:

`class` C4:
    x = 23
    `def` amethod(self):
        print(C4.x)              *`# must use C4.x or self.x`*, *`not`* *`just x!`*

属性引用(即,表达式如C.x)的语义比属性绑定更丰富。我们在“属性参考基础”中详细介绍这样的引用。

类主体中的函数定义

大多数类主体包括一些def语句,因为函数(在此上下文中称为方法)对于大多数类实例都是重要的属性。类主体中的def语句遵循“函数”中涵盖的规则。此外,类主体中定义的方法有一个强制的第一个参数,通常总是命名为 self,它引用调用方法的实例。self 参数在方法调用中起着特殊的作用,如“绑定和非绑定方法”中所述。

下面是一个包含方法定义的类的示例:

`class` C5:
    `def` hello(self):
        print('Hello')

一个类可以定义各种与其实例上的特定操作相关的特殊双下划线方法。我们在“特殊方法”中详细讨论这些方法。

类私有变量

当类主体中的语句(或主体中的方法)使用以两个下划线开头(但不以两个下划线结尾)的标识符时,例如__ident,Python 隐式地将标识符更改为_Classname__ident,其中Classname是类的名称。这种隐式更改允许类使用“私有”名称来命名属性、方法、全局变量和其他用途,从而减少意外重复使用其他地方使用的名称(特别是在子类中)的风险。

根据约定,以单个下划线开头的标识符是私有的,无论绑定它们的作用域是或不是一个类。Python 编译器不强制执行此隐私约定:由程序员来尊重它。

类文档字符串

如果类主体中的第一个语句是一个字符串文字,则编译器将该字符串绑定为类的文档字符串(或docstring);如果类主体中的第一个语句不是字符串文字,则其值为None。有关文档字符串的更多信息,请参见“文档字符串”。

描述符

描述符是一个对象,其类提供一个或多个名为 getsetdelete 的特殊方法。作为类属性的描述符控制访问该类实例上的属性的语义。粗略地说,当您访问一个实例属性时,Python 通过调用相应的描述符上的 get 来获取属性的值,如果有的话。例如:

`class` Const:  *`# class with an overriding descriptor, see later`*
    `def` __init__(self, value):
        self.__dict__['value'] = value
    `def` __set__(self, *_):  
        # silently ignore any attempt at setting
        # (a better design choice might be to raise AttributeError)
        `pass`
    `def` __get__(self, *_):
        *`# always return the constant value`*
        `return` self.__dict__['value']
    `def` __delete__(self, *_): 
        *`# silently ignore any attempt at deleting`* 
        # (a better design choice might be to raise AttributeError)
        `pass`

`class` X:
    c = Const(23)

x = X()
print(x.c)  *`# prints:`* *`23`*
x.c = 42    *`# silently ignored (unless you raise AttributeError)`*
print(x.c)  *`# prints:`* *`23`*
`del` x.c *`# silently ignored again (ditto)`*
print(x.c)  *`# prints:`* *`23`*

欲了解更多详细信息,请参阅“属性参考基础”。

覆盖和非覆盖描述符

当描述符的类提供了名为 set 的特殊方法时,该描述符称为 覆盖描述符(或者,使用较旧且令人困惑的术语,数据描述符);当描述符的类提供了 get 而没有提供 set 时,该描述符称为 非覆盖描述符

例如,函数对象的类提供了 get,但没有提供 set;因此,函数对象是非覆盖描述符。粗略地说,当您使用具有对应覆盖描述符的值分配实例属性时,Python 通过调用描述符的 set 方法设置属性值。有关详细信息,请参见“实例对象的属性”。

描述符协议的第三个双下划线方法是 delete,当使用 del 语句删除描述符实例时调用。如果不支持 del,实现 delete 并引发适当的 AttributeError 异常是一个好主意;否则,调用者将得到一个神秘的 AttributeError: delete 异常。

在线文档包含许多描述符及其相关方法的示例。

实例

要创建类的实例,请将类对象视为函数进行调用。每次调用返回一个类型为该类的新实例:

an_instance = C5()

内置函数 isinstance(i, C),其参数 C 是一个类,当 i 是类 C 或其任何子类的实例时返回 True。否则返回 False。如果 C 是类型元组(3.10+ 或使用 | 运算符连接的多个类型),isinstance 在 i 是任何给定类型的实例或子类实例时返回 True,否则返回 False

init

当一个类定义或继承了名为 init 的方法时,在调用类对象时会执行 init 方法来对新实例进行每个实例的初始化。调用时传递的参数必须对应于 init 的参数,除了参数 self。例如,考虑以下类定义:

`class` C6:
    `def` __init__(self, n):
        self.x = n

下面是创建 C6 类实例的方法:

another_instance = C6(42)

如 C6 类定义所示,init 方法通常包含绑定实例属性的语句。init 方法不能返回除 None 以外的值;如果返回其他值,Python 将引发 TypeError 异常。

init 的主要目的是绑定并创建新创建实例的属性。您也可以在 init 方法之外绑定、重新绑定或解绑实例属性。然而,当您最初在 init 方法中绑定所有类实例属性时,您的代码更易读。

init 方法不存在(且未从任何基类继承)时,必须以无参数调用类,并且新实例没有实例特定的属性。

实例对象的属性

创建实例后,可以使用点(.)运算符访问其属性(数据和方法)。例如:

an_instance.hello()                      *`# prints:`* *`Hello`*
print(another_instance.x)                *`# prints:`* *`42`*

Python 中这样的属性引用具有相当丰富的语义;我们会在“属性引用基础”中详细介绍它们。

你可以通过将值绑定到属性引用来为实例对象添加属性。例如:

`class` C7:
    `pass`
z = C7()
z.x = 23
print(z.x)                               *`# prints:`* *`23`*

实例对象z现在有一个名为 x 的属性,绑定到值 23,z.x 引用该属性。如果存在 setattr 特殊方法,则会拦截每次绑定属性的尝试。(我们在表 4-1 中介绍了 setattr。)

当尝试绑定到一个实例属性时,如果该属性名称对应于类中的重写描述符,描述符的 set 方法会拦截该尝试:如果 C7.x 是一个重写描述符,z.x=23 会执行 type(z).x.set(z, 23)。

创建实例会设置两个实例属性。对于任何实例zz.classz所属的类对象,z.dictz用来保存其其他属性的映射。例如,对于我们刚创建的实例z

print(z.__class__.__name__, z.__dict__)  *`# prints:`* *`C7 {'x':23}`*

你可以重新绑定(但不能解绑)这两个属性中的任何一个或两个,但这很少是必要的。

对于任何实例z、任何对象x和任何标识符S(除了 classdict),z.S=x 等同于 z.dict['S']=x(除非 setattr 特殊方法或重写描述符的 set 特殊方法拦截绑定尝试)。例如,再次引用我们刚创建的z

z.y = 45
z.__dict__['z'] = 67
print(z.x, z.y, z.z)                     *`# prints:`* *`23 45 67`*

在 Python 中,通过给属性赋值或显式绑定z.dict 中的条目创建的实例属性之间没有区别。

工厂函数习语

经常需要根据某些条件创建不同类的实例,或者如果可重用的实例已存在则避免创建新实例。一个常见的误解是通过让 init 方法返回特定对象来满足这些需求。然而,这种方法行不通:如果 init 返回除了None之外的任何值,Python 会引发异常。实现灵活对象创建的最佳方式是使用函数而不是直接调用类对象。以这种方式使用的函数称为工厂函数

调用工厂函数是一种灵活的方法:一个函数可以返回一个现有可重用的实例或通过调用适当的类创建一个新实例。假设你有两个几乎可以互换的类,SpecialCase 和 NormalCase,并且想要根据参数灵活地生成其中任何一个类的实例。下面这个适当的工厂函数示例允许你做到这一点(我们将在“绑定和未绑定方法”中更多地讨论 self 参数):

`class` SpecialCase:
    `def` amethod(self):
        print('special')
`class` NormalCase:
    `def` amethod(self):
        print('normal')
`def` appropriate_case(isnormal=`True`):
    `if` isnormal:
        `return` NormalCase()
    `else``:`
 `return` SpecialCase()
aninstance = appropriate_case(isnormal=`False`)
aninstance.amethod()                  *`# prints:`* *`special`*

new

每个类都有(或继承)一个名为 new 的类方法(我们在“类方法”中讨论)。当您调用C(args, **kwds)来创建类C的新实例时,Python 首先调用C.new(C, args, **kwds),并使用 new 的返回值x作为新创建的实例。然后 Python 调用C.init(x, args, **kwds),但仅当x确实是C或其任何子类的实例时(否则,x的状态将保持 new 留下的状态)。因此,例如,语句x*=C(23)等同于:

*`x`* = *`C`*.__new__(*`C`*, 23)
`if` isinstance(*`x`*, *`C`*):
    type(*`x`*).__init__(*`x`*, 23)

object.new 创建一个新的未初始化实例,该实例作为其第一个参数接收的类的实例。当该类具有 init 方法时,它会忽略其他参数,但当它接收到第一个参数之外的其他参数,并且第一个参数的类没有 init 方法时,它会引发异常。当您在类体内部重写 new 时,您无需添加 new=classmethod(new),也不需要使用@classmethod 装饰器,因为 Python 在此上下文中识别名称 new 并将其视为特殊名称。在那些偶发情况下,您稍后在类C的体外重新绑定C.new 时,您确实需要使用C.new=classmethod(whatever)。

new 具有工厂函数的大部分灵活性,如前一节所述。new 可以选择返回现有实例或根据需要创建新实例。当 new 确实创建新实例时,它通常将创建委托给 object.newC的另一个超类的 new 方法。

以下示例显示如何重写类方法 new 以实现 Singleton 设计模式的版本:

`class` Singleton:
    _singletons = {}
    `def` __new__(cls, *args, **kwds):
        `if` cls `not` `in` cls._singletons:
            cls._singletons[cls] = obj = super().__new__(cls)
            obj._initialized = False
        `return` cls._singletons[cls]

(我们在“协作超类方法调用”中介绍了内置的 super。)

任何 Singleton 的子类(不进一步重写 new 的子类)都有且仅有一个实例。当子类定义 init 时,它必须确保 init 可以安全地重复调用(在每次子类调用时)子类的唯一实例。³ 在此示例中,我们插入 _initialized 属性,设置为False,当 new 实际上创建新实例时。子类的 init 方法可以测试 self._initialized 是否为False,如果是,则将其设置为True并继续执行 init 方法的其余部分。当后续创建单例实例再次调用 init 时,self._initialized 将为True,表示实例已初始化,并且 init 通常可以直接返回,避免某些重复工作。

属性引用基础知识

属性引用是形式为x.name的表达式,其中x是任何表达式,name是称为属性名称的标识符。许多 Python 对象都有属性,但是当x引用类或实例时,属性引用具有特殊而丰富的语义。方法也是属性,因此我们对一般属性的所有说法也适用于可调用属性(即方法)。

假设x是类C的实例,该类继承自基类B。这些类和实例都有几个属性(数据和方法),如下所示:

`class` B:
    a = 23
    b = 45
    `def` f(self):
        print('method f in class B')
    `def` g(self):
        print('method g in class B')
`class` C(B):
    b = 67
    c = 89
    d = 123
    `def` g(self):
        print('method g in class C')
    `def` h(self):
        print('method h in class C')
x = C()
x.d = 77
x.e = 88

几个属性 dunder 名称是特殊的。C.name 是字符串'C',类的名称。C.bases 是元组(B,),C的基类的元组。x.classx所属的类C。当您使用这些特殊名称之一引用属性时,属性引用直接查找类或实例对象中的专用槽,并获取找到的值。您不能解绑这些属性。您可以即时重新绑定它们,更改类或实例的名称或基类,但这种高级技术很少必要。

C和实例x各自还有一个特殊属性:名为 dict 的映射(对x通常是可变的,但对C不是)。类或实例的所有其他属性,⁴除了少数特殊属性外,都保存为类或实例的 dict 属性中的项。

从类获取属性

当您使用语法C.name引用类对象C的属性时,查找进行两个步骤:

  1. 当'name'是C.dict 中的键时,C.nameC.dict['name']中获取值v。然后,当v是描述符时(即,type(v)提供名为 get 的方法),C.name的值是调用 type(v).get(v, None, C)的结果。当v不是描述符时,C.name的值是v

  2. 当'name'不是C.dict 中的键时,C.name将查找委托给C的基类,这意味着它在C的祖先类上循环,并在每个类上尝试name查找(按照"继承"中详述的方法解析顺序)。

从实例获取属性

当您使用语法x.name引用类C的实例x的属性时,查找进行三个步骤:

  1. 当'name'出现在C(或C的祖先类之一)中作为覆盖描述符v的名称时(即,type(v)提供方法 getset),x.name的值是 type(v).get(v, x, C)的结果。

  2. 否则,当'name'是x.dict 中的键时,x.name获取并返回x.dict['name']中的值。

  3. 否则,x.name将查找委托给x的类(按照与C.name相同的两步查找过程):

    • 当这找到描述符v时,属性查找的整体结果再次是 type(v).get(v, x, C)。

    • 当此查找到一个非描述符值v时,属性查找的整体结果就是v

当这些查找步骤未找到属性时,Python 会引发 AttributeError 异常。然而,对于x.name的查找,当C定义或继承特殊方法 getattr 时,Python 会调用C.getattr(*x, 'name')而不是引发异常。然后由 getattr 决定返回适当的值或引发适当的异常,通常是 AttributeError。

考虑前面定义的以下属性引用:

print(x.e, x.d, x.c, x.b, x.a)             *`# prints:`* *`88 77 89 67 23`*

当在步骤 2 的实例查找过程中,x.e 和 x.d 成功时,因为没有涉及描述符,并且'e'和'd'都是 x.dict 中的键。因此,查找不会继续,而是返回 88 和 77。另外三个引用必须继续到步骤 3 的实例查找过程,并查找 x.class(即 C)。x.c 和 x.b 在类查找过程的步骤 1 中成功,因为'c'和'b'都是 C.dict 中的键。因此,查找不会继续,而是返回 89 和 67。x.a 一直到类查找过程的步骤 2,查找 C.bases[0](即 B)。'a'是 B.dict 中的键;因此,x.a 最终成功并返回 23。

设置一个属性

注意,属性查找步骤只有在引用属性时才会像刚才描述的那样发生,而在绑定属性时不会。当绑定到一个名称不是特殊的类或实例属性时(除非 setattr 方法或覆盖描述符的 set 方法拦截实例属性的绑定),你只影响该属性的 dict 条目(在类或实例中分别)。换句话说,对于属性绑定,除了检查覆盖描述符外,不涉及查找过程。

绑定方法和非绑定方法

函数对象的 get 方法可以返回函数对象本身,或者包装该函数的绑定方法对象;绑定方法与从特定实例获取它时关联。

在前一节的代码中,属性 f、g 和 h 是函数;因此,对它们中的任何一个进行属性引用都会返回一个包装相应函数的方法对象。考虑以下内容:

print(x.h, x.g, x.f, C.h, C.g, C.f)

此语句输出三个绑定方法,用如下字符串表示:

<bound method C.h of <__main__.C object at 0x8156d5c>>

然后是三个函数对象,用字符串表示如下:

<function C.h at 0x102cabae8>

绑定方法与函数对象的比较

当属性引用在实例x上时,我们得到绑定方法,而当属性引用在类C上时,我们得到函数对象。

因为绑定方法已经与特定实例关联,所以可以按以下方式调用方法:

x.h()                   *`# prints:`* *`method h in class C`*

这里需要注意的关键点是,你不会通过通常的参数传递语法传递方法的第一个参数 self。相反,实例 x 的绑定方法会将 self 参数隐式绑定到对象 x。因此,方法体可以访问实例的属性,就像它们是 self 的属性一样,即使我们没有显式地向方法传递参数。

让我们仔细看看绑定方法。当实例上的属性引用在查找过程中找到一个在实例类中作为属性的函数对象时,查找会调用函数的 get 方法来获取属性的值。在这种情况下,调用会创建并返回一个绑定方法,它包装了该函数。

注意,当属性引用的查找直接在 x.dict 中找到一个函数对象时,属性引用操作不会创建绑定方法。在这种情况下,Python 不会将函数视为描述符,也不会调用函数的 get 方法;相反,函数对象本身就是属性的值。同样地,对于不是普通函数的可调用对象,如内置函数(而不是 Python 编写的函数),Python 不会创建绑定方法。

除了包装的函数对象的属性外,绑定方法还有三个只读属性:im_class 是提供方法的类对象,im_func 是被包装的函数,im_self 指的是来自你获取方法的实例 x

你可以像使用其 im_func 函数一样使用绑定方法,但是对绑定方法的调用不会显式提供一个对应于第一个参数(通常命名为 self)的参数。当你调用绑定方法时,在给定调用点的其他参数(如果有)之前,绑定方法会将 im_self 作为第一个参数传递给 im_func。

让我们详细地跟随一下使用常规语法 x.name(arg) 进行方法调用所涉及的概念步骤。

`def` f(a, b): ...              *`# a function f with two arguments`*

`class` C:
    name = f
x = C()

x 是类 C 的实例对象,name 是 x 的方法名称(C 的属性,其值是一个函数,在本例中是函数 f 的属性),arg是任何表达式。Python 首先检查'name'是否是 C 中覆盖描述符的属性名称,但它不是——函数是描述符,因为它们的类型定义了方法 get,但不是覆盖的描述符,因为它们的类型没有定义方法 set。Python 接下来检查'name'是否是 x.dict_ 中的一个键,但它不是。所以,Python 在 C 中找到了 name(如果 name 通过继承在 C 的一个 bases 中找到,则一切都将同样工作)。Python 注意到属性的值,函数对象 f,是一个描述符。因此,Python 调用 f.get(x, C),返回一个绑定方法对象,其 im_func 设置为 f,im_class 设置为 C,im_self 设置为 x。然后 Python 调用这个绑定方法对象,arg作为唯一的参数。绑定方法将 im_self(即 x)插入为调用绑定方法的 im_func(即函数 f)的第一个参数,arg成为第二个参数。整体效果就像调用:

x.__class__.__dict__'name'

当绑定方法的函数体执行时,它与其 self 对象或任何类之间没有特殊的命名空间关系。引用的变量是局部或全局的,就像任何其他函数一样,详见“命名空间”。变量不会隐式地指示 self 中的属性,也不会指示任何类对象中的属性。当方法需要引用、绑定或解绑其 self 对象的属性时,它通过标准的属性引用语法来完成(例如,self.name)。⁵ 缺乏隐式作用域可能需要一些时间来适应(因为在这一点上,Python 与许多面向对象的语言不同),但它确保了清晰性、简单性并消除了潜在的歧义。

绑定方法对象是一类一等公民对象:你可以在任何可调用对象的地方使用它们。由于绑定方法同时持有对其包装的函数和执行它的 self 对象的引用,它是闭包的一个强大而灵活的替代方案(详见“嵌套函数和嵌套作用域”)。如果一个实例对象的类提供了特殊方法 call(详见表 4-1),那么这是另一种可行的替代方案。这些构造允许你将一些行为(代码)和一些状态(数据)打包到一个可调用对象中。闭包最简单,但在适用性上有些限制。以下是来自嵌套函数和嵌套作用域部分的闭包示例:

`def` make_adder_as_closure(augend):
    `def` add(addend, _augend=augend):
        `return` addend + _augend
    `return` add

绑定方法和可调用实例比闭包更丰富和灵活。以下是如何使用绑定方法实现相同功能的方式:

`def` make_adder_as_bound_method(augend):
    `class` Adder:
        `def` __init__(self, augend):
            self.augend = augend
        `def` add(self, addend):
            `return` addend+self.augend
    `return` Adder(augend).add

这是如何使用可调用实例(一个其类提供特殊方法 call 的实例)来实现它的方式:

`def` make_adder_as_callable_instance(augend):
    `class` Adder:
        `def` __init__(self, augend):
            self.augend = augend
        `def` __call__(self, addend):
            `return` addend+self.augend
    `return` Adder(augend)

从调用函数的代码视角来看,所有这些工厂函数都是可互换的,因为它们都返回多态的可调用对象。在实现方面,闭包是最简单的;面向对象的方法,即绑定方法和可调用实例,使用更灵活、通用和强大的机制,但在这个简单的例子中并不需要这种额外的功能(因为除了加数之外,不需要其他状态,闭包和面向对象方法都可以轻松处理)。

继承

当您在类对象C上使用属性引用C.name,并且'name'不是C.dict 中的键时,查找将隐式地在C.bases 中的每个类对象上进行,按特定顺序(由于历史原因称为方法解析顺序或 MRO,但实际上适用于所有属性,而不仅仅是方法)。C的基类可能会有它们自己的基类。查找将逐个在 MRO 中的直接和间接祖先中进行,停止在找到'name'时。

方法解析顺序

在类中查找属性名的查找基本上是通过按左到右、深度优先顺序访问祖先类来进行的。然而,在多重继承的情况下(使得继承图成为一般有向无环图(DAG),而不仅仅是特定的树),这种简单方法可能导致某些祖先类被访问两次。在这种情况下,解析顺序在查找序列中只保留任何给定类的最右出现。

每个类和内置类型都有一个特殊的只读类属性称为 mro,它是用于方法解析的类型元组,按顺序排列。只能在类上引用 mro,而不能在实例上引用,并且由于 mro 是只读属性,因此无法重新绑定或解绑。有关 Python MRO 的所有方面的详细且高度技术性的解释,请参阅 Michele Simionato 的文章“Python 2.3 方法解析顺序”⁶和 Guido van Rossum 关于“Python 历史”的文章。特别要注意,Python 可能无法确定某个类的任何明确的 MRO:在这种情况下,当 Python 执行该语句时会引发 TypeError 异常。

覆盖属性

正如我们刚刚看到的,对属性的搜索沿着 MRO(通常是沿着继承树向上)进行,并且一旦找到属性就会停止。子类始终在其祖先之前进行检查,因此当子类定义与超类中同名的属性时,搜索将找到子类中的定义并在此处停止。这被称为子类覆盖超类中的定义。考虑以下代码:

`class` B:
    a = 23
    b = 45
    `def` f(self):
        print('method f in class B')
    `def` g(self):
        print('method g in class B')
`class` C(B):
    b = 67
    c = 89
    d = 123
    `def` g(self):
        print('method g in class C')
    `def` h(self):
        print('method h in class C')

在这里,类 C 覆盖了其超类 B 的属性 b 和 g。请注意,与某些其他语言不同,Python 中你可以像轻松覆盖可调用属性(方法)一样覆盖数据属性。

委托给超类方法

当子类 C 覆盖其超类 B 的方法 f 时,Cf 方法体通常希望将其操作的某部分委托给超类方法的实现。这有时可以使用函数对象来完成,如下所示:

`class` Base:
    `def` greet(self, name):
        print('Welcome', name)
`class` Sub(Base):
    `def` greet(self, name):
        print('Well Met and', end=' ')
        Base.greet(self, name)
x = Sub()
x.greet('Alex')

在 Sub 类的 greet 方法体中,委托到超类的方法使用了通过属性引用 Base.greet 获得的函数对象,因此通常会传递所有参数,包括 self。(如果显式使用基类看起来有点丑陋,请耐心等待;在本节中很快你会看到更好的方法)。委托到超类实现是这种函数对象的常见用法。

委托(Delegation)的一种常见用法出现在特殊方法 init 中。当 Python 创建一个实例时,不像一些其他面向对象的语言那样自动调用任何基类的 init 方法。这由子类来初始化其超类,必要时使用委托。例如:

`class` Base:
    `def` __init__(self):
        self.anattribute = 23
`class` Derived(Base):
    `def` __init__(self):
        Base.__init__(self)
        self.anotherattribute = 45

如果 Derived 类的 init 方法没有显式调用 Base 类的 init 方法,那么 Derived 的实例将缺少其初始化的部分。因此,这些实例将违反 里氏替换原则(LSP),因为它们将缺少属性 anattribute。如果子类不定义 init,则不会出现此问题,因为在这种情况下,它会从超类继承 init。因此,绝对不有理由编写以下代码:

`class` Derived(Base):
    `def` __init__(self):
        Base.__init__(self)

绝对不要编写仅委托给超类的方法。

永远不要定义一个语义上空的 init(即仅委托给超类的方法)。相反,应该从超类继承 init。这条建议适用于所有方法,特殊的或不是,但出于某种原因,编码这种语义上空的方法似乎最常见于 init

上述代码说明了将委托概念应用于对象的超类,但在今天的 Python 中,通过名称显式编码这些超类实际上是一种不良实践。如果基类重命名,所有对它的调用点都必须更新。或者更糟的是,如果重构类层次结构在 Derived 和 Base 类之间引入新层,则新插入类的方法将被静默跳过。

推荐的方法是使用内置的 super 类型调用定义在超类中的方法。要调用继承链中的方法,只需调用 super(),不带参数:

`class` Derived(Base):
    `def` __init__(self):
        super().__init__()
        self.anotherattribute = 45

合作超类方法调用

在多重继承和所谓的“菱形图”情况下,使用超类名称显式调用超类版本的方法也会带来很多问题。考虑以下代码:

`class` A:
    `def` met(self):
        print('A.met')
`class` B(A):
    `def` met(self):
        print('B.met')
        A.met(self)
`class` C(A):
    `def` met(self):
        print('C.met')
        A.met(self)
`class` D(B,C):
    `def` met(self):
        print('D.met')
        B.met(self)
        C.met(self)

当我们调用 D().met() 时,A.met 实际上被调用了两次。如何确保每个祖先方法的实现仅被调用一次?解决方案是使用 super:

`class` A:
    `def` met(self):
        print('A.met')
`class` B(A):
    `def` met(self):
        print('B.met')
        super().met()
`class` C(A):
    `def` met(self):
        print('C.met')
        super().met()
`class` D(B,C):
    `def` met(self):
        print('D.met')
        super().met()

现在,D().met() 将确保每个类的 met 方法仅被调用一次。如果你养成了使用 super 来编码超类调用的好习惯,你的类将在复杂的继承结构中表现得很顺畅——即使继承结构实际上很简单也不会有任何负面影响。

唯一的情况可能更喜欢通过显式语法调用超类方法的粗糙方法是,当不同类具有相同方法的不同和不兼容签名时。在许多方面,这种情况令人不快;如果你确实必须处理它,显式语法有时可能是最不受欢迎的方法。正确使用多重继承受到严重阻碍;但是,即使在面向对象编程的最基本属性中,如基类和子类实例之间的多态性中,在超类和其子类中为相同名称的方法指定不同签名时,也会受到影响。

使用内置函数 type 进行动态类定义

除了使用 type(obj) 的方式外,你还可以使用三个参数调用 type 来定义一个新的类:

NewClass = type(name, bases, class_attributes, **kwargs)

其中 name 是新类的名称(应与目标变量匹配),bases 是直接超类的元组,class_attributes 是要在新类中定义的类级方法和属性的字典,**kwargs 是要传递给其中一个基类的元类的可选命名参数。

例如,使用简单的 Vehicle 类层次结构(如 LandVehicle、WaterVehicle、AirVehicle、SpaceVehicle 等),你可以在运行时动态创建混合类,如:

AmphibiousVehicle = type('AmphibiousVehicle', 
                         (LandVehicle, WaterVehicle), {})

这相当于定义一个多重继承的类:

`class` AmphibiousVehicle(LandVehicle, WaterVehicle): `pass`

当你调用 type 在运行时创建类时,你无需手动定义所有 Vehicle 子类的组合扩展,并且添加新的子类也不需要大量扩展已定义的混合类。⁷ 欲了解更多注解和示例,请参阅在线文档

“删除”类属性

继承和重写提供了一种简单有效的方式来非侵入性地添加或修改(重写)类属性(如方法)——即在子类中添加或重写属性而无需修改定义属性的基类。然而,继承并未提供一种非侵入性地删除(隐藏)基类属性的方法。如果子类简单地未定义(重写)某个属性,则 Python 会找到基类的定义。如果需要执行此类删除操作,则可能的选择包括以下几种:

  • 重写方法并在方法体中引发异常。

  • 避免继承,将属性保存在子类的 dict 之外,并为选择性委派定义 getattr 方法。

  • 覆盖 getattribute 以类似的效果。

这些技术的最后一个在 “getattribute” 中演示。

考虑使用聚合而不是继承

继承的替代方法是使用 聚合:而不是从基类继承,而是将基类的实例作为私有属性。通过在包含类中提供公共方法(即调用属性上的等效方法)委托给包含的属性,您可以完全控制属性的生命周期和公共接口。这样,包含类对于属性的创建和删除有更多的控制权;此外,对于属性类提供的任何不需要的方法,您只需不在包含类中编写委派方法即可。

内置的 object 类型

内置的 object 类型是所有内置类型和类的祖先。object 类型定义了一些特殊方法(在 “特殊方法” 中记录),实现了对象的默认语义:

new, init

您可以通过调用 object() 而不传递任何参数来创建对象的直接实例。该调用使用 object.new 和 object.init 来创建并返回一个没有属性(甚至没有用于保存属性的 dict)的实例对象。这样的实例对象可能作为“哨兵”非常有用,确保与任何其他不同对象比较时不相等。

delattr, getattr, getattribute, setattr

默认情况下,任何对象都使用对象的这些方法处理属性引用(如 “属性引用基础知识” 中所述)。

hash, repr, str

将对象传递给 hash、repr 或 str 调用对象的相应 dunder 方法。

对象的子类(即任何类)可以——而且通常会!——覆盖这些方法中的任何一个,和/或添加其他方法。

类级方法

Python 提供了两种内置的非覆盖描述符类型,这使得类具有两种不同类型的“类级方法”:静态方法类方法

静态方法

静态方法 是可以在类上调用,或者在类的任何实例上调用的方法,而不受普通方法关于第一个参数的特殊行为和约束的影响。静态方法可以具有任何签名;它可以没有参数,并且如果有的话,第一个参数也不起任何特殊作用。您可以将静态方法视为一种普通函数,您可以正常调用它,尽管它恰好绑定到类属性上。

虽然定义静态方法从未 必需(您可以选择定义一个普通函数,而不是在类外部定义),但某些程序员认为它们是一种优雅的语法替代品,当函数的目的与某个特定类紧密绑定时。

要创建一个静态方法,调用内置的 type staticmethod,并将其结果绑定到一个类属性。与所有绑定类属性的方式一样,通常应在类的主体中完成,但您也可以选择在其他地方执行。staticmethod 的唯一参数是 Python 调用静态方法时要调用的函数。以下示例展示了定义和调用静态方法的一种方式:

`class` AClass:
    `def` astatic():
        print('a static method')
    astatic = staticmethod(astatic)

an_instance = AClass()
print(AClass.astatic())             *`# prints:`* *`a static method`*
print(an_instance.astatic())        *`# prints:`* *`a static method`*

此示例将同一名称用于传递给 staticmethod 的函数和绑定到 staticmethod 结果的属性。这种命名惯例并非强制性,但是是个好主意,我们建议您始终使用它。Python 提供了一种特殊的简化语法来支持这种风格,详见“装饰器”。

类方法

类方法是您可以在类上或在类的任何实例上调用的方法。Python 将方法的第一个参数绑定到调用该方法的类或调用该方法的实例的类;它不将其绑定到实例,如普通绑定方法。类方法的第一个参数通常被命名为 cls。

与静态方法一样,虽然定义类方法从不是必需的(您始终可以选择在类外定义一个普通函数,并将类对象作为其第一个参数),但类方法是这种函数的一种优雅替代方式(特别是在需要在子类中重写它们时)。

要创建一个类方法,调用内置的 type classmethod,并将其结果绑定到一个类属性。与所有绑定类属性的方式一样,通常应在类的主体中完成,但您也可以选择在其他地方执行。classmethod 的唯一参数是 Python 调用类方法时要调用的函数。以下是定义和调用类方法的一种方式:

`class` ABase:
    `def` aclassmet(cls):
        print('a class method for', cls.__name__)
    aclassmet = classmethod(aclassmet)
`class` `ADeriv`(ABase):
    `pass`

b_instance = ABase()
d_instance = ADeriv()
print(ABase.aclassmet())        *`# prints:`* *`a class method for ABase`*
print(b_instance.aclassmet())   *`# prints:`* *`a class method for ABase`*
print(ADeriv.aclassmet())       *`# prints:`* *`a class method for ADeriv`*
print(d_instance.aclassmet())   *`# prints:`* *`a class method for ADeriv`*

此示例将同一名称用于传递给 classmethod 的函数和绑定到 classmethod 结果的属性。同样,这种命名约定并非强制性,但是是个好主意,我们建议您始终使用它。Python 提供了一种特殊的简化语法来支持这种风格,详见“装饰器”。

属性

Python 提供了一种内置的重写描述符类型,可用于给类的实例提供属性。属性是具有特殊功能的实例属性。您可以使用普通语法(例如,print(x.prop),x.prop=23,del x.prop)引用、绑定或解绑属性。但是,与通常的属性引用、绑定和解绑语义不同,这些访问会在实例x上调用您作为内置 type property 的参数指定的方法。以下是定义只读属性的一种方式:

`class` Rectangle:
    `def` __init__(self, width, height):
        self.width = width
        self.height = height
    `def` area(self):
        `return` self.width * self.height
    area = property(area, doc='area of the rectangle')

类 Rectangle 的每个实例r都有一个合成的只读属性r.area,方法r.area()通过动态计算乘以边的方法来生成。Rectangle.area.doc 的文档字符串是'rectangle 的面积'。r.area 属性是只读的(尝试重新绑定或解绑它会失败),因为我们在 property 调用中仅指定了一个 get 方法,而没有 set 或 del 方法。

属性执行与特殊方法 getattrsetattrdelattr(在“通用特殊方法”中介绍)类似的任务,但属性更快更简单。要构建一个属性,请调用内置类型 property 并将其结果绑定到一个类属性。与类属性的所有绑定一样,通常在类的主体中完成,但您可以选择在其他地方完成。在类C的主体内部,您可以使用以下语法:

*`attrib`* = property(fget=`None`, fset=`None`, fdel=`None`, doc=`None`)

x是类C的一个实例,并且您引用x.attrib时,Python 会在x上调用作为 fget 参数传递给属性构造函数的方法,不带参数。当您赋值x.attrib = value时,Python 会调用作为 fset 参数传递的方法,并将value作为唯一的参数传递给它。当您执行del x.attrib时,Python 会调用作为 fdel 参数传递的方法,不带参数。Python 使用作为 doc 参数传递的参数作为属性的文档字符串。属性的所有参数都是可选的。当缺少某个参数时,当某些代码尝试进行该操作时,Python 会引发异常。例如,在矩形示例中,我们使属性 area 为只读,因为我们仅为参数 fget 传递了一个参数,而没有为参数 fset 和 fdel 传递参数。

在类中创建属性的一种优雅语法是使用 property 作为装饰器(参见“装饰器”):

`class` Rectangle:
    `def` __init__(self, width, height):
        self.width = width
        self.height = height
    @property
    `def` area(self):
        *`"""area of the rectangle"""`*
        `return` self.width * self.height

要使用这种语法,您必须将 getter 方法命名为您希望属性具有的相同名称;该方法的文档字符串将成为属性的文档字符串。如果您还想添加设置器和/或删除器,请使用名为(在此示例中)area.setter 和 area.deleter 的装饰器,并将如此装饰的方法命名为属性的相同名称。例如:

`import` math
`class` Rectangle:
    `def` __init__(self, width, height):
        self.width = width
        self.height = height
    @property
    `def` area(self):
        *`"""area of the rectangle"""`*
        `return` self.width * self.height
    @area.setter
    `def` area(self, value):
        scale = math.sqrt(value/self.area)
        self.width *= scale
        self.height *= scale

为什么属性很重要

属性的关键重要性在于它们的存在使得将公共数据属性作为类公共接口的一部分完全安全(事实上也是建议性的)。如果在将来的类版本或者需要与之多态的其他类中,需要在引用、重新绑定或解绑属性时执行一些代码,您可以将普通属性更改为属性,并获得所需效果,而不会对使用您的类的任何代码(即“客户端代码”)产生任何影响。这让您可以避免面向对象语言中缺乏属性而需要使用的笨拙惯用法,如访问器修改器方法。例如,客户端代码可以使用如下自然的惯用法:

some_instance.widget_count += 1

而不是被迫进入这样的复杂嵌套访问器和修改器:

some_instance.set_widget_count(some_instance.get_widget_count() + 1)

如果你有时候想要编写方法的自然名称像 get_this 或 set_that,最好将这些方法包装成属性,以增加代码的清晰度。

属性和继承

属性的继承与任何其他属性一样工作。然而,对于不留心的人来说,有一个小陷阱:用于访问属性的方法是在定义属性的类中定义的方法,而不是使用后续在子类中发生的进一步覆盖。考虑这个例子:

`class` B:
    `def` f(self):
        `return` 23
    g = property(f)
`class` C(B):
    `def` f(self):
        `return` 42

c = C()
print(c.g)                *`# prints:`* *`23`*, *`not`* *`42`*

访问属性 c.g 会调用 B.f,而不是你可能期望的 C.f。原因非常简单:属性构造函数(直接或通过装饰器语法)接收的是 函数对象 f(这发生在执行 B 的 class 语句时,因此问题中的函数对象也称为 B.f)。因此,稍后在子类 C 中重新定义名称 f 是无关紧要的,因为属性在创建时并不会查找该名称,而是使用它在创建时收到的函数对象。如果需要解决这个问题,可以通过手动添加额外的查找间接性来实现:

`class` B:
    `def` f(self):
        `return` 23
    `def` _f_getter(self):
        `return` self.f()
    g = property(_f_getter)
`class` C(B):
    `def` f(self):
        `return` 42

c = C()
print(c.g)                *`# prints:`* *`42`**`,`* *`as expected`*

在这里,属性所持有的函数对象是 B._f_getter,它反过来确实会查找名称 f(因为它调用 self.f());因此,对 f 的覆盖具有预期的效果。正如 David Wheeler 所说,“计算机科学中的所有问题都可以通过另一级间接性来解决。”⁸

slots

通常,任何类 C 的实例对象 x 都有一个字典 x.dict,Python 使用它让你在 x 上绑定任意属性。为了节省一点内存(以只允许 x 有预定义的一组属性名称为代价),可以在类 C 中定义一个类属性名为 slots,一个序列(通常是元组)的字符串(通常是标识符)。当类 Cslots 时,类 C 的实例 x 就没有 dict:试图在 x 上绑定一个不在 C.slots 中的属性名将会引发异常。

使用 slots 可以减少小实例对象的内存消耗,这些对象可以没有强大和便利的能力拥有任意命名的属性。只有在类可能同时有成百上千个实例的情况下,才值得为类添加 slots,以便每个实例节省几十个字节的内存。然而,与大多数其他类属性不同,只有在类体中的赋值将其绑定为类属性时,slots 才能像我们刚才描述的那样工作。以后对 slots 的任何更改、重绑定或取消绑定,以及从基类继承 slots 都没有效果。以下是如何在前面定义的 Rectangle 类中添加 slots 以获得更小(虽然不太灵活)的实例:

`class` OptimizedRectangle(Rectangle):
    __slots__ = 'width', 'height'

不需要为 area 属性定义槽:slots 不限制属性,只限制普通实例属性,如果没有定义 slots,则这些属性将存储在实例的 dict 中。

3.8+ slots 属性也可以使用以属性名称为键和以文档字符串为值的字典来定义。OptimizedRectangle 可以更详细地声明为:

`class` OptimizedRectangle(Rectangle):
    __slots__ = {'width': 'rectangle width in pixels',
                 'height': 'rectangle height in pixels'}

getattribute

所有对实例属性的引用都通过特殊方法 getattribute。这个方法来自 object,在那里它实现了属性引用的语义(如“属性引用基础”中所述)。你可以覆盖 getattribute 来隐藏子类实例的继承类属性等目的。例如,以下示例展示了一种实现无需 append 方法的列表的方法:

`class` listNoAppend(list):
    `def` __getattribute__(self, name):
        `if` name == 'append':
            `raise` AttributeError(name)
        `return` list.__getattribute__(self, name)

类 listNoAppend 的实例 x 几乎与内置的列表对象无法区分,唯一的区别是其运行时性能显著较差,并且任何对 x.append 的引用都会引发异常。

实现 getattribute 可能会比较棘手;使用内置函数 getattr 和 setattr 以及实例的 dict(如果有的话)或重新实现 getattrsetattr 通常更容易。当然,在某些情况下(如前面的例子),没有其他选择。

每个实例方法

实例可以具有所有属性的实例特定绑定,包括可调用属性(方法)。对于方法,与任何其他属性(除了绑定到覆盖描述符的属性之外),实例特定绑定会隐藏类级绑定:属性查找在找到直接在实例中绑定时不考虑类。对于可调用属性的实例特定绑定不执行“绑定和非绑定方法”中详细描述的任何转换:属性引用返回完全相同的可调用对象,该对象之前直接绑定到实例属性。

然而,这对于 Python 隐式调用的各种操作的每个实例绑定的特殊方法可能不像你期望的那样工作,如“特殊方法”中所述。这些特殊方法的隐式使用总是依赖于特殊方法的类级绑定(如果有的话)。例如:

`def` fake_get_item(idx):
    `return` idx
`class` MyClass:
    `pass`
n = MyClass()
n.__getitem__ = fake_get_item
print(n[23])                      *`# results in:`*
*`# Traceback (most recent call last):`*
*`#   File "<stdin>", line 1, in ?`*
*`# TypeError: unindexable object`*

从内置类型继承

类可以从内置类型继承。但是,类只能直接或间接扩展多个内置类型,前提是这些类型专门设计为允许这种互操作兼容性。Python 不支持从多个任意内置类型无约束地继承。通常,新式类最多只扩展一个实质性的内置类型。例如,这样:

`class` noway(dict, list):
    `pass`

引发 TypeError 异常,并详细说明“多个基类具有实例布局冲突。” 当你看到这样的错误消息时,意味着你试图直接或间接地从多个不特别设计以在如此深层次上合作的内置类型继承。

特殊方法

一个类可以定义或继承特殊方法,通常被称为“dunder”方法,因为如前所述,它们的名称前后都有双下划线。每个特殊方法与特定操作相关联。Python 在你对实例对象执行相关操作时会隐式调用特殊方法。在大多数情况下,方法的返回值是操作的结果,当操作所关联的方法不存在时,尝试该操作会引发异常。

在本节中,我们指出一般规则不适用的情况。在以下讨论中,x 是执行操作的类 C 的实例,y 是另一个操作数(如果有的话)。每个方法的参数 self 也指代实例对象 x。每当我们提到对 x.whatever(...) 的调用时,请记住,严格来说,正在发生的确切调用实际上是 x.class.whatever(x, ...)。

通用特殊方法

一些 dunder 方法与通用操作相关。定义或继承这些方法的类允许其实例控制这些操作。这些操作可以分为几类:

初始化和结束处理

类可以通过特殊方法 newinit 控制其实例的初始化(这是一个非常常见的需求),并且/或者通过 del 控制其终结处理(这是一个罕见的需求)。

字符串表示

类可以通过特殊方法 repr, str, formatbytes 控制 Python 如何将其实例呈现为字符串。

比较、哈希和在布尔上下文中的使用

一个类可以控制其实例如何与其他对象比较(通过特殊方法 lt, le, gt, ge, eqne),字典如何将其用作键以及集合如何将其用作成员(通过 hash),以及它们在布尔上下文中是否评估为真值或假值(通过 bool)。

属性引用、绑定和解绑

一个类可以通过特殊方法 getattribute, getattr, setattrdelattr 控制对其实例属性(引用、绑定、解绑)的访问。

可调用实例

类可以通过特殊方法 call 使其实例可调用,就像函数对象一样。

表 4-1 记录了通用的特殊方法。

表 4-1. 通用特殊方法

boolbool(self) 当对x进行真假判断(参见“布尔值”)时,例如在调用 bool(x)时,Python 会调用x.bool(),该方法应返回TrueFalse。当不存在 bool 方法时,Python 会调用 len 方法,并且当x.len()返回 0 时将x视为假(为了检查容器是否非空,避免编写if len(container)>0:;而是使用if container:)。当既不存在 bool 方法也不存在 len 方法时,Python 将x视为真。
bytesbytes(self) 调用 bytes(x)时会调用x.bytes(),如果存在的话。如果一个类同时提供了 bytesstr 特殊方法,它们分别应返回“等效”的 bytes 类型和 str 类型字符串。
callcall(self[, args...]) 当调用x([args...])时,Python 会将此操作转换为对x.call([args...])的调用。调用操作的参数对应于 call 方法的参数,去除第一个参数。第一个参数,通常称为 self,引用x:Python 会隐式提供它,就像对绑定方法的任何其他调用一样。

| del | del(self) 当x通过垃圾收集消失之前,Python 会调用x.del()让x完成最后操作。如果没有 del 方法,Python 在回收x时不会进行特殊处理(这是最常见的情况:很少有类需要定义 del)。Python 忽略 del 的返回值,并且不会隐式调用C类超类的 del 方法。C.del 必须显式执行任何需要的最终操作,包括必要时通过委托来完成。当类C有需要终结的基类时,C.del 必须调用 super().del()。

del 方法与“del 语句”中涵盖的del语句没有特定联系。

一般情况下,当您需要及时和确保的最终操作时,del 并不是最佳选择。对于这种需求,应使用“try/finally”中涵盖的try/finally语句(或者更好的是“The with Statement”中涵盖的with语句)。定义有 del 方法的类的实例不参与循环垃圾收集,详见“垃圾收集”。注意避免涉及这些实例的引用循环:只有在没有可行的替代方案时才定义 del

delattrdelattr(self, name) 每次请求解绑属性x.y(通常是del x.y),Python 会调用x.delattr('y')。所有后续讨论的 setattr 都适用于 delattr。Python 忽略 delattr 的返回值。如果不存在 delattr,Python 会将del x.y转换为del x.dict['y']。
dirdir(self) 当调用 dir(x)时,Python 将操作转换为调用x.dir(),它必须返回x的属性的排序列表。当x的类没有 dir 时,dir(x)执行内省以返回x的属性的排序列表,努力产生相关而非完整的信息。

| eq, ge, gt, le,

lt, ne | eq(self, other), ge(self, other), gt(self, other), le(self, other),

lt(self, other), ne(self, other)

比较x == y, x >= y, x > y, x <= y, x < y, 和x != y,分别调用列出的特殊方法,应返回FalseTrue。每个方法可以返回 NotImplemented 告知 Python 以替代方式处理比较(例如,Python 可能尝试y > x来代替x < y)。

最佳实践是仅定义一个不等比较方法(通常是 lt)加上 eq,并用 functools.total_ordering 修饰类(在表 8-7 中有介绍),以避免模板和比较中的逻辑矛盾风险。 |

formatformat(self, format_string='') 调用 format(x)会调用x.format(''),调用 format(x, format_string)会调用x.format(format_string)。类负责解释格式字符串(每个类可以定义自己的小型格式规范语言,受内置类型实现的启发,如在“字符串格式化”中介绍)。当从 object 继承 format 时,它委托给 str 并且不接受非空格式字符串。
getattrgetattr(self, name) 当找不到x.y的常规步骤时(即通常会引发 AttributeError 时),Python 会调用x.getattr('y')。Python 不会对通过常规方式找到的属性调用 getattr(如作为x.dict 中的键或通过x.class 访问)。如果希望 Python 对每个属性都调用 getattr,可以将属性存放在其他位置(例如通过私有名称引用的另一个字典中),或者改写 getattribute。如果 getattr 找不到y,应该引发 AttributeError。

| getattribute | _getattribute(self, name) 每次访问属性x.y时,Python 调用x.getattribute('y'),它必须获取并返回属性值,否则引发 AttributeError。属性访问的通常语义(x.dictC.slotsC的类属性、x.getattr)都归因于 object.getattribute

当类C重写 getattribute 时,必须实现它想要提供的所有属性语义。实现属性访问的典型方式是委托(例如,在重写 getattribute 的操作中调用 object.getattribute(self, ...))。

重写 getattribute 会减慢属性访问速度

当一个类覆盖了 getattribute,则该类实例上的所有属性访问变得缓慢,因为覆盖的代码会在每次属性访问时执行。

|

| hash | hash(self) 调用 hash(x) 会调用 x.hash()(以及其他需要知道 x 哈希值的上下文,如将 x 作为字典键使用,如 D[x] 其中 D 是一个字典,或将 x 作为集合成员使用)。hash 必须返回一个 int,以便当 x==y 时意味着 hash(x)==hash(y),并且对于给定的对象必须始终返回相同的值。

当缺少 hash 时,调用 hash(x) 会调用 id(x),只要同时缺少 eq。其他需要知道 x 哈希值的上下文行为相同。

任何 x,使得 hash(x) 返回一个结果而不是引发异常,被称为可哈希对象。当缺少 hash,但存在 eq 时,调用 hash(x) 会引发异常(以及其他需要知道 x 的哈希值的上下文)。在这种情况下,x 不可哈希,因此不能作为字典键或集合成员。

通常只为不可变对象定义 hash,而且还定义了 eq。请注意,如果存在任何 y 使得 x==y,即使 y 是不同类型的对象,并且 xy 都是可哈希的,必须确保 hash(x)==hash(y)。(在 Python 内置类型中,存在一些情况,其中对象的不同类型之间可以相等。最重要的是不同数字类型之间的相等性:int 可以等于 bool、float、fractions.Fraction 实例或 decimal.Decimal 实例。)

| init | init(self[, args...]) 当调用 C([args...]) 创建类 C 的实例 x 时,Python 调用 x.init([args...]) 让 x 初始化自己。如果缺少 init(即从 object 继承),必须无参数调用 C,即 C(),并且在创建时 x 没有实例特定的属性。Python 不会对 C 类及其超类的 init 方法进行隐式调用。C.init 必须显式执行任何初始化操作,包括必要时委托。例如,当类 C 有一个需要无参数初始化的基类 B 时,C.init 中的代码必须显式调用 super().init()。init 的继承与任何其他方法或属性相同:如果 C 本身没有覆盖 init,它会从其 mro 中的第一个超类继承它,就像其他属性一样。

__init__ 必须返回 None;否则,调用该类会引发 TypeError。

| __new__ | __new__(cls[, *args*...]) 当你调用 *C*([*args*...]) 时,Python 通过调用 *C*.__new__(*C*[, *args*...]) 来获取你正在创建的新实例 x。每个类都有类方法 __new__(通常只是从 object 继承而来),它可以返回任何值 x。换句话说,__new__ 不需要返回 C 的新实例,尽管期望如此。如果 __new__ 返回的值 xC 的实例或 C 的任何子类的实例(无论是新的还是之前存在的实例),Python 就会在 x 上调用 __init__(使用最初传递给 __new__ 的相同 [args...])。

__new__ 中初始化不可变对象,其他对象在 __init__ 中初始化。

你可以在 __init____new__ 中执行大多数类型的新实例初始化,所以你可能想知道最好将它们放在哪里。最佳实践是只将初始化放在 __init__ 中,除非你有一个特定的理由将其放在 __new__ 中。(当类型是不可变的时,__init__ 不能改变其实例:在这种情况下,__new__ 必须执行所有的初始化。)

| |

__repr____repr__(self) 调用 repr(*x*)(当 x 是表达式语句的结果时,在交互解释器中会隐式发生)会调用 *x*.__repr__() 来获取并返回 x 的完整字符串表示。如果没有 __repr__,Python 就会使用默认的字符串表示。 __repr__ 应返回一个包含关于 x 的无歧义信息的字符串。在可行的情况下,尝试使 eval(repr(*x*))==*x*(但不要为了达到这个目标而过度努力!)。
__setattr____setattr__(self, *name*, *value*) 对于绑定属性 *x.y* 的任何请求(通常是赋值语句 *x.y=value*,但也可以是 setattr(*x*, '*y*', *value*)),Python 调用 *x*.__setattr__('*y*', *value*)。Python 总是对 x任何 属性绑定调用 __setattr__ —— 这与 __getattr__ 的主要区别(在这一点上,__setattr__ 更接近 __getattribute__)。为了避免递归,当 *x*.__setattr__ 绑定 x 的属性时,它必须直接修改 *x*.__dict__(例如,通过 *x*.__dict__[*name*]=*value*);或者更好的是,__setattr__ 可以委托给超类(调用 super().__setattr__('*y*', *value*))。Python 忽略 __setattr__ 的返回值。如果 __setattr__ 不存在(即从 object 继承),并且 C.y 不是覆盖描述符,Python 通常会将 *x.y=z* 翻译为 *x*.__dict__['*y*']=*z*(但 __setattr__ 也可以与 __slots__ 一起很好地工作)。
__str____str__(self) 类似于 print(*x*)str(*x*) 调用 *x*.__str__() 来获取 x 的非正式、简洁的字符串表示。如果没有 __str__,Python 就会调用 *x*.__repr____str__ 应返回一个方便阅读的字符串,即使这可能需要一些近似。

容器的特殊方法

实例可以是一个 container(序列、映射或集合——相互排斥的概念⁹)。为了最大限度地提高实用性,容器应提供特殊方法 getitemcontainsiter(如果可变,则还应提供 setitemdelitem),以及后续章节讨论的常规方法。在许多情况下,您可以通过扩展 collections.abc 模块中的适当抽象基类(例如 Sequence、MutableSequence 等)来获得合适的常规方法实现,如 “Abstract Base Classes” 中所述。

序列

在每个项目访问的特殊方法中,一个包含 L 个项目的序列应接受任何整数 key,使得 -L<=key<L。¹⁰ 为了与内置序列兼容,负索引 key,0>key>=-L,应等同于 key+L。当 key 具有无效类型时,索引应引发 TypeError 异常。当 key 是有效类型的值但超出范围时,索引应引发 IndexError 异常。对于不定义 iter 的序列类,for 语句依赖于这些要求,以及接受可迭代参数的内置函数也依赖于这些要求。每个序列的项目访问特殊方法也应(如果有可能)接受作为其索引参数的内置类型切片的实例,其 start、step 和 stop 属性为 int 或 None;slicing 语法依赖于此要求,如 “Container slicing” 中所述。

序列还应允许通过 + 进行连接(与同类型的另一个序列),并通过 *(乘以整数)进行重复。因此,序列应具有特殊方法 addmulraddrmul,如 “Special Methods for Numeric Objects” 中所述;此外,可变序列应具有等效的就地方法 iaddimul。序列应与同类型的另一个序列有意义地进行比较,实现 字典序比较,就像列表和元组一样。(继承自 Sequence 或 MutableSequence 抽象基类不能满足所有这些要求;最多只能从 MutableSequence 继承,只提供 iadd。)

每个序列都应包括 “List methods” 中介绍的常规方法:不区分大小写的 count 和 index 方法,如果可变,则还应包括 append、insert、extend、pop、remove、reverse 和 sort 方法,其签名和语义与列表的相应方法相同。(继承自 Sequence 或 MutableSequence 抽象基类足以满足这些要求,除了 sort 方法。)

如果一个不可变序列的所有项都是可散列的,那么它本身也应该是可散列的。序列类型可能以某些方式限制其项(例如,仅接受字符串项),但这不是强制性的。

映射

映射的元素访问特殊方法在接收到无效的 key 参数值(有效类型)时应引发 KeyError 异常,而不是 IndexError。任何映射都应定义在“字典方法”中介绍的非特殊方法:copy、get、items、keys 和 values。可变映射还应定义方法 clear、pop、popitem、setdefault 和 update。(从 Mapping 或 MutableMapping 抽象基类继承可以满足这些要求,但不包括 copy。)

如果其所有项都是可哈希的,则不可变映射类似类型应该是可哈希的。映射类似类型可以在某些方面对其键进行约束,例如仅接受可哈希键,或者(更具体地)例如仅接受字符串键,但这不是强制性的。任何映射应该与相同类型的另一个映射有意义地可比较(至少在等式和不等式方面,尽管不一定是有序比较)。

集合

集合是一种特殊的容器:它们既不是序列也不是映射,不能被索引,但是有长度(元素个数)并且可迭代。集合还支持许多运算符(&、|、^ 和 -),以及成员测试和比较,还有等效的非特殊方法(交集、并集等)。如果你实现了类似集合的容器,它应该对 Python 内置的集合具有多态性,详见“集合”。(从 Set 或 MutableSet 抽象基类继承可以满足这些要求。)

如果其所有元素都是可哈希的,则不可变集合类似类型应该是可哈希的。集合类似类型可以在某些方面对其元素进行约束,例如仅接受可哈希元素,或(更具体地)例如仅接受整数元素,但这不是强制性的。

容器切片

当你引用、绑定或取消绑定容器 x 上的切片,例如 x[i:j] 或 x[i:j:k](在实践中,这仅用于序列时),Python 调用 x 的适用的元素访问特殊方法,将一个名为 slice object 的内置类型的对象作为 key。切片对象具有属性 start、stop 和 step。如果在切片语法中省略了相应的值,则每个属性都是 None。例如,del x[:3] 调用 x.delitem(y),其中 y 是一个切片对象,使得 y.stop 为 3,y.start 为 Noney.step 为 None。容器对象 x 应适当地解释传递给 x 的特殊方法的切片对象参数。切片对象的方法 indices 可以帮助:以你的容器长度作为其唯一参数调用它,它返回一个包含三个非负索引的元组,适合作为循环索引切片中每个项目的开始、停止和步长。例如,在序列类的 getitem 特殊方法中,完全支持切片的常见习惯用法是:

`def` __getitem__(self, index):
    *`# Recursively special-case slicing`*
    `if` isinstance(index, slice):
        `return` self.__class__(self[x]
                              `for` x `in` range(*index.indices(len(self))))
    *`# Check index, and deal with a negative and/or out-of-bounds index`*
    index = operator.index(index)
    `if` index < 0:
        index += len(self)
    `if` `not` (0 <= index < len(self)):
        `raise` `IndexError`
    *`# Index is now a correct int, within range(len(self))`*
 *`# ...rest of __getitem__, dealing with single-item access...`*

这种习惯用法使用生成器表达式(genexp)语法,并假定你的类的 init 方法可以使用可迭代参数调用,以创建适当的新实例。

容器方法

特殊方法 getitemsetitemdelitemiterlencontains 公开容器功能(参见表 4-2)。

表格 4-2. 容器方法

| contains | 布尔测试y in x 调用x.contains(y)。当x是一个序列或类似集合时,contains 应该在y等于x中的一个项的值时返回True。当x是一个映射时,contains 应该在y等于x中的一个键的值时返回True。否则,contains 应该返回False。当 contains 不存在且x是可迭代的时候,Python 执行y in x如下,时间与 len(x)成正比:

`for` *`z`* `in` *`x`*:
    `if` *`y`*==*`z`*:
        `return` `True`
`return` `False`

|

delitem当一个请求要解除x的一个项或片段的绑定(通常是 del x[key]),Python 调用x.delitem(key)。如果x是可变的且可以删除项(及可能的片段),则容器x应该有 delitem
getitem当你访问x[key](即当你索引或切片容器x时),Python 调用x.getitem(key)。所有(非类似集合的)容器都应该有 getitem
iter当一个请求要循环遍历x的所有项(通常是 for item in x),Python 调用x.iter()来获取x上的迭代器。内置函数 iter(x)也调用x.iter()。当 iter 不存在时,iter(x)会合成并返回一个迭代器对象,该对象包装x并产生x[0]、x[1]等,直到这些索引中的一个引发 IndexError 异常以指示容器的末尾。但是,最好确保你编写的所有容器类都有 iter
len调用 len(x)会调用x.len()(其他需要知道容器x中有多少项的内置函数也会这样)。len 应该返回一个整数,即x中的项数。当 bool 不存在时,Python 还会调用x.len_()来评估x在布尔上下文中的值;在这种情况下,当且仅当容器为空时(即容器的长度为 0 时),容器是虚假的。所有容器都应该有 len,除非容器确定包含的项数太昂贵。
setitem当一个请求要绑定x的一个项或片段(通常是一个赋值x[key]=value),Python 调用x.setitem(key, value)。如果x是可变的,则容器x应该有 setitem,因此可以添加或重新绑定项,也许还有片段。

抽象基类

抽象基类(ABCs)是面向对象设计中的重要模式:它们是不能直接实例化的类,而是存在于被具体类扩展的目的(通常的类,可以被实例化的那种)。

一个推荐的面向对象设计方法(归功于 Arthur J. Riel)是永远不要扩展一个具体类。¹¹ 如果两个具体类有足够的共同点,使你想让其中一个继承另一个,那么可以通过创建一个 抽象 基类来替代,该抽象基类涵盖它们所有的共同点,并让每个具体类扩展该 ABC。这种方法避免了继承中许多微妙的陷阱和问题。

Python 对 ABCs 提供了丰富的支持,足以使它们成为 Python 对象模型的一部分。¹²

abc 模块

标准库模块 abc 提供了元类 ABCMeta 和类 ABC(继承 abc.ABC 使得 abc.ABCMeta 成为元类,且没有其他效果)。

当你将 abc.ABCMeta 作为任何类 C 的元类时,这使得 C 成为一个 ABC,并提供了类方法 C.register,可用一个参数调用:该参数可以是任何现有类(或内置类型) X

调用 C.register(X) 使 X 成为 C 的一个 虚拟 子类,这意味着 issubclass(X, C) 返回 True,但 C 不出现在 X.mro 中,X 也不继承 C 的任何方法或其他属性。

当然,也可以像通常的子类化方式一样,让一个新类 Y 继承自 C,在这种情况下 C 会出现在 Y.mro 中,并且 Y 继承 C 的所有方法,就像通常的子类化一样。

一个 ABC C 还可以选择重写类方法 subclasshook,当 issubclass(X, C) 调用时会传入单一参数 XX 是任何类或类型)。当 C.subclasshook(X) 返回 True 时,issubclass(X, C) 也返回 True;当 C.subclasshook(X) 返回 False 时,issubclass(X, C) 也返回 False。当 C.subclasshook(X) 返回 NotImplemented 时,issubclass(X, C) 会按照通常的方式进行。

abc 模块还提供了 decorator abstractmethod 来指定必须在继承类中实现的方法。你可以通过先使用 property 然后是 abstractmethod decorators 的顺序来将属性定义为抽象。¹³ 抽象方法和属性可以有实现(通过 super 内建函数对子类可见),但将方法和属性设为抽象的目的是,只有当 X 覆盖了 ABC C 的每个抽象属性和方法时,才能实例化 ABC C 的非虚拟子类 X

collections 模块中的 ABCs

collections 提供了许多 ABCs,在 collections.abc.¹⁴ 中列出了一些这样的 ABCs,这些 ABCs 接受作为虚拟子类任何定义或继承特定抽象方法的类,如 表 4-3 中所列。

表 4-3. 单方法 ABCs

ABC抽象方法
Callablecall
Containercontains
Hashablehash
Iterableiter
Sizedlen

collections.abc 中的其他 ABCs 扩展了其中一个或多个,添加了更多基于抽象方法的抽象方法和/或 mixin 方法。(当你在具体类中扩展任何 ABC 时,你 必须 覆盖抽象方法;你也可以覆盖一些或所有的 mixin 方法,以帮助提高性能,但这不是必须的——当这样做能够获得足够的性能以满足你的目的时,你可以直接继承它们。)

表 4-4 详细说明了 collections.abc 中直接扩展了前述 ABCs 的 ABCs。

表 4-4. 具有附加方法的 ABCs

ABC扩展抽象方法Mixin 方法
IteratorIterablenextiter

| Mapping | Container Iterable

Sized | getitem iter

len | contains eq

ne

getitems

keys

values |

MappingViewSized len

| Sequence | Container Iterable

Sized | getitem len | contains iter

reversed

count

index |

| Set | Container Iterable

Sized | contains __iter

len | and^(a) eq

ge^(b)

gt

le

lt

ne

or

sub

xor

isdisjoint |

^(a) 对于集合和可变集合,许多 dunder 方法等效于具体类 set 中的非特殊方法;例如,add 就像交集,而 iadd 就像 intersection_update。^(b) 对于集合,排序方法反映了“子集”的概念:s1 <= s2 意味着“s1s2 的子集或等于 s2。”

表 4-5 详细说明了本模块中进一步扩展的 ABCs。

表 4-5. collections.abc 中剩余的 ABCs

ABC扩展抽象方法Mixin 方法
ItemsViewMappingView Set contains iter
KeysViewMappingView Set contains iter

| MutableMapping | Mapping | delitem getitem

iter

_len

setitem | Mapping’s methods, plus: clear

pop

popitem

setdefault

update |

| MutableSequence | Sequence | delitem getitem

len

setitem

insert | Sequence’s methods, plus: iadd

append

extend

pop

remove

reverse |

| MutableSet | Set | contains __iter

len

add

discard | Set’s methods, plus: iand

ior

isub

ixor

clear

pop

remove |

ValuesViewMappingView contains iter

查看 在线文档 获取更多详细信息和使用示例。

numbers 模块中的 ABCs

numbers 提供了一个层次结构(也称为 tower)的 ABCs,表示各种类型的数字。 表 4-6 列出了 numbers 模块中的 ABCs。

表 4-6. numbers 模块提供的 ABCs

ABC描述
Number层次结构的根。包括任意类型的数值;不需要支持任何给定的操作。
Complex扩展自 Number。必须支持(通过特殊方法)转换为 complex 和 bool,以及 +,-,*,/,==,!=,和 abs,以及直接的方法 conjugate 和属性 real 和 imag。
Real扩展自 Complex。此外,必须支持(通过特殊方法)转换为 float,math.trunc,round,math.floor,math.ceil,divmod,//,%,<,<=,>,和 >=。
Rational扩展自 Real。此外,必须支持 numerator 和 denominator 属性。
Integral扩展自 Rational。此外,必须支持(通过特殊方法)转换为 int,**,和位运算 <<,>>,&,^,,和 ~。
^(a) 因此,每个整数或浮点数都有一个 real 属性等于其值,以及一个 imag 属性等于 0.^(b) 因此,每个整数都有一个 numerator 属性等于其值,以及一个 denominator 属性等于 1.

查看 在线文档 获取关于实现自定义数值类型的说明。

数值对象的特殊方法

一个实例可以通过多个特殊方法支持数值操作。一些不是数字的类也支持 Table 4-7 中的一些特殊方法,以重载如 + 和 * 的运算符。特别是,序列应该有特殊方法 add, mul, radd, 和 rmul,如 “Sequences” 所述。当二进制方法之一(如 add, sub 等)被调用时,如果操作数的类型不支持该方法,则该方法应返回内置的 NotImplemented 单例。

表 4-7. 数值对象的特殊方法

| abs, invert,

neg,

pos | _abs(self), invert(self), neg(self), pos(self) 一元运算符 abs(x), ~x, -x, 和 +x,分别调用这些方法。

| add, mod,

mul,

sub | add (self, other), mod(self, other),

mul(self, other),

sub(self, other)

运算符 x + y, x % y, *x ** y, 和 x - y 分别调用这些方法,通常用于算术计算。

| and, lshift,

or,

rshift,

xor | and(self, other), lshift(self, other), or(self, other), _rshift(self, other),

xor(self, other)

运算符 x & y, x << y, x | y, x >> y, 和 x ^ y 分别调用这些方法,通常用于位运算。

| complex, float,

int | complex(self), float(self), int(self) 内置类型 complex(x), float(x), 和 int(x),分别调用这些方法。

divmoddivmod(self, other) 内置函数 divmod(x, y) 调用 x.divmod(y)。divmod 应返回一对 (quotient, remainder) 等于 (x // y, x % y)。

| floordiv, truediv | floordiv(self, other),truediv(self, other),

运算符 x // yx / y,通常用于算术除法。 |

| iadd, ifloordiv,

imod

imul

isub

itruediv

imatmul | iadd(self, other),ifloordiv(self, other),

imod(self, other),

imul(self, other),

isub(self, other),

itruediv(self, other),

imatmul(self, other),

增强赋值 x += yx //= yx %= yx *= yx -= yx /= y,和 x @= y,分别调用这些方法。每个方法应该就地修改 x 并返回 self。当 x 是可变的时候定义这些方法(即,当 x 可以 就地更改时)。 |

| iand, ilshift,

ior

irshift

ixor | _iand(self, other),_ilshift(self, other),

ior(self, other),

irshift(self, other),

ixor(self, other),

增强赋值 x &= yx <<= yx = yx >>= y,和 x ^= y,分别调用这些方法。每个方法应该就地修改 x 并返回 self。当 x 是可变的时候定义这些方法(即,当 x 可以 就地更改时)。 |

indexindex(self) 像 int 一样,但只应由整数的替代实现类型提供(换句话说,该类型的所有实例都可以精确映射到整数)。例如,所有内置类型中,只有 int 提供 index;float 和 str 不提供,尽管它们提供 int。序列的索引和切片内部使用 index 来获取所需的整数索引。
ipowipow(self,other) 增强赋值 x **= y 调用 x.ipow(y)。ipow 应该就地修改 x 并返回 self。
matmulmatmul(self, other) 运算符 x @ y 调用这个方法,通常用于矩阵乘法。
powpow(self,other[, modulo]) x ** y 和 pow(x, y) 都调用 x.pow(y),而 pow(x, y, z) 调用 x.pow(y, z)。 x.pow(y, z) 应该返回等于表达式 x.pow(y) % z 的值。

| radd, rmod,

rmul

rsub

rmatmul | radd(self, other),rmod(self, other),

rmul(self, other),

rsub(self, other),

rmatmul(self, other),

运算符 y + xy / xy % xy * xy - x,和 y @ x,分别在 y 没有所需方法 addtruediv 等,或者当该方法返回 NotImplemented 时,在 x 上调用这些方法。 |

| rand, rlshift,

ror

rrshift

rxor | rand(self, other),rlshift(self, other),

ror(self, other),

rrshift(self, other),

rxor(self, other),

运算符 y & xy << xy | xy >> x,以及 x ^ y 分别在 y 没有所需方法 andlshift,等等,或者当该方法返回 NotImplemented 时,在 x 上调用这些方法。|

rdivmod_rdivmod(self, other) 内置函数 divmod(y, x) 调用 x.rdivmod(y) 当 y 没有 divmod,或者当该方法返回 NotImplemented 时。rdivmod 应返回一个对 (remainder, quotient)。
rpowrpow(self,other) y ** x 和 pow(y, x) 调用 x.rpow(y) 当 y 没有 pow,或者当该方法返回 NotImplemented 时。在这种情况下,没有三个参数的形式。

装饰器

在 Python 中,经常使用 高阶函数:接受函数作为参数并返回函数作为结果的可调用对象。例如,描述符类型,如 staticmethod 和 classmethod,在类体内可以使用,如 “类级方法” 所述:

`def` f(cls, ...):
 *`# ...definition of f snipped...`*
f = classmethod(f)

然而,将 classmethod 的调用在 def 语句之后的文本上,对代码的可读性有所影响:当阅读 f 的定义时,代码的读者尚不知道 f 将成为类方法而不是实例方法。如果在 def 前面提到 classmethod,则代码更易读。为此,使用称为 装饰 的语法形式:

@classmethod
`def` f(cls, ...):
 *`# ...definition of f snipped...`*

装饰器,这里的 @classmethod,必须紧随其后的 def 语句,并意味着 f = classmethod(f) 在 def 语句之后立即执行(无论 fdef 定义的任何名称)。更一般地,@expression 评估表达式(必须是一个名称,可能是限定的,或者是一个调用),并将结果绑定到一个内部临时名称(比如,__aux);任何装饰器必须紧跟在 def(或 class)语句之后,并意味着 f = __aux(f) 在 defclass 语句之后立即执行(无论 fdefclass 定义的任何名称)。绑定到 __aux 的对象称为 装饰器,它被称为 装饰 函数或类 f

装饰器是一种便捷的高阶函数缩写。你可以将装饰器应用于任何 defclass 语句,不仅限于类体内。你可以编写自定义装饰器,它们只是接受函数或类对象作为参数,并返回函数或类对象作为结果的高阶函数。例如,这是一个简单的装饰器示例,它不修改其装饰的函数,而是在函数定义时将函数的文档字符串打印到标准输出:

`def` showdoc(f):
    `if` f.__doc__:
        print(f'{f.__name__}: {f.__doc__}')
    `else`:
        print(f'{f.__name__}: No docstring!')
    `return` f

@showdoc
`def` f1():
    """a docstring"""  *`# prints:`* *`f1: a docstring`*

@showdoc
`def` f2():
    `pass`               *`# prints:`* *`f2: No docstring!`*

标准库模块 functools 提供了一个方便的装饰器 wraps,用于增强常见的“包装”习惯建立的装饰器:

import functools

`def` announce(f):
    @functools.wraps(f)
    `def` wrap(*a, **k):
        print(f'Calling {f.__name__}')
        `return` f(*a, **k)
    `return` wrap

使用 @announce 装饰函数 f 导致在每次调用 f 之前打印一行公告。由于 functools.wraps(f) 装饰器,包装器采用被包装函数的名称和文档字符串:例如,在调用这样一个装饰过的函数时调用内置帮助是有用的。

元类

任何对象,甚至是类对象,都有一种类型。在 Python 中,类型和类也是一等对象。类对象的类型也称为类的 元类。¹⁵ 对象的行为主要由对象的类型确定。对于类也是如此:类的行为主要由类的元类确定。元类是一个高级主题,您可能想跳过本节的其余部分。但是,完全掌握元类可以带您深入了解 Python;偶尔定义自己的自定义元类可能会有用。

简单类定制的替代方法元类。

虽然自定义元类允许您以几乎任何想要的方式调整类的行为,但通常可以通过编写自定义元类来更简单地实现一些自定义。

当类 C 具有或继承类方法 init_subclass 时,Python 在每次对 C 进行子类化时调用该方法,将新构建的子类作为唯一的位置参数传递。init_subclass 也可以有命名参数,在这种情况下,Python 会传递在执行子类化的类语句中找到的相应命名参数。作为一个纯粹的说明性例子:

>>> `class` C:
...     `def` __init_subclass__(cls, foo=None, **kw):
...         print(cls, kw)
...         cls.say_foo = staticmethod(lambda: f'*{foo}*')
...         super().__init_subclass__(**kw)
... 
>>> `class` D(C, foo='bar'):
...     `pass`
...
<class '__main__.D'> {}
>>> D.say_foo()
'*bar*'

init_subclass 中的代码可以以适用的方式修改 cls,在类创建后工作方式上本质上像一个 Python 自动应用于 C 的任何子类的类装饰器。

另一个用于定制的特殊方法是 set_name,它允许您确保将描述符的实例添加为类属性时,它们知道您正在向其添加的类和名称。在将 ca 添加到名为 C 的类并命名为 n 的类语句的末尾,当 ca 的类型具有方法 set_name 时,Python 调用 ca.set_name(C, n)。例如:

>>> `class` Attrib:
...     `def` __set_name__(self, cls, name):
...         print(f'Attribute {name!r} added to {cls}')
... 
>>> `class` AClass:
...     some_name = Attrib()
...
Attribute 'some_name' added to <class '__main__.AClass'>
>>>

如何确定 Python 类的元类。

class 语句接受可选的命名参数(在基类之后,如果有)。最重要的命名参数是 metaclass,如果存在,则标识新类的元类。如果存在非类型元类,则还允许其他命名参数,此时这些参数传递给元类的可选 prepare 方法(完全由 prepare 方法决定如何使用此类命名参数)。¹⁶ 当命名参数 metaclass 不存在时,Python 通过继承来确定元类;对于没有明确指定基类的类,默认元类为 type。

Python 调用 prepare 方法(如果存在)来确定元类后立即调用元类,如下所示:

`class` M:
    `def` __prepare__(classname, *classbases, **kwargs):
        `return` {}
 *`# ...rest of M snipped...`*
`class` X(onebase, another, metaclass=M, foo='bar'):
 *`# ...body of X snipped...`*

在这里,调用等同于 M.prepare('X', onebase, another, foo='bar')。如果存在 prepare,则必须返回映射(通常只是字典),Python 将其用作执行类体的d映射。如果不存在 prepare,Python 将使用一个新的、最初为空的字典作为d

元类如何创建类

确定了元类M后,Python 使用三个参数调用M:类名(一个字符串)、基类元组t和字典(或其他由 prepare 生成的映射)d,其中类体刚刚执行完毕。¹⁷ 这个调用返回类对象C,Python 随后将其绑定到类名上,完成class语句的执行。注意,这实际上是类型M的实例化,因此对M的调用执行M.init(C, namestring, t, d),其中CM.new(M, namestring, t, d)的返回值,就像在任何其他实例化中一样。

Python 创建类对象C之后,类C与其类型(通常为M的类型)之间的关系与任何对象与其类型之间的关系相同。例如,当你调用类对象C(创建C的实例)时,M.call 执行,类对象C作为第一个参数。

注意,在这种情况下,描述的方法(“按实例方法”)的方法,仅在类上查找特殊方法,而不是在实例上。调用C实例化它必须执行元类的M.call,无论C是否具有每实例属性(方法)call(即,独立于C的实例是否可调用)。这种方式,Python 对象模型避免了必须将类及其元类的关系作为专门情况的问题。避免专门情况是 Python 强大的关键:Python 有少量、简单、通用的规则,并且一贯地应用这些规则。

自定义元类的定义和使用

定义自定义元类很容易:继承自 type 并重写其部分方法。你还可以使用 newinitgetattribute 等方法执行大多数你可能考虑创建元类的任务,而不涉及元类。然而,自定义元类可能会更快,因为特殊处理仅在类创建时执行,这是一种罕见的操作。自定义元类允许你在框架中定义一整类具有你编码的任何有趣行为的类,这与类本身可能选择定义的特殊方法完全独立。

要以明确的方式修改特定的类,一个很好的替代方法通常是使用类装饰器,如 “装饰器” 中所述。然而,装饰器不会被继承,因此必须显式地将装饰器应用于每个感兴趣的类。¹⁸ 另一方面,元类是可以继承的;事实上,当你定义一个自定义元类 M 时,通常也会定义一个否则为空的类 C,其元类为 M,这样需要 M 的其他类可以直接继承自 C

类对象的某些行为只能在元类中定制。下面的示例展示了如何使用元类来更改类对象的字符串格式:

`class` MyMeta(type):
    `def` __str__(cls):
        `return` f'Beautiful class {cls.__name__!r}'
`class` MyClass(metaclass=MyMeta):
    `pass`
x = MyClass()
print(type(x))      *`# prints:`* *`Beautiful class 'MyClass'`*

一个实质性的自定义元类示例

假设在 Python 编程中,我们想念 C 语言的结构体类型:一个按顺序排列、具有固定名称的数据属性对象(数据类,在下一节中详细讨论,完全满足此需求,这使得此示例纯粹是说明性的)。Python 允许我们轻松定义一个通用的 Bunch 类,它与固定顺序和名称除外是类似的:

`class` Bunch:
    `def` __init__(self, **fields):
        self.__dict__ = fields
p = Bunch(x=2.3, y=4.5)
print(p)       *`# prints:`* *`<_main__.Bunch object at 0x00AE8B10>`*

自定义元类可以利用属性名称在类创建时固定的事实。在 示例 4-1 中显示的代码定义了一个元类 MetaBunch 和一个类 Bunch,使我们能够编写如下代码:

`class` Point(Bunch):
    *`"""A Point has x and y coordinates, defaulting to 0.0,`*
       *`and a color, defaulting to 'gray'-and nothing more,`*
       *`except what Python and the metaclass conspire to add,`*
       *`such as __init__ and __repr__.`*
    *`"""`*
    x = 0.0
    y = 0.0
    color = 'gray'
*`# example uses of class Point`*
q = Point()
print(q)                    *`# prints:`* *`Point()`*
p = Point(x=1.2, y=3.4)
print(p)                    *`# prints:`* *`Point(x=1.2, y=3.4)`*

在这段代码中,print 调用会生成我们的 Point 实例的可读字符串表示。Point 实例非常节省内存,并且它们的性能基本上与前面示例中简单类 Bunch 的实例相同(由于对特殊方法的隐式调用没有额外开销)。示例 4-1 非常实质性,要理解其所有细节需要掌握本书后面讨论的 Python 方面,比如字符串(在 第九章 中讨论)和模块警告(在 “warnings 模块” 中讨论)。在 示例 4-1 中使用的标识符 mcl 表示“元类”,在这种特殊的高级情况下比 cls 表示“类”更清晰。

示例 4-1. MetaBunch 元类
`import` warnings
`class` MetaBunch(type):
    *`"""`*
    *`Metaclass for new and improved "Bunch": implicitly defines`*
    *`__slots__, __init__, and __repr__ from variables bound in`*
    *`class scope.`*
    *`A class statement for an instance of MetaBunch (i.e., for a`*
    *`class whose metaclass is MetaBunch) must define only`*
    *`class-scope data attributes (and possibly special methods, but`*
    *`NOT __init__ and __repr__). MetaBunch removes the data`*
    *`attributes from class scope, snuggles them instead as items in`*
    *`a class-scope dict named __dflts__, and puts in the class a`*
    *`__slots__ with those attributes' names, an __init__ that takes`*
    *`as optional named arguments each of them (using the values in`*
    *`__dflts__ as defaults for missing ones), and a __repr__ that`*
    *`shows the repr of each attribute that differs from its default`*
    *`value (the output of __repr__ can be passed to __eval__ to make`*
    *`an equal instance, as per usual convention in the matter, if`*
    *`each non-default-valued attribute respects that convention too).`*
    *`The order of data attributes remains the same as in the`* *`class body.`*
    *`"""`*
    `def` __new__(mcl, classname, bases, classdict):
        *`"""Everything needs to be done in __new__, since`*
           *`type.__new__ is where __slots__ are taken into account.`*
        *`"""`*
        *`# Define as local functions the __init__ and __repr__ that`*
        *`# we'll use in the new class`*
        `def` __init__(self, **kw):
            *`"""__init__ is simple: first, set attributes without`*
 *`explicit values to their defaults; then, set`* *`those`*
 *`explicitly`* *`passed in kw.`*
            *`"""`*
            `for` k `in` self.__dflts__:
                `if` `not` k `in` kw:
                    setattr(self, k, self.__dflts__[k])
            `for` k `in` kw:
                setattr(self, k, kw[k])
        `def` __repr__(self):
            *`"""__repr__ is minimal: shows only attributes that`*
               *`differ`* *`from default values, for compactness.`*
            *`"""`*
            rep = [f'{k}={getattr(self, k)!r}'
                    `for` k `in` self.__dflts__
                    `if` getattr(self, k) != self.__dflts__[k]
                  ]
            `return` f'{classname}({', '.join(rep)})'
        *`# Build the newdict that we'll use as class dict for the`*
        *`# new class`*
        newdict = {'__slots__': [], '__dflts__': {},
                   '__init__': __init__, '__repr__' :__repr__,}
        `for` k `in` classdict:
            `if` k.startswith('__') `and` k.endswith('__'):
                *`# Dunder methods: copy to newdict, or warn`*
                *`# about conflicts`*
                `if` k `in` newdict:
                    warnings.warn(f'Cannot set attr {k!r}'
                                  f' in bunch-class {classname!r}')
 `else``:`
                    newdict[k] = classdict[k]
 `else``:`
                *`# Class variables: store name in __slots__, and`*
                *`# name and value as an item in __dflts__`*
                newdict['__slots__'].append(k)
                newdict['__dflts__'][k] = classdict[k]
        *`# Finally, delegate the rest of the work to type.__new__`*
        `return` super().__new__(mcl, classname, bases, newdict)

`class` Bunch(metaclass=MetaBunch):
    *`"""For convenience: inheriting from Bunch can be used to get`*
       *`the new metaclass (same as defining metaclass= yourself).`*
    *`"""`*
 `pass`

数据类

正如前面的 Bunch 类所示,一个其实例仅仅是一组命名数据项的类是非常方便的。Python 的标准库通过 dataclasses 模块涵盖了这一点。

你将使用 dataclass 函数,它是 dataclasses 模块的主要特性:一种装饰器,你可以将其应用于希望成为一组命名数据项的任何类的实例。作为一个典型的例子,考虑以下代码:

`import` dataclasses
`@`dataclasses.dataclass
`class` Point:
    x: float
    y: float

现在,您可以调用例如 pt = Point(0.5, 0.5),并获得一个具有 pt.x 和 pt.y 属性的变量,每个属性都等于 0.5。默认情况下,dataclass 装饰器已经为 Point 类赋予了一个接受属性 x 和 y 的初始浮点值的 init 方法,并准备好适当显示类的任何实例的 repr 方法:

>>> pt
Point(x=0.5, y=0.5)

dataclass 函数接受许多可选的命名参数,以便调整装饰的类的详细信息。您可能经常明确使用的参数列在 Table 4-8 中。

Table 4-8. dataclass 函数常用参数

Parameter nameDefault value and resulting behavior
eqTrue 当为 True 时,生成一个 eq 方法(除非类已定义了一个)
frozenFalse 当为 True 时,使得类的每个实例为只读(不允许重新绑定或删除属性)
initTrue 当为 True 时,生成一个 init 方法(除非类已定义了一个)
kw_onlyFalse 3.10+ 当为 True 时,强制要求将参数传递给 init 方法时使用命名方式,而非位置方式
orderFalse 当为 True 时,生成顺序比较的特殊方法(如 lelt 等),除非类已定义这些方法
reprTrue 当为 True 时,生成一个 repr 方法(除非类已定义了一个)
slotsFalse 3.10+ 当为 True 时,向类添加适当的 slots 属性(为每个实例节省一些内存,但不允许向类实例添加其他任意属性)

当设置 frozen 为 True 时,装饰器还会为类添加一个 hash 方法(允许实例作为字典的键和集合的成员),当这是安全的时候(通常是这样的情况)。即使在不安全的情况下,您也可以强制添加 hash 方法,但我们强烈建议您不要这样做;如果您坚持要这样做,请查阅 online docs 了解详细信息。

如果需要在自动生成的 init 方法完成为每个实例属性分配核心工作后调整数据类的每个实例,请定义一个名为 post_init 的方法,装饰器将确保在 init 完成后立即调用它。

假设您希望向 Point 添加一个属性,以捕获创建点的时间。可以将其添加为在 post_init 中分配的属性,为 Point 的定义成员添加名为 create_time 的属性,类型为 float,默认值为 0,并添加一个 post_init 的实现:

`def` __post_init__(self):
    self.create_time = time.time()

现在,如果您创建变量 pt = Point(0.5, 0.5),打印它将显示创建时间戳,类似于以下内容:

>>> pt
Point(x=0.5, y=0.5, create_time=1645122864.3553088)

与常规类似,dataclass 还支持额外的方法和属性,例如计算两个点之间距离的方法以及返回到原点的点的距离的属性:

`def` distance_from(self, other):
    dx, dy = self.x - other.x, self.y - other.y
    `return` math.hypot(dx, dy)

@property
`def` distance_from_origin(self):
    `return` self.distance_from(Point(0, 0))

例如:

>>> pt.distance_from(Point(-1, -1))
2.1213203435596424
>>> pt.distance_from_origin
0.7071067811865476

dataclasses 模块还提供了 asdict 和 astuple 函数,每个函数的第一个参数都是 dataclass 实例,分别返回一个字典和一个元组,这些字典和元组包含类的字段。此外,该模块还提供了一个 field 函数,用于自定义数据类字段(即实例属性)的处理方式,以及几个其他专门用于非常高级、神秘目的的函数和类;要了解有关它们的全部信息,请查阅在线文档

枚举类型(Enums)

在编程时,通常希望创建一组相关的值,用于列举特定属性或程序设置的可能值,¹⁹ 无论它们是什么:终端颜色、日志级别、进程状态、扑克牌花色、服装尺寸,或者你能想到的任何其他东西。枚举类型(enum)是定义这种值组的一种类型,具有可作为类型化全局常量使用的符号名称。Python 提供了 enum 模块中的 Enum 类及其相关子类用于定义枚举。

定义一个枚举为你的代码提供了一组代表枚举中的值的符号常量。在没有枚举的情况下,常量可能会被定义为整数,如下所示:

*`# colors`*
RED = 1
GREEN = 2
BLUE = 3

*`# sizes`*
XS = 1
S = 2
M = 3
L = 4
XL = 5

然而,在这种设计中,没有机制可以警告类似 RED > XL 或 L * BLUE 这样的无意义表达式,因为它们都只是整数。也没有颜色或尺码的逻辑分组。

相反,你可以使用 Enum 子类来定义这些值:

`from` enum `import` Enum, auto

`class` Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

`class` Size(Enum):
    XS = auto()
    S = auto()
    M = auto()
    L = auto()
    XL = auto()

现在,像 Color.RED > Size.S 这样的代码在视觉上显得不正确,并且在运行时会引发 Python TypeError。使用 auto() 自动分配从 1 开始递增的整数值(在大多数情况下,分配给枚举成员的实际值是无意义的)。

调用 Enum 创建一个类,而不是一个实例

令人惊讶的是,当你调用 enum.Enum() 时,它不会返回一个新建的实例,而是一个新建的子类。因此,前面的片段等效于:

`from` enum `import` Enum
Color = Enum('Color', ('RED', 'GREEN', 'BLUE'))
Size = Enum('Size', 'XS S M L XL')

当你调用 Enum(而不是在类语句中显式地对其进行子类化)时,第一个参数是你正在构建的子类的名称;第二个参数给出了该子类成员的所有名称,可以是字符串序列或单个以空格分隔(或逗号分隔)的字符串。

我们建议您使用类继承语法定义 Enum 子类,而不是这种简写形式。形式更加视觉明确,因此更容易看出是否缺少、拼写错误或以后添加的成员。

枚举内部的值称为其成员。习惯上,使用全大写字符来命名枚举成员,将它们视为显式常量。枚举成员的典型用法包括赋值和身份检查:

`while` process_state `is` ProcessState.RUNNING:
 *`# running process code goes here`*
    `if` processing_completed():
        process_state = ProcessState.IDLE

通过迭代枚举类本身或从类的 members 属性获取,你可以获得枚举的所有成员。枚举成员都是全局单例,因此推荐使用 isis not 进行比较,而不是 == 或 !=。

枚举模块包含几个类²⁰,支持不同形式的枚举,列在 表 4-9 中。

表 4-9. 枚举类

描述
枚举基本枚举类;成员值可以是任何 Python 对象,通常是整数或字符串,但不支持整数或字符串方法。适用于定义成员为无序组的枚举类型。
Flag用于定义可以使用操作符 |, &, ^ 和 ~ 进行组合的枚举;成员值必须定义为整数以支持这些位操作(Python 但是不假定它们之间的顺序)。值为 0 的 Flag 成员为假;其他成员为真。在创建或检查使用位操作的值时非常有用(例如文件权限)。为了支持位操作,通常使用 2 的幂次方(1、2、4、8 等)作为成员值。
IntEnum相当于 class IntEnum(int, Enum);成员值为整数并支持所有整数操作,包括排序。在需要对值进行排序时非常有用,比如定义日志级别。
IntFlag相当于 class IntFlag(int, Flag);成员值为整数(通常是 2 的幂次方),支持所有整数操作,包括比较。
StrEnum3.11+ 相当于 class StrEnum(str, Enum);成员值为字符串并支持所有字符串操作。

枚举模块还定义了一些支持函数,列在 表 4-10 中。

表 4-10. 枚举支持函数

支持函数描述
自动在定义成员时自动递增成员值。通常值从 1 开始,每次增加 1;对于 Flag,增量为 2 的幂次方。
唯一类装饰器,确保成员值彼此不同。

下面的示例展示了如何定义一个 Flag 子类,以处理从调用 os.stat 或 Path.stat 返回的 st_mode 属性中的文件权限(有关 stat 函数的描述,请参见 第十一章):

`import` enum
`import` stat

`class` Permission(enum.Flag):
    EXEC_OTH = stat.S_IXOTH
    WRITE_OTH = stat.S_IWOTH
    READ_OTH = stat.S_IROTH
    EXEC_GRP = stat.S_IXGRP
    WRITE_GRP = stat.S_IWGRP
    READ_GRP = stat.S_IRGRP
    EXEC_USR = stat.S_IXUSR
    WRITE_USR = stat.S_IWUSR
    READ_USR = stat.S_IRUSR

    @classmethod
    `def` from_stat(cls, stat_result):
        `return` cls(stat_result.st_mode & 0o777)

`from` pathlib `import` Path

cur_dir = Path.cwd()
dir_perm = Permission.from_stat(cur_dir.stat())
`if` dir_perm & Permission.READ_OTH:
    print(f'{cur_dir} is readable by users outside the owner group')

*`# the following raises TypeError: Flag enums do not support order`* 
*`# comparisons`*
print(Permission.READ_USR > Permission.READ_OTH)

在代码中使用枚举替代任意的整数或字符串可以提升可读性和类型完整性。你可以在 Python 文档 中找到枚举模块的更多类和方法的详细信息。

¹ 或者,根据一位评论者的观点,也可以说是“缺点”。有人的福祸相依。

² 当情况如此时,在 metaclass= 后也可以有其他命名参数。这些参数(如果有)将传递给元类。

³ 这种需求是因为在 Singleton 的任何子类上定义了这个特殊方法的情况下,init 会在每次你实例化子类时重复执行,在每个 Singleton 子类的唯一实例上执行。

⁴ 除了定义了 slots 的类的实例外,涵盖在 “slots” 中。

⁵ 其他一些面向对象语言,如Modula-3,同样需要显式地使用 self。

⁶ 多个 Python 版本之后,Michele 的论文仍然适用!

⁷ 其中一位作者使用这种技术动态组合小的混合测试类,创建复杂的测试用例类来测试多个独立的产品特性。

⁸ 为了完整引用常被截断的名言:“当然除了太多的间接问题。”

⁹ 第三方扩展还可以定义不是序列、映射或集合的容器类型。

¹⁰ 包含下限,排除上限——这一点对于 Python 来说一直是规范。

¹¹ 参见例如 “避免扩展类”,作者是 Bill Harlan。

¹² 关于类型检查的相关概念,请参阅 typing.Protocols,涵盖在 “协议” 中。

¹³ abc 模块确实包含 abstractproperty 装饰器,它结合了这两者,但 abstractproperty 已经弃用,新代码应该按描述使用这两个装饰器。

¹⁴ 为了向后兼容性,这些 ABCs 在 Python 3.9 之前也可以在 collections 模块中访问,但在 Python 3.10 中移除了兼容性导入。新代码应该从 collections.abc 导入这些 ABCs。

¹⁵ 严格来说,类 C 的类型可以说是 C 的实例的元类,而不是 C 本身,但这种微妙的语义区别在实践中很少被注意到。

¹⁶ 或者当基类有 init_subclass 的情况下,命名参数将传递给该方法,如 “简单类定制的替代方案” 中描述的那样。

¹⁷ 这类似于调用 type 函数的三个参数版本,如 “使用 type 内置函数动态定义类” 中描述的那样。

¹⁸ init_subclass,在 “简单类定制的替代方案” 中讨论过,工作方式类似于“继承装饰器”,因此通常是自定义元类的替代选择。

¹⁹ 不要将这个概念与无关的内置函数 enumerate 混淆,该函数在 第八章 中介绍,它从可迭代对象生成 (序号, ) 对。

²⁰ enum 的专用元类与通常的类型元类行为差异如此之大,以至于值得指出 enum.Enum 和普通类之间的所有差异。你可以在 Python 在线文档的 “枚举有何不同?”章节 中阅读有关内容。

第五章:类型注释

使用类型信息对 Python 代码进行注释是一个可选步骤,在开发和维护大型项目或库时可能非常有用。静态类型检查器和 lint 工具可帮助识别和定位函数参数和返回值的数据类型不匹配。IDE 可以使用这些类型注释(也称为类型提示)来改进自动完成,并提供弹出式文档。第三方软件包和框架可以使用类型注释来定制运行时行为,或者根据方法和变量的类型注释自动生成代码。

Python 中的类型注释和检查仍在不断发展,并涉及许多复杂的问题。本章涵盖了类型注释的一些最常见用例;您可以在本章末尾列出的资源中找到更全面的资料。

Python 版本的类型注释支持因版本而异

支持类型注释的 Python 功能已经从一个版本发展到另一个版本,其中包括一些重大的增加和删除。本章的其余部分将描述 Python 最新版本(3.10 及更高版本)中的类型注释支持,其中包含一些可能在其他版本中存在或不存在的功能的注释。

历史

Python 本质上是一种动态类型语言。这使您能够通过命名和使用变量来快速开发代码,而无需声明它们。动态类型允许使用灵活的编码习惯、通用容器和多态数据处理,而无需显式定义接口类型或类层次结构。缺点是,在开发过程中,语言无法在传递给函数或从函数返回的不兼容类型的变量上提供帮助。Python 不像一些语言那样利用开发时编译步骤来检测和报告数据类型问题,而是依靠开发人员通过一系列测试用例在运行时环境中重建来发现数据类型错误。

类型注释不是强制性的

类型注释在运行时被强制执行。Python 不执行任何基于类型注释的类型验证或数据转换;可执行的 Python 代码仍然负责正确使用变量和函数参数。但是,类型注释必须在语法上是正确的。包含无效类型注释的延迟导入或动态导入模块会在运行中的 Python 程序中引发 SyntaxError 异常,就像任何无效的 Python 语句一样。

历史上,Python 缺乏任何类型检查被认为是其短板之一,一些程序员因此选择其他编程语言。然而,社区希望 Python 保持其运行时类型自由,因此逻辑上的做法是增加对由类似 lint 工具(在下一节进一步描述)和 IDE 执行的静态类型检查的支持。一些尝试是基于解析函数签名或文档字符串进行类型检查。Guido van Rossum 在Python 开发者邮件列表上引用了几个案例,显示类型注解可以帮助,例如在维护大型遗留代码库时。使用注解语法,开发工具可以执行静态类型检查,以突出显示与预期类型冲突的变量和函数使用。

类型注解的第一个官方版本使用特殊格式的注释来指示变量类型和返回代码,如PEP 484所定义的,这是 Python 3.5 的一项临时 PEP。² 使用注释可以快速实现和尝试新的类型语法,而无需修改 Python 编译器本身。³ 第三方包mypy通过使用这些注释进行静态类型检查得到了广泛接受。随着 Python 3.6 采纳了PEP 526,类型注解已完全整合到 Python 语言本身,并在标准库中添加了一个支持的 typing 模块。

类型检查工具

随着类型注解成为 Python 的一个已确立部分,类型检查工具和 IDE 插件也成为 Python 生态系统的一部分。

mypy

独立的mypy实用程序继续作为静态类型检查的主要工具,始终与 Python 类型注解形式的演变保持最新状态(只要考虑 Python 版本!)。mypy 还作为插件提供给编辑器,包括 Vim、Emacs 和 SublimeText,以及 Atom、PyCharm 和 VS Code IDE。 (PyCharm、VS Code 和 Wing IDE 还单独包含了自己的类型检查功能,与 mypy 分开)。运行 mypy 的最常见命令只是mypy my_python_script.py

您可以在mypy 在线文档中找到更详细的用法示例和命令行选项,以及一个作为便捷参考的速查表。本节后面的代码示例将包含 mypy 错误消息示例,以说明可以通过类型检查捕获的 Python 错误类型。

其他类型检查器

其他考虑使用的类型检查器包括:

MonkeyType

Instagram 的 MonkeyType 使用 sys.setprofile 钩子在运行时动态检测类型;像 pytype 一样(见下文),它也可以生成 .pyi(存根)文件,而不是或者除了在 Python 代码文件中插入类型注解。

pydantic

pydantic 也可以在运行时工作,但不生成存根或插入类型注解;它的主要目标是解析输入并确保 Python 代码获得干净的数据。正如在线文档中所述,它还允许您扩展其验证功能以适应自己的环境。参见“FastAPI”的简单示例。

Pylance

Pylance 是一个类型检查模块,主要用于将 Pyright(见下文)嵌入到 VS Code 中。

Pyre

Facebook 的 Pyre 也可以生成 .pyi 文件。目前在 Windows 上无法运行,除非安装了 Windows Subsystem for Linux (WSL)

Pyright

Pyright 是微软的静态类型检查工具,作为命令行实用程序和 VS Code 扩展提供。

pytype

pytype 是谷歌的静态类型检查器,专注于类型推断(即使在没有类型提示的情况下也能提供建议),除了类型注解。类型推断提供了强大的能力,即使在没有注解的代码中也能检测类型错误。pytype 还可以生成 .pyi 文件,并将存根文件合并回 .py 源代码中(最新版本的 mypy 也在效仿)。目前,pytype 在 Windows 上无法运行,除非你首先安装 WSL

多个主要软件组织开发的类型检查应用的出现,证明了 Python 开发者社区在使用类型注解方面的广泛兴趣。

类型注解语法

类型注解 在 Python 中使用以下形式指定:

*`identifier`*: *`type_specification`*

type_specification 可以是任何 Python 表达式,但通常涉及一个或多个内置类型(例如,仅提到 Python 类型就是一个完全有效的表达式)和/或从 typing 模块导入的属性(在下一节中讨论)。典型的形式是:

*`type_specifier`*[*`type_parameter`*, ...]

这里是一些作为变量类型注解使用的类型表达式示例:

`import` typing

*`# an int`*
count: int

*`# a list of ints, with a default value`*
counts: list[int] = []

*`# a dict with str keys, values are tuples containing 2 ints and a str`*
employee_data: dict[str, tuple[int, int, str]]

*`# a callable taking a single str or bytes argument and returning a bool`*
str_predicate_function: typing.Callable[[str | bytes], bool]

*`# a dict with str keys, whose values are functions that take and return`* 
*`# an int`*
str_function_map: dict[str, typing.Callable[[int], int]] = {
    'square': `lambda` x: x * x,
    'cube': `lambda` x: x * x * x,
}

请注意,lambda 不接受类型注解。

要为函数添加返回类型注解,请使用以下形式:

`def` identifier(argument, ...) -> type_specification :

每个 参数 的形式如下:

*`identifier`*[: *`type_specification`*[ = *`default_value`*]]

这是一个带有注解函数的示例:

`def` pad(a: list[str], min_len: int = 1, padstr: str = ' ') -> list[str]:
    *`"""Given a list of strings and a minimum length, return a copy of`*
 *`the list extended with "padding" strings to be at least the`*
 *`minimum length.`*
 *`"""`*
    `return` a + ([padstr] * (min_len - len(a)))

注意,当带有默认值的注解参数时,PEP 8 建议在等号周围使用空格。

还未完全定义的前向引用类型

有时,函数或变量定义需要引用尚未定义的类型。这在类方法或必须定义当前类类型的参数或返回值的方法中非常常见。这些函数签名在编译时解析,此时类型尚未定义。例如,此类方法无法编译通过:

`class` A:
    @classmethod
    `def` factory_method(cls) -> A:
        *`# ... method body goes here ...`*

由于 Python 编译 factory_method 时,类 A 尚未定义,因此代码会引发 NameError 错误。

问题可以通过在类型 A 的返回类型中添加引号来解决:

`class` A:
    @classmethod
    `def` factory_method(cls) -> 'A':
        *`# ... method body goes here ...`*

未来版本的 Python 可能会推迟对类型注解的评估,直到运行时,从而使封闭引号变得不必要(Python 的指导委员会正在评估各种可能性)。您可以使用 from future import annotations 预览此行为。

typing 模块

typing 模块支持类型提示。它包含在创建类型注释时有用的定义,包括:

  • 用于定义类型的类和函数

  • 用于修改类型表达式的类和函数

  • 抽象基类(ABCs)

  • 协议

  • 实用程序和装饰器

  • 用于定义自定义类型的类

类型

typing 模块最初的实现包括对应于 Python 内置容器和其他类型的类型定义,以及标准库模块中的类型。许多这些类型已被弃用(见下文),但某些仍然有用,因为它们不直接对应任何 Python 内置类型。 Table 5-1 列出了在 Python 3.9 及更高版本中仍然有用的 typing 类型。

表 5-1. typing 模块中有用的定义

Type描述
Any匹配任何类型。
AnyStr等效于 str | bytes。AnyStr 用于注释函数参数和返回类型,其中任一字符串类型都可以接受,但不应在多个参数之间混合使用,或者在参数和返回类型之间混合使用。
BinaryIO匹配具有二进制(bytes)内容的流,例如使用 mode='b' 打开的流或 io.BytesIO。
CallableCallable[[argument_type, ...], return_type] 定义可调用对象的类型签名。接受与可调用对象的参数对应的类型列表,以及函数返回值的类型。如果可调用对象不接受任何参数,请使用空列表 []。如果可调用对象没有返回值,请使用 None 作为 return_type
IO等效于 BinaryIO | TextIO。
Lit⁠e⁠r⁠a⁠l​[⁠e⁠x⁠p⁠ression,...]3.8+ 指定变量可能采用的有效值列表。
LiteralString3.11+ 指定必须实现为文字引号值的 str。用于防止代码易受到注入攻击。
NoReturn用作“永久运行”函数的返回类型,比如调用 http.serve_forever 或 event_loop.run_forever 而没有返回值的情况。这不适用于简单返回无明确值的函数;对于这种情况,请使用 -> None。更多有关返回类型的讨论详见“为现有代码添加类型注解(逐步类型化)”。
Self3.11+ 用作实例函数返回类型,返回 self(以及其他少数情况,详见PEP 673)。
TextIO匹配文本流(str 类型内容),比如使用 mode='t' 打开的文件或者 io.StringIO 返回的对象。

-3.9 在 Python 3.9 之前,typing 模块的定义用于创建表示内置类型的类型,例如 List[int] 表示整数列表。从 Python 3.9 开始,这些名称已弃用,因为其对应的内置或标准库类型现在支持 [] 语法:整数列表现在简单地使用 list[int] 类型声明。Table 5-2 列出了在 Python 3.9 之前使用 typing 模块进行类型注解时必要的定义。

Table 5-2. Python 内置类型及其在 typing 模块中 3.9 之前的定义

内置类型Python 3.9 前的 typing 模块等效类型
dictDict
frozensetFrozenSet
listList
setSet
strText
tupleTuple
typeType
collections.ChainMapChainMap
collections.CounterCounter
collections.defaultdictDefaultDict
collections.dequeDeque
collections.OrderedDictOrderedDict
re.MatchMatch
re.PatternPattern

Type Expression Parameters

在 typing 模块中定义的某些类型修改其他类型表达式。Table 5-3 列出的类型提供了关于 type_expression 修改类型的额外类型信息或约束。

Table 5-3. 类型表达式参数

Parameter用法和描述
AnnotatedAnnotated[type_expression, expression, ...] 3.9+ 用额外的元数据扩展 type_expression。函数 fn 的额外元数据可以在运行时使用 get_type_hints(fn, include_extras=True) 获取。
ClassVarClassVar[type_expression] 表示变量是类变量,不应该作为实例变量赋值。
FinalFinal[type_expression] 3.8+ 表示变量不应该在子类中写入或重写。
OptionalOptional[type_expression] Equivalent to type_expression | None. Often used for named arguments with a default value of None. (Optional does not automatically define None as the default value, so you must still follow it with =None in a function signature.) 3.10+ With the availability of the | operator for specifying alternative type attributes, there is a growing consensus to prefer type_expression | None over using Optional[type_expression].

抽象基类

与内置类型类似,typing 模块的初始实现包括了与 collections.abc 模块中的抽象基类对应的类型定义。许多这些类型后来已被弃用(见下文),但两个定义已保留为 collections.abc 中 ABCs 的别名(见 表格 5-4)。

表格 5-4. 抽象基类别名

TypeMethod subclasses must implement
Hashablehash
Sizedlen

-3.9 在 Python 3.9 之前,typing 模块中的以下定义表示在 collections.abc 模块中定义的抽象基类,例如 Sequence[int] 用于整数序列。从 3.9 开始,typing 模块中这些名称已被弃用,因为它们在 collections.abc 中对应的类型现在支持 [] 语法:

AbstractSetContainerMapping
AsyncContextManagerContextManagerMappingView
AsyncGeneratorCoroutineMutableMapping
AsyncIterableGeneratorMutableSequence
AsyncIteratorItemsViewMutableSet
AwaitableIterableReversible
ByteStringIteratorSequence
CollectionKeysViewValuesView

协议

typing 模块定义了几个协议,类似于其他一些语言称为“接口”的概念。协议是抽象基类,旨在简洁表达对类型的约束,确保其包含某些方法。typing 模块中当前定义的每个协议都与单个特殊方法相关,其名称以 Supports 开头,后跟方法名(然而,如 typeshed 中定义的其他库可能不遵循相同的约束)。协议可用作确定类对该协议功能支持的最小抽象类:要遵守协议,类所需做的就是实现协议的特殊方法。

表格 5-5 列出了 typing 模块中定义的协议。

表格 5-5. typing 模块中的协议及其必需方法

ProtocolHas method
SupportsAbsabs
SupportsBytesbytes
SupportsComplexcomplex
SupportsFloatfloat
SupportsIndex 3.8+index
SupportsIntint
SupportsRoundround

类不必显式从协议继承以满足 issubclass(clsprotocol_type),或使其实例满足 isinstance(objprotocol_type)。类只需实现协议中定义的方法即可。例如,想象一个实现罗马数字的类:

`class` RomanNumeral:
    *`"""Class representing some Roman numerals and their int`* 
 *`values.`*
 *`"""`*
    int_values = {'I': 1, 'II': 2, 'III': 3, 'IV': 4, 'V': 5}

    `def` __init__(self, label: str):
        self.label = label

    `def` __int__(self) -> int:
        `return` RomanNumeral.int_values[self.label]

要创建此类的实例(例如,表示电影标题中的续集)并获取其值,您可以使用以下代码:

>>> movie_sequel = RomanNumeral('II')
>>> print(int(movie_sequel))
2

RomanNumeral 满足 issubclass,并且由于实现了 int,与 SupportsInt 进行 isinstance 检查,即使它不是显式从协议类 SupportsInt 继承:⁴

>>> issubclass(RomanNumeral, typing.SupportsInt)
True
>>> isinstance(movie_sequel, typing.SupportsInt)
True

实用程序和装饰器

Table 5-6 列出了在 typing 模块中定义的常用函数和装饰器;接下来是一些示例。

表 5-6.定义在 typing 模块中的常用函数和装饰器

Function/decorator用法和描述
castcast(typevar)向静态类型检查器发出信号,var应被视为type类型。返回var;在运行时,var没有更改、转换或验证。请参见表后的示例。
final@final 3.8+ 用于装饰类定义中的方法,如果该方法在子类中被重写则发出警告。也可以用作类装饰器,用于警告是否正在对类本身进行子类化。
get_argsget_args(custom_type)返回用于构造自定义类型的参数。
get_originget_origin(custom_type)3.8+ 返回用于构造自定义类型的基础类型。
get_type_hintsget_type_hints(obj)返回结果,就像访问obj.annotations 一样。可以使用可选的 globalns 和 localns 命名空间参数调用,以解析作为字符串给出的前向类型引用,和/或使用包含 Annotations 的任何非类型注释的可选 Boolean include_extras 参数。
NewTypeNewType(type_nametype)定义了从type派生的自定义类型。type_name是一个字符串,应与分配 NewType 的局部变量匹配。用于区分常见类型的不同用途,例如用于员工姓名的 str 与用于部门名称的 str。有关此函数的更多信息,请参见“NewType”。
no_type_check@no_type_check 用于指示注释不打算用作类型信息。可应用于类或函数。
no_type_che⁠c⁠k⁠_​d⁠e⁠c⁠orator@no_type_check_decorator 用于向另一个装饰器添加 no_type_check 行为。
overload@overload 用于允许定义多个方法,名称相同但签名类型不同。请参见表后的示例。
r⁠u⁠n⁠t⁠i⁠m⁠e⁠_​c⁠h⁠e⁠c⁠k⁠a⁠b⁠l⁠e@runtime_checkable 3.8+ 用于为自定义协议类添加 isinstance 和 issubclass 支持。有关此装饰器的更多信息,请参见 “Using Type Annotations at Runtime” 。
TypeAliasname: TypeAlias = type_expression 3.10+ 用于区分类型别名的定义和简单赋值。在 type_expression 是简单类名或字符串值引用尚未定义的类的情况下最有用,这可能看起来像是一个赋值。TypeAlias 只能在模块范围内使用。一个常见用法是使得一致重用冗长的类型表达式变得更容易,例如:Number: TypeAlias = int | float | Fraction。更多关于此注解的信息,请参见 “TypeAlias” 。
type_check_only@type_check_only 用于指示类或函数仅在类型检查时使用,而在运行时不可用。
TYPE_CHECKING一个特殊的常量,静态类型检查器将其评估为 True,但在运行时设置为 False。使用它可以跳过导入用于支持类型检查的大型、导入缓慢的模块(以便在运行时不需要该导入)。
TypeVarTypeVar(type_name, *types) 定义用于复杂泛型类型中的类型表达式元素,使用 Generic。type_name 是一个字符串,应与分配给 TypeVar 的局部变量匹配。如果未提供 types,则相关的 Generic 将接受任何类型的实例。如果提供了 types,则 Generic 将仅接受提供的类型或其子类的实例。还接受名为协变和逆变(默认为 False)的布尔参数,以及 bound 参数。关于这些参数的详细信息,请参见 “Generics and TypeVars” 和 typing 模块文档

在类型检查时使用 overload 来标记必须以特定组合使用的命名参数。在这种情况下,fn 必须以 str 键和 int 值对或单个布尔值调用:

@typing.overload
`def` fn(*, key: str, value: int):
    `.``.``.`

@typing.overload
`def` fn(*, strict: bool):
    `.``.``.`

`def` fn(**kwargs):
    *`# implementation goes here, including handling of differing`* 
    *`# named arguments`*
    `pass`

*`# valid calls`*
fn(key='abc', value=100)
fn(strict=True)

*`# invalid calls`*
fn(1)
fn('abc')
fn('abc', 100)
fn(key='abc')
fn(`True`)
fn(strict=True, value=100)

请注意,overload 装饰器仅用于静态类型检查。要根据参数类型在运行时实际分派到不同方法,请使用 functools.singledispatch。

使用 cast 函数可以强制类型检查器在 cast 的作用域内将变量视为特定类型:

`def` func(x: list[int] | list[str]):
    `try`:
        `return` sum(x)
    `except` TypeError:
        x = cast(list[str], x)
        `return` ','.join(x)

谨慎使用 cast

cast 是一种覆盖代码中特定位置可能存在的所有推断或先前注释的方法。它可能隐藏代码中的实际类型错误,导致类型检查通行不完整或不准确。在前面的示例中,func 本身不会引发任何 mypy 警告,但如果传递了混合整数和字符串的列表,则在运行时会失败。

定义自定义类型

正如 Python 的class语法允许创建新的运行时类型和行为一样,本节讨论的 typing 模块构造使得能够创建用于高级类型检查的专门类型表达式。

typing 模块包括三个类,你可以继承这些类来获取类型定义和其他默认特性,详见表 5-7。

表 5-7. 定义自定义类型的基类

GenericGeneric[type_var, ...] 定义了一个类型检查抽象基类,用于类的方法引用一个或多个 TypeVar 定义的类型。泛型将在以下小节详述。
NamedTupleNamedTuple 是 collections.namedtuple 的有类型实现。详见“NamedTuple”获取更多详情和示例。
TypedDictTypedDict 3.8+ 定义了一个类型检查字典,其具有每个键的特定键和值类型。详见“TypedDict”了解详情。

泛型和 TypeVar

泛型 是定义类模板的类型,这些类可以根据一个或多个类型参数调整其方法签名的类型注释。例如,dict 是一个泛型,接受两个类型参数:字典键的类型和字典值的类型。以下是如何使用 dict 来定义一个将颜色名称映射到 RGB 三元组的字典:

color_lookup: dict[str, tuple[int, int, int]] = {}

变量 color_lookup 将支持如下语句:

color_lookup['red'] = (255, 0, 0)
color_lookup['red'][2]

然而,以下语句由于键或值类型不匹配而生成了 mypy 错误:

color_lookup[0]
error: Invalid index type "int" for "dict[str, tuple[int, int, int]]";
expected type "str"
color_lookup['red'] = (255, 0, 0, 0)
error: Incompatible types in assignment (expression has type
"tuple[int, int, int, int]", target has type "tuple[int, int, int]")

泛型类型允许在一个类中定义与该类所处理对象的具体类型无关的行为。泛型通常用于定义容器类型,如 dict、list、set 等。通过定义泛型类型,我们避免了对 DictOfStrInt、DictOfIntEmployee 等详细定义类型的必要性。相反,泛型 dict 被定义为 dict[KT, VT],其中KTVT是字典的键类型和值类型的占位符,并且可以在实例化字典时定义任何特定类型。

举个例子,让我们定义一个假想的泛型类:一个累加器,可以更新值,但也支持撤销方法。由于累加器是一个泛型容器,我们声明一个 TypeVar 来表示所包含对象的类型:

`import` typing
T = typing.TypeVar('T')

累加器类被定义为泛型的子类,其中 T 作为类型参数。以下是类声明及其 init 方法,它创建了一个包含对象类型 T 的初始为空的列表:

`class` Accumulator(typing.Generic[T]):
    `def` __init__(self):
        self._contents: list[T] = []

要添加 update 和 undo 方法,我们定义引用类型 T 的参数,表示所包含的对象类型:

    `def` update(self, *args: T) -> `None`:
        self._contents.extend(args)

    `def` undo(self) -> `None`:
        *`# remove last value added`*
        `if` self._contents:
            self._contents.pop()

最后,我们添加 leniter 方法,以便可以对累加器实例进行迭代:

    `def` __len__(self) -> int:
        `return` len(self._contents)

    `def` __iter__(self) -> typing.Iterator[T]:
        `return` iter(self._contents)

现在,可以使用 Accumulator[int]编写代码来收集多个整数值:

acc: Accumulator[int] = Accumulator()
acc.update(1, 2, 3)
print(sum(acc))  # prints 6
acc.undo()
print(sum(acc))  # prints 3

因为 acc 是包含 ints 的 Accumulator,所以下面的语句会生成 mypy 错误消息:

acc.update('A')
error: Argument 1 to "update" of "Accumulator" has incompatible type
"str"; expected "int"
print(''.join(acc))
error: Argument 1 to "join" of "str" has incompatible type
"Accumulator[int]"; expected "Iterable[str]"

限制 TypeVar 为特定类型

在我们的 Accumulator 类中,我们从未直接调用所包含的 T 对象的方法。对于这个示例,T TypeVar 是纯粹无类型的,因此像 mypy 这样的类型检查器无法推断出 T 对象的任何属性或方法的存在。如果泛型需要访问其包含的 T 对象的属性,则应使用 TypeVar 的修改形式来定义 T。

下面是一些 TypeVar 定义的示例:

*`# T must be one of the types listed (int, float, complex, or str)`*
T = typing.TypeVar('T', int, float, complex, str)
*`# T must be the class MyClass or a subclass of the class MyClass`*
T = typing.TypeVar('T', bound=MyClass)
*`# T must implement __len__ to be a valid subclass of the Sized protocol`*
T = typing.TypeVar('T', bound=collections.abc.Sized)

这些形式的 T 允许在 T 的 TypeVar 定义中使用这些类型的方法。

NamedTuple

collections.namedtuple 函数简化了支持对元组元素进行命名访问的类似类的元组类型的定义。NamedTuple 提供了此功能的类型化版本,使用类似于数据类(在“数据类”中介绍)的属性样式语法的类。下面是一个具有四个元素的 NamedTuple,带有名称、类型和可选默认值:

`class` HouseListingTuple(typing.NamedTuple):
    address: str
    list_price: int
    square_footage: int = 0
    condition: str = 'Good'

NamedTuple 类生成一个默认的构造函数,接受每个命名字段的位置参数或命名参数:

listing1 = HouseListingTuple(
    address='123 Main',
    list_price=100_000,
    square_footage=2400,
    condition='Good',
)

print(listing1.address)  *`# prints: 123 Main`*
print(type(listing1))    *`# prints: <class 'HouseListingTuple'>`*

尝试创建元组时如果元素数量过少会引发运行时错误:

listing2 = HouseListingTuple(
    '123 Main',
)
*`# raises a runtime error: TypeError: HouseListingTuple.__new__()` 
`# missing 1 required positional argument: 'list_price'`*

TypedDict

3.8+ Python 字典变量在旧代码库中经常难以理解,因为字典有两种用法:作为键/值对的集合(例如,从用户 ID 到用户名的映射),以及将已知字段名映射到值的记录。通常很容易看出函数参数将作为字典传递,但实际的键和值类型取决于可能调用该函数的代码。除了简单地定义字典可以是一个 str 到 int 值的映射,例如 dict[str, int],TypedDict 还定义了预期的键和每个相应值的类型。以下示例定义了之前房屋列表类型的 TypedDict 版本(注意,TypedDict 定义不接受默认值定义):

`class` HouseListingDict(typing.TypedDict):
    address: str
    list_price: int
    square_footage: int
    condition: str

TypedDict 类生成一个默认的构造函数,为每个定义的键接受命名参数:

listing1 = HouseListingDict(
    address='123 Main',
    list_price=100_000,
    square_footage=2400,
    condition='Good',
)

print(listing1['address'])  # prints *`123 Main`*
print(type(listing1))  # prints *`<class 'dict'>`*

listing2 = HouseListingDict(
    address='124 Main',
    list_price=110_000,
)

与 NamedTuple 示例不同,listing2 不会引发运行时错误,只是创建一个具有给定键的字典。但是,mypy 将使用消息标记 listing2 为类型错误:

error: Missing keys ("square_footage", "condition") for TypedDict
"HouseListing"

要向类型检查器指示某些键可能被省略(但仍然验证给定的键),请将 total=False 添加到类声明中:

`class` HouseListing(typing.TypedDict, total=False):
    *`# ...`*

3.11+ 个别字段还可以使用 Required 或 NotRequired 类型注释显式地标记它们为必需或可选:

`class` HouseListing(typing.TypedDict):
    address: typing.Required[str]
    list_price: int
    square_footage: typing.NotRequired[int]
    condition: str

TypedDict 也可以用来定义泛型类型:

T = typing.TypeVar('T')

`class` Node(typing.TypedDict, typing.Generic[T]):
    label: T
    neighbors: list[T]

n = Node(label='Acme', neighbors=['anvil', 'magnet', 'bird seed'])

不要使用传统的 TypedDict(name, **fields) 格式

为了支持向较旧版本的 Python 进行回溯,TypedDict 的初始版本也允许您使用类似于 namedtuple 的语法,例如:

HouseListing = TypedDict('HouseListing',
                         address=str, 
                         list_price=int, 
                         square_footage=int, 
                         condition=str)

或:

HouseListing = TypedDict('HouseListing',
                         {'address': str, 
                          'list_price': int, 
                          'square_footage': int,
                          'condition': str})

这些形式在 Python 3.11 中已被弃用,并计划在 Python 3.13 中移除。

请注意,TypedDict 实际上不定义新类型。通过从 TypedDict 继承创建的类实际上充当字典工厂,从而创建的实例 字典。通过重新使用定义 Node 类的先前代码片段,我们可以看到这一点,使用 type 内置函数:

n = Node(label='Acme', neighbors=['anvil', 'magnet', 'bird seed'])
print(type(n))           *`# prints: <class 'dict'>`*
print(type(n) is dict)   *`# prints: True`*

使用 TypedDict 时没有特殊的运行时转换或初始化;TypedDict 的好处来自静态类型检查和自我文档化,这些自然地通过使用类型注解积累。

TypeAlias

3.10+ 定义简单类型别名可能会被误解为将类分配给变量。例如,在这里我们为数据库中的记录标识符定义了一个类型:

Identifier = int

为了澄清这个声明是为了定义用于类型检查的自定义类型名称,请使用 TypeAlias:

Identifier: TypeAlias = int

TypeAlias 在定义尚未定义的类型并以字符串值引用时非常有用:

*`# Python will treat this like a standard str assignment`*
TBDType = 'ClassNotDefinedYet'

*`# indicates that this is actually a forward reference to a class`*
TBDType: TypeAlias = 'ClassNotDefinedYet'

TypeAlias 类型只能在模块范围内定义。使用 TypeAlias 定义的自定义类型与目标类型可互换。与后续章节中涵盖的 NewType 相对比(NewType 不创建新类型,仅为现有类型提供新名称),TypeAlias 仅为现有类型提供新名称。

NewType

NewType 允许您定义特定于应用程序的子类型,以避免使用相同类型为不同变量可能导致的混淆。例如,如果您的程序使用 str 值来表示不同类型的数据,很容易意外地交换值。假设您有一个模拟员工和部门的程序。以下类型声明不够描述清楚——哪一个是关键,哪一个是值?

employee_department_map: dict[str, str] = {}

为员工和部门 ID 定义类型使得声明更清晰:

EmpId = typing.NewType('EmpId', str)
DeptId = typing.NewType('DeptId', str)
employee_department_map: dict[EmpId, DeptId] = {}

这些类型定义也将允许类型检查器标记此不正确的使用:

`def` transfer_employee(empid: EmpId, to_dept: DeptId):
 *`# update department for employee`
*     employee_department_map[to_dept] = empid

运行 mypy 时会报告这些错误,如下所示:employee_department_map[to_dept] = empid。

error: Invalid index type "DeptId" for "Dict[EmpId, DeptId]"; expected
type "EmpId"
error: Incompatible types in assignment (expression has type "EmpId",
target has type "DeptId")

使用 NewType 通常需要您也使用 typing.cast;例如,要创建一个 EmpId,您需要将一个 str 强制转换为 EmpId 类型。

您还可以使用 NewType 指示应用程序特定类型的所需实现类型。例如,基本的美国邮政编码是五位数字。通常会看到这种实现使用 int,这在具有前导 0 的邮政编码时会出现问题。为了指示邮政编码应使用 str 实现,您的代码可以定义此类型检查类型:

ZipCode = typing.NewType("ZipCode", str)

使用 ZipCode 注释变量和函数参数将有助于标记错误的 int 用于邮政编码值的使用。

在运行时使用类型注解

函数和类变量的注释可以通过访问函数或类的 annotations 属性进行内省(尽管更好的做法是调用 inspect.get_annotations()):

>>> `def` f(a:list[str], b) -> int:
...     `pass`
...
>>> f.__annotations__
{'a': list[str], 'return': <class 'int'>}
>>> `class` Customer:
...     name: str
...     reward_points: int = 0
...
>>> Customer.__annotations__
{'name': <class 'str'>, 'reward_points': <class 'int'>}

此功能被 pydantic 和 FastAPI 等第三方包使用,以提供额外的代码生成和验证功能。

3.8+ 要定义自己的自定义协议类,以支持运行时检查的子类和 isinstance,请将该类定义为 typing.Protocol 的子类,并对所需的协议方法进行空方法定义,并使用@runtime_checkable(在表 5-6 中介绍)。如果使用@runtime_checkable 装饰它,您仍然定义了一个非常适用于静态类型检查的协议,但它不会使用 issubclass 和 isinstance 进行运行时检查。

例如,我们可以定义一个协议,指示一个类实现了更新和撤销方法,如下所示(Python 中的省略号...是指示空方法定义的便捷语法):

T = typing.TypeVar('T')

@typing.runtime_checkable
`class` SupportsUpdateUndo(typing.Protocol):
    `def` update(self, *args: T) -> `None`:
        ...
    `def` undo(self) -> `None`:
        ...

在不对 Accumulator 的继承路径进行任何更改(在“泛型和 TypeVars”中定义)的情况下,它现在满足了对 SupportsUpdateUndo 的运行时类型检查:

>>> issubclass(Accumulator, SupportsUpdateUndo)
True
>>> isinstance(acc, SupportsUpdateUndo)
True

另外,现在任何其他实现了更新和撤销方法的类都将被视为SupportsUpdateUndo的“子类”。

如何为您的代码添加类型注解

看到了使用类型注解提供的一些特性和功能,您可能想知道最佳的入门方式。本节描述了添加类型注解的几种情景和方法。

向新代码添加类型注解

当您开始编写一个简短的 Python 脚本时,添加类型注解可能会显得多余。作为“两个披萨规则”的一个衍生,我们建议使用“两个函数规则”:一旦您的脚本包含两个函数或方法,就回头添加方法签名的类型注解,以及必要时添加任何共享变量或类型。使用 TypedDict 来注释任何在类的位置使用的 dict 结构,以便在一开始就清晰地定义 dict 键或在进行过程中进行文档化;使用 NamedTuples(或数据类:本书的一些作者强烈倾向于后者)来定义所需的特定属性,以用于这些数据“捆”。

如果您开始一个具有许多模块和类的重大项目,那么您一定应该从一开始就使用类型注解。它们可以让您更加高效,因为它们有助于避免常见的命名和类型错误,并确保您在 IDE 中获得更全面的支持自动完成。在具有多个开发人员的项目中,这一点尤为重要:在代码中记录类型有助于告诉团队中的每个人对类型和值的期望。将这些类型捕获在代码中使它们在开发过程中立即可访问和可见,比单独的文档或规范要更加方便。

如果你正在开发一个要在多个项目中共享的库,那么最好从一开始就使用类型注解,很可能与你 API 设计中的函数签名并行。在库中添加类型注解将会为客户开发者简化生活,因为所有现代 IDE 都包含类型注解插件来支持静态类型检查、函数自动完成和文档编写。它们在编写单元测试时也会帮助你,因为你将受益于相同的丰富 IDE 支持。

对于任何这些项目,将类型检查实用程序添加到你的预提交挂钩中,这样你可以及时解决任何可能潜入你新代码库中的类型违规。这样一来,你可以在出现问题时修复它们,而不是等到做大的提交后才发现在多个地方都出现了基本的类型错误。

给现有代码添加类型注解(渐进式类型)

有几家公司已经运行了将类型注解应用于大型现有代码库的项目,推荐采用渐进式的方法,称为渐进式类型。通过渐进式类型,你可以逐步地按步骤处理你的代码库,逐步添加和验证类型注解到几个类或模块。

有些工具,比如 mypy,会让你逐个函数地添加类型注解。默认情况下,mypy 会跳过没有类型签名的函数,因此你可以逐步地逐个函数地处理你的代码库。这种增量的过程允许你将精力集中在代码的各个部分,而不是一次性地在所有地方添加类型注解,然后试图解决一堆类型检查器错误。

推荐的一些方法包括:

  • 确定你使用最频繁的模块,并逐步添加类型,逐个方法地进行。(这些可能是核心应用程序类模块,或广泛共享的实用程序模块。)

  • 逐个方法地添加注解,以便逐步引发并解决类型检查问题。

  • 使用 pytype 或 pyre 推断生成初始的 .pyi 桩文件(在下一节中讨论)。然后,逐步从 .pyi 文件中迁移类型,可以手动进行,也可以使用像 pytype 的 merge_pyi 工具这样的自动化工具。

  • 开始使用类型检查器的宽松默认模式,这样大部分代码会被跳过,你可以将注意力集中在特定的文件上。随着工作的进行,逐渐转向更严格的模式,以突出剩余的项目,并且已经注释的文件不会因为接受新的非注释代码而退步。

使用 .pyi 桩文件

有时候你可能无法访问 Python 的类型注解。例如,你可能正在使用一个没有类型注解的库,或者使用一个其函数是用 C 实现的模块。

在这些情况下,可以使用单独的*.pyi*存根文件,其中只包含相关的类型注解。本章开头提到的多个类型检查器可以生成这些存根文件。您可以从typeshed 存储库下载流行的 Python 库以及 Python 标准库本身的存根文件。您可以从 Python 源文件中维护存根文件,或者使用某些类型检查器中可用的合并工具将其集成回原始 Python 源代码中。

摘要

Python 作为一个强大的语言和编程生态系统已经稳步崛起,支持重要的企业应用程序。曾经作为脚本和任务自动化的实用语言,现在已经成为影响数百万用户的重要和复杂应用程序平台,用于关键任务甚至是地外系统。⁵ 添加类型注解是开发和维护这些系统的重要一步。

Python 的类型注解在线文档提供了最新的描述、示例和最佳实践,因为类型注解的语法和实践不断演变。作者还特别推荐了流畅的 Python第二版,作者是 Luciano Ramalho(O'Reilly),尤其是第八章和第十五章,这些章节专门讲解了 Python 类型注解。

¹ 强大而广泛的单元测试也将防范许多商业逻辑问题,这是任何类型检查都无法捕捉的—所以,类型提示不应该代替单元测试,而应该单元测试一起使用。

² 类型注解的语法在 Python 3.0 中引入,但其语义则是后来才明确指定的。

³ 这种方法也兼容 Python 2.7 代码,当时广泛使用。

⁴ 并且 SupportsInt 使用了 runtime_checkable 装饰器。

⁵ NASA 的喷气推进实验室使用 Python 开发了坚韧号火星车和毅力号火星直升机;负责发现引力波的团队既用 Python 协调仪器,也用 Python 分析了得到的大量数据。