9. 类 — Python 文档

来自菜鸟教程
Python/docs/3.8/tutorial/classes
跳转至:导航、​搜索

9. 班级

类提供了一种将数据和功能捆绑在一起的方法。 创建一个新类会创建一个新的 类型 对象,从而允许创建该类型的新 实例 。 每个类实例都可以附加属性以维护其状态。 类实例也可以有方法(由它的类定义)来修改它的状态。

与其他编程语言相比,Python 的类机制以最少的新语法和语义添加了类。 它混合了 C++ 和 Modula-3 中的类机制。 Python 类提供了面向对象编程的所有标准特性:类继承机制允许多个基类,派生类可以覆盖其基类的任何方法,一个方法可以调用同名基类的方法. 对象可以包含任意数量和种类的数据。 与模块一样,类也具有 Python 的动态特性:它们是在运行时创建的,并且可以在创建后进一步修改。

在 C++ 术语中,通常类成员(包括数据成员)是 public(除了见下文 Private Variables),所有成员函数都是 virtual。 与 Modula-3 一样,从对象的方法中引用对象的成员没有简写:方法函数是用表示对象的显式第一个参数声明的,该参数由调用隐式提供。 和 Smalltalk 一样,类本身也是对象。 这为导入和重命名提供了语义。 与 C++ 和 Modula-3 不同,内置类型可以用作用户扩展的基类。 此外,就像在 C++ 中一样,大多数具有特殊语法的内置运算符(算术运算符、下标等)都可以为类实例重新定义。

(由于缺乏普遍接受的术语来谈论类,我将偶尔使用 Smalltalk 和 C++ 术语。 我会使用 Modula-3 术语,因为它的面向对象语义比 C++ 更接近 Python 的语义,但我希望很少有读者听说过它。)

9.1. 关于名称和对象的一句话

对象具有个性,多个名称(在多个范围内)可以绑定到同一个对象。 这在其他语言中称为别名。 乍一看 Python 时通常不会意识到这一点,并且在处理不可变的基本类型(数字、字符串、元组)时可以安全地忽略它。 然而,别名可能对涉及可变对象(如列表、字典和大多数其他类型)的 Python 代码的语义产生令人惊讶的影响。 这通常用于程序的好处,因为别名在某些方面表现得像指针。 例如,传递一个对象很便宜,因为实现只传递一个指针; 如果一个函数修改了一个作为参数传递的对象,调用者会看到变化——这消除了对 Pascal 中两种不同的参数传递机制的需要。


9.2. Python 范围和命名空间

在介绍类之前,我首先要告诉你一些 Python 的作用域规则。 类定义对命名空间起到了一些巧妙的作用,您需要了解作用域和命名空间的工作原理才能完全理解正在发生的事情。 顺便说一下,有关此主题的知识对任何高级 Python 程序员都很有用。

让我们从一些定义开始。

命名空间 是从名称到对象的映射。 大多数命名空间目前都实现为 Python 字典,但这通常不会以任何方式引起注意(性能除外),并且将来可能会发生变化。 命名空间的例子有:内置名称集(包含诸如 abs() 和内置异常名称的函数); 模块中的全局名称; 以及函数调用中的本地名称。 从某种意义上说,对象的属性集也形成了一个命名空间。 了解命名空间的重要一点是,不同命名空间中的名称之间绝对没有关系; 例如,两个不同的模块都可以毫无混淆地定义一个函数 maximize——模块的用户必须在它前面加上模块名称。

顺便说一下,我使用单词 attribute 来表示点后的任何名称——例如,在表达式 z.real 中,real 是对象 z。 严格来说,对模块中名称的引用是属性引用:在表达式 modname.funcname 中,modname 是一个模块对象,而 funcname 是它的一个属性。 在这种情况下,模块的属性和模块中定义的全局名称之间恰好有一个直接的映射:它们共享相同的命名空间! 1

属性可以是只读的或可写的。 在后一种情况下,可以分配给属性。 模块属性可写:可以写modname.the_answer = 42。 也可以使用 del 语句删除可写属性。 例如,del modname.the_answer 将从 modname 命名的对象中删除属性 the_answer

命名空间是在不同时刻创建的,具有不同的生命周期。 包含内置名称的命名空间是在 Python 解释器启动时创建的,并且永远不会被删除。 模块的全局命名空间是在读入模块定义时创建的; 通常,模块命名空间也会持续到解释器退出。 由解释器的顶级调用执行的语句,无论是从脚本文件中读取还是以交互方式读取,都被视为名为 __main__ 的模块的一部分,因此它们具有自己的全局命名空间。 (内置名称实际上也存在于模块中;这称为 builtins。)

函数的本地命名空间在调用函数时创建,并在函数返回或引发函数内未处理的异常时删除。 (实际上,忘记是描述实际发生的更好的方法。)当然,每个递归调用都有自己的本地命名空间。

scope 是 Python 程序的文本区域,其中命名空间可直接访问。 这里的“可直接访问”意味着对名称的非限定引用尝试在名称空间中查找名称。

尽管范围是静态确定的,但它们是动态使用的。 在执行过程中的任何时候,都有 3 或 4 个嵌套的作用域,它们的命名空间是可直接访问的:

  • 首先搜索的最里面的范围包含本地名称
  • 从最近的封闭范围开始搜索的任何封闭函数的范围包含非本地名称,但也包含非全局名称
  • 倒数第二个范围包含当前模块的全局名称
  • 最外面的范围(最后搜索)是包含内置名称的命名空间

如果一个名称被声明为全局的,那么所有的引用和赋值都会直接进入包含模块全局名称的中间作用域。 要重新绑定在最内部范围之外发现的变量,可以使用 nonlocal 语句; 如果未声明为非局部变量,则这些变量是只读的(尝试写入这样的变量只会在最内层范围内创建一个 new 局部变量,而保持同名的外部变量不变)。

通常,局部作用域引用(文本)当前函数的局部名称。 在函数外部,局部作用域引用与全局作用域相同的命名空间:模块的命名空间。 类定义在本地作用域中放置了另一个命名空间。

重要的是要认识到作用域是通过文本确定的:模块中定义的函数的全局作用域是该模块的命名空间,无论从何处调用函数或通过什么别名调用该函数。 另一方面,实际的名称搜索是在运行时动态完成的——然而,语言定义正在向静态名称解析发展,在“编译”时,所以不要依赖动态名称解析! (实际上,局部变量已经是静态确定的。)

Python 的一个特殊之处在于——如果没有 globalnonlocal 语句有效——对名称的赋值总是进入最内部的范围。 赋值不会复制数据——它们只是将名称绑定到对象。 删除也是如此:语句 del x 从本地作用域引用的命名空间中删除 x 的绑定。 实际上,所有引入新名称的操作都使用局部作用域:特别是 import 语句和函数定义将模块或函数名称绑定在局部作用域中。

global 语句可用于指示特定变量存在于全局范围内并应在那里重新绑定; nonlocal 语句表明特定变量存在于封闭范围内,应该在那里重新绑定。

9.2.1. 范围和命名空间示例

这是一个示例,演示如何引用不同的作用域和命名空间,以及 globalnonlocal 如何影响变量绑定:

def scope_test():
    def do_local():
        spam = "local spam"

    def do_nonlocal():
        nonlocal spam
        spam = "nonlocal spam"

    def do_global():
        global spam
        spam = "global spam"

    spam = "test spam"
    do_local()
    print("After local assignment:", spam)
    do_nonlocal()
    print("After nonlocal assignment:", spam)
    do_global()
    print("After global assignment:", spam)

scope_test()
print("In global scope:", spam)

示例代码的输出是:

After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam

请注意 local 分配(默认)如何没有改变 spamscope_test 的绑定。 nonlocal赋值改变了scope_testspam的绑定,global赋值改变了模块级绑定。

您还可以看到,在 global 分配之前,之前没有绑定 spam


9.3. 类的第一眼

类引入了一些新语法、三个新对象类型和一些新语义。

9.3.1. 类定义语法

类定义的最简单形式如下所示:

class ClassName:
    <statement-1>
    .
    .
    .
    <statement-N>

类定义,如函数定义(def 语句)必须在它们生效之前执行。 (可以想象,您可以将类定义放在 if 语句的分支中,或放在函数内。)

在实践中,类定义中的语句通常是函数定义,但允许使用其他语句,有时也很有用——我们稍后会回到这一点。 类中的函数定义通常具有特殊形式的参数列表,由方法的调用约定决定——同样,稍后解释。

当输入一个类定义时,会创建一个新的命名空间,并将其用作局部作用域——因此,所有对局部变量的赋值都进入这个新的命名空间。 特别是,函数定义在这里绑定了新函数的名称。

当类定义正常保留时(通过末尾),会创建一个 类对象 。 这基本上是对由类定义创建的命名空间内容的包装; 我们将在下一节中了解有关类对象的更多信息。 原始局部作用域(在输入类定义之前生效的作用域)被恢复,类对象在此处绑定到类定义头中给出的类名(在示例中为 ClassName)。


9.3.2. 类对象

类对象支持两种操作:属性引用和实例化。

属性引用使用Python中所有属性引用的标准语法:obj.name。 有效的属性名称是创建类对象时在类的命名空间中的所有名称。 因此,如果类定义如下所示:

class MyClass:
    """A simple example class"""
    i = 12345

    def f(self):
        return 'hello world'

那么 MyClass.iMyClass.f 是有效的属性引用,分别返回一个整数和一个函数对象。 类属性也可以赋值给,所以你可以通过赋值来改变MyClass.i的值。 __doc__ 也是一个有效的属性,返回属于类的文档字符串:"A simple example class"

实例化 使用函数表示法。 假设类对象是一个无参数函数,它返回类的一个新实例。 例如(假设上面的类):

x = MyClass()

创建类的新 实例 并将此对象分配给局部变量 x

实例化操作(“调用”一个类对象)创建一个空对象。 许多类喜欢创建具有自定义为特定初始状态的实例的对象。 因此,一个类可以定义一个名为 __init__() 的特殊方法,如下所示:

def __init__(self):
    self.data = []

当一个类定义了一个 __init__() 方法时,类实例化会自动为新创建的类实例调用 __init__()。 所以在这个例子中,一个新的初始化实例可以通过以下方式获得:

x = MyClass()

当然,__init__() 方法可能具有更大灵活性的参数。 在这种情况下,提供给类实例化运算符的参数将传递给 __init__()。 例如,

>>> class Complex:
...     def __init__(self, realpart, imagpart):
...         self.r = realpart
...         self.i = imagpart
...
>>> x = Complex(3.0, -4.5)
>>> x.r, x.i
(3.0, -4.5)

9.3.3. 实例对象

现在我们可以用实例对象做什么? 实例对象理解的唯一操作是属性引用。 有两种有效的属性名称:数据属性和方法。

数据属性对应于Smalltalk中的“实例变量”,以及C++中的“数据成员”。 数据属性不需要声明; 就像局部变量一样,它们在第一次被赋值时就会出现。 例如,如果 x 是上面创建的 MyClass 的实例,下面的代码将打印值 16,不留痕迹:

x.counter = 1
while x.counter < 10:
    x.counter = x.counter * 2
print(x.counter)
del x.counter

另一种实例属性引用是 方法 。 方法是“属于”对象的函数。 (在 Python 中,术语方法不是类实例独有的:其他对象类型也可以有方法。 例如,列表对象具有称为 append、insert、remove、sort 等的方法。 但是,在下面的讨论中,除非另有明确说明,否则我们将专门使用术语方法来表示类实例对象的方法。)

实例对象的有效方法名称取决于其类。 根据定义,作为函数对象的类的所有属性都定义了其实例的相应方法。 所以在我们的例子中,x.f 是一个有效的方法引用,因为 MyClass.f 是一个函数,但 x.i 不是,因为 MyClass.i 不是。 但是 x.fMyClass.f 不是一回事——它是一个 方法对象 ,而不是一个函数对象。


9.3.4. 方法对象

通常,方法在绑定后立即调用:

x.f()

MyClass 示例中,这将返回字符串 'hello world'。 但是,没有必要立即调用方法:x.f 是一个方法对象,可以存储起来稍后调用。 例如:

xf = x.f
while True:
    print(xf())

将继续打印 hello world 直到时间结束。

当一个方法被调用时到底发生了什么? 您可能已经注意到 x.f() 在上面没有参数的情况下被调用,即使 f() 的函数定义指定了一个参数。 争论怎么了? 当一个需要参数的函数在没有任何参数的情况下被调用时,Python 肯定会引发一个异常——即使该参数实际上没有被使用...…

实际上,您可能已经猜到了答案:方法的特殊之处在于实例对象作为函数的第一个参数传递。 在我们的示例中,调用 x.f() 完全等同于 MyClass.f(x)。 通常,调用带有 n 参数列表的方法等效于使用参数列表调用相应的函数,该参数列表是通过在第一个参数之前插入方法的实例对象而创建的。

如果您仍然不了解方法的工作原理,那么查看实现也许可以澄清问题。 当引用实例的非数据属性时,将搜索实例的类。 如果名称表示作为函数对象的有效类属性,则通过打包(指向)实例对象和刚刚在抽象对象中一起找到的函数对象来创建方法对象:这就是方法对象。 当使用参数列表调用方法对象时,会根据实例对象和参数列表构造一个新的参数列表,并使用这个新的参数列表调用函数对象。


9.3.5. 类和实例变量

一般来说,实例变量用于每个实例唯一的数据,类变量用于类的所有实例共享的属性和方法:

class Dog:

    kind = 'canine'         # class variable shared by all instances

    def __init__(self, name):
        self.name = name    # instance variable unique to each instance

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind                  # shared by all dogs
'canine'
>>> e.kind                  # shared by all dogs
'canine'
>>> d.name                  # unique to d
'Fido'
>>> e.name                  # unique to e
'Buddy'

正如 关于名称和对象的词 中所讨论的那样,共享数据在涉及 可变 对象(例如列表和字典)时可能会产生令人惊讶的效果。 例如,以下代码中的 tricks 列表不应用作类变量,因为所有 Dog 实例只会共享一个列表:

class Dog:

    tricks = []             # mistaken use of a class variable

    def __init__(self, name):
        self.name = name

    def add_trick(self, trick):
        self.tricks.append(trick)

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks                # unexpectedly shared by all dogs
['roll over', 'play dead']

正确的类设计应该使用实例变量:

class Dog:

    def __init__(self, name):
        self.name = name
        self.tricks = []    # creates a new empty list for each dog

    def add_trick(self, trick):
        self.tricks.append(trick)

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks
['roll over']
>>> e.tricks
['play dead']

9.4. 随机备注

如果在实例和类中都出现相同的属性名称,则属性查找会优先考虑实例:

>>> class Warehouse:
        purpose = 'storage'
        region = 'west'

>>> w1 = Warehouse()
>>> print(w1.purpose, w1.region)
storage west
>>> w2 = Warehouse()
>>> w2.region = 'east'
>>> print(w2.purpose, w2.region)
storage east

数据属性可以被方法引用,也可以被对象的普通用户(“客户端”)引用。 换句话说,类不能用于实现纯抽象数据类型。 事实上,Python 中没有任何东西可以强制执行数据隐藏——这一切都基于约定。 (另一方面,用 C 编写的 Python 实现可以完全隐藏实现细节并在必要时控制对对象的访问;这可以被用 C 编写的 Python 扩展使用。)

客户端应该小心地使用数据属性——客户端可能会通过在它们的数据属性上打戳来弄乱由方法维护的不变量。 请注意,客户端可以将自己的数据属性添加到实例对象中,而不会影响方法的有效性,只要避免名称冲突——同样,命名约定可以在这里省去很多麻烦。

从方法中引用数据属性(或其他方法!)没有简写。 我发现这实际上增加了方法的可读性:在浏览方法时不会混淆局部变量和实例变量。

通常,方法的第一个参数称为 self。 这只不过是一个约定:名称 self 对 Python 绝对没有特殊意义。 但是请注意,如果不遵循约定,您的代码对其他 Python 程序员的可读性可能会降低,并且还可以想象,可能会编写依赖于此类约定的 类浏览器 程序。

任何作为类属性的函数对象都为该类的实例定义了一个方法。 函数定义不必以文本形式包含在类定义中:将函数对象分配给类中的局部变量也是可以的。 例如:

# Function defined outside the class
def f1(self, x, y):
    return min(x, x+y)

class C:
    f = f1

    def g(self):
        return 'hello world'

    h = g

现在 fgh 都是类 C 的属性,引用函数对象,因此它们都是 Ch 完全等同于 g。 请注意,这种做法通常只会混淆程序的读者。

方法可以使用 self 参数的方法属性调用其他方法:

class Bag:
    def __init__(self):
        self.data = []

    def add(self, x):
        self.data.append(x)

    def addtwice(self, x):
        self.add(x)
        self.add(x)

方法可以以与普通函数相同的方式引用全局名称。 与方法关联的全局范围是包含其定义的模块。 (类从不用作全局作用域。)虽然很少遇到在方法中使用全局数据的充分理由,但全局作用域有许多合法用途:一方面,导入全局作用域的函数和模块可以被方法以及其中定义的函数和类使用。 通常,包含该方法的类本身是在这个全局范围内定义的,在下一节中,我们将找到一个方法想要引用它自己的类的一些很好的理由。

每个值都是一个对象,因此有一个 (也称为 类型 )。 它存储为 object.__class__


9.5. 遗产

当然,如果不支持继承,语言特性就配不上“类”这个名字。 派生类定义的语法如下所示:

class DerivedClassName(BaseClassName):
    <statement-1>
    .
    .
    .
    <statement-N>

名称 BaseClassName 必须在包含派生类定义的范围内定义。 代替基类名称,还允许使用其他任意表达式。 例如,当基类在另一个模块中定义时,这可能很有用:

class DerivedClassName(modname.BaseClassName):

派生类定义的执行过程与基类相同。 构造类对象时,会记住基类。 这用于解析属性引用:如果在类中找不到请求的属性,则搜索继续在基类中查找。 如果基类本身是从某个其他类派生的,则将递归应用此规则。

派生类的实例化没有什么特别之处:DerivedClassName() 创建类的一个新实例。 方法引用的解析如下:搜索相应的类属性,必要时沿基类链向下搜索,如果生成函数对象,则方法引用有效。

派生类可以覆盖其基类的方法。 由于方法在调用同一对象的其他方法时没有特殊权限,因此调用同一基类中定义的另一个方法的基类方法可能最终会调用覆盖它的派生类的方法。 (对于 C++ 程序员:Python 中的所有方法都是有效的 virtual。)

派生类中的覆盖方法实际上可能想要扩展而不是简单地替换同名的基类方法。 直接调用基类方法有一个简单的方法:调用BaseClassName.methodname(self, arguments)即可。 这有时也对客户有用。 (请注意,这仅适用于基类在全局范围内可作为 BaseClassName 访问的情况。)

Python 有两个用于继承的内置函数:

  • 使用 isinstance() 检查实例的类型:isinstance(obj, int) 将是 True 仅当 obj.__class__int 或某些派生类来自 int
  • 使用 issubclass() 检查类继承: issubclass(bool, int)True 因为 boolint 的子类。 然而,issubclass(float, int)False,因为 float 不是 int 的子类。

9.5.1. 多重继承

Python 也支持一种多重继承形式。 具有多个基类的类定义如下所示:

class DerivedClassName(Base1, Base2, Base3):
    <statement-1>
    .
    .
    .
    <statement-N>

对于大多数目的,在最简单的情况下,您可以将搜索从父类继承的属性视为深度优先,从左到右,而不是在层次结构重叠的同一类中搜索两次。 因此,如果在 DerivedClassName 中找不到属性,则在 Base1 中搜索,然后(递归地)在 Base1 的基类中搜索,如果没有找到在那里,它在 Base2 中被搜索,依此类推。

事实上,它比这稍微复杂一些; 方法解析顺序动态更改以支持对 super() 的协作调用。 这种方法在其他一些多继承语言中称为 call-next-method,并且比单继承语言中的超级调用更强大。

动态排序是必要的,因为多重继承的所有情况都表现出一种或多种菱形关系(其中至少一个父类可以通过来自最底层类的多条路径访问)。 例如,所有类都继承自 object,因此任何多重继承的情况都提供了多个到达 object 的路径。 为了防止基类被多次访问,动态算法以一种保留每个类中指定的从左到右顺序的方式线性化搜索顺序,只调用每个父类一次,并且是单调的(意味着一个类可以被子类化而不影响其父类的优先顺序)。 综上所述,这些特性使得设计具有多重继承的可靠且可扩展的类成为可能。 更多详细信息,请参见 https://www.python.org/download/releases/2.3/mro/。


9.6. 私有变量

Python 中不存在只能从对象内部访问的“私有”实例变量。 但是,大多数 Python 代码都遵循一个约定:以下划线作为前缀的名称(例如 _spam) 应被视为 API 的非公开部分(无论是函数、方法还是数据成员)。 它应被视为实施细节,如有更改,恕不另行通知。

由于类私有成员有一个有效的用例(即避免名称与子类定义的名称的名称冲突),因此对这种机制的支持有限,称为 name mangling__spam 形式的任何标识符(至少两个前导下划线,最多一个尾随下划线)在文本上替换为 _classname__spam,其中 classname 是当前类名,前导下划线(s) 剥离。 只要它出现在类的定义中,就可以不考虑标识符的句法位置来完成这种修改。

名称修改有助于让子类覆盖方法而不破坏类内方法调用。 例如:

class Mapping:
    def __init__(self, iterable):
        self.items_list = []
        self.__update(iterable)

    def update(self, iterable):
        for item in iterable:
            self.items_list.append(item)

    __update = update   # private copy of original update() method

class MappingSubclass(Mapping):

    def update(self, keys, values):
        # provides new signature for update()
        # but does not break __init__()
        for item in zip(keys, values):
            self.items_list.append(item)

即使 MappingSubclass 引入 __update 标识符,上面的例子也能工作,因为它被替换为 Mapping 类中的 _Mapping__update_MappingSubclass__update ] 分别在 MappingSubclass 类中。

请注意,重整规则主要是为了避免事故; 仍然可以访问或修改被视为私有的变量。 这甚至在特殊情况下也很有用,例如在调试器中。

请注意,传递给 exec()eval() 的代码不会将调用类的类名视为当前类; 这类似于 global 语句的效果,其效果同样仅限于字节编译在一起的代码。 同样的限制适用于 getattr()setattr()delattr(),以及直接引用 __dict__ 时。


9.7. 什物

有时,使用类似于 Pascal“记录”或 C“结构”的数据类型将一些命名数据项捆绑在一起很有用。 一个空的类定义会很好:

class Employee:
    pass

john = Employee()  # Create an empty employee record

# Fill the fields of the record
john.name = 'John Doe'
john.dept = 'computer lab'
john.salary = 1000

一段需要特定抽象数据类型的 Python 代码通常可以传递一个类来模拟该数据类型的方法。 例如,如果您有一个函数来格式化文件对象中的一些数据,您可以使用 read()readline() 方法定义一个类,这些方法从字符串缓冲区中获取数据,并将其传递作为论据。

实例方法对象也有属性:m.__self__是方法m()的实例对象,m.__func__是方法对应的函数对象。


9.8. 迭代器

到现在为止,您可能已经注意到大多数容器对象都可以使用 for 语句进行循环:

for element in [1, 2, 3]:
    print(element)
for element in (1, 2, 3):
    print(element)
for key in {'one':1, 'two':2}:
    print(key)
for char in "123":
    print(char)
for line in open("myfile.txt"):
    print(line, end='')

这种访问方式清晰、简洁且方便。 迭代器的使用遍及并统一了 Python。 在幕后,for 语句在容器对象上调用 iter()。 该函数返回一个定义方法 __next__() 的迭代器对象,该方法一次访问一个容器中的元素。 当没有更多元素时, __next__() 引发 StopIteration 异常,告知 for 循环终止。 您可以使用 next() 内置函数调用 __next__() 方法; 这个例子展示了它是如何工作的:

>>> s = 'abc'
>>> it = iter(s)
>>> it
<iterator object at 0x00A1DB50>
>>> next(it)
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    next(it)
StopIteration

了解了迭代器协议背后的机制后,很容易将迭代器行为添加到您的类中。 定义一个 __iter__() 方法,该方法返回一个具有 __next__() 方法的对象。 如果类定义了 __next__(),那么 __iter__() 可以只返回 self

class Reverse:
    """Iterator for looping over a sequence backwards."""
    def __init__(self, data):
        self.data = data
        self.index = len(data)

    def __iter__(self):
        return self

    def __next__(self):
        if self.index == 0:
            raise StopIteration
        self.index = self.index - 1
        return self.data[self.index]
>>> rev = Reverse('spam')
>>> iter(rev)
<__main__.Reverse object at 0x00A1DB50>
>>> for char in rev:
...     print(char)
...
m
a
p
s

9.9. 发电机

Generators 是一个简单而强大的创建迭代器的工具。 它们的编写方式与常规函数类似,但在需要返回数据时使用 yield 语句。 每次在其上调用 next() 时,生成器都会从停止的地方恢复(它记住所有数据值以及最后执行的语句)。 一个例子表明生成器可以非常容易地创建:

def reverse(data):
    for index in range(len(data)-1, -1, -1):
        yield data[index]
>>> for char in reverse('golf'):
...     print(char)
...
f
l
o
g

任何可以用生成器完成的事情也可以用基于类的迭代器来完成,如上一节所述。 生成器如此紧凑的原因是 __iter__()__next__() 方法是自动创建的。

另一个关键特性是局部变量和执行状态在调用之间自动保存。 与使用 self.indexself.data 等实例变量的方法相比,这使得该函数更易于编写且更清晰。

除了自动方法创建和保存程序状态之外,当生成器终止时,它们会自动引发 StopIteration。 结合起来,这些功能可以轻松创建迭代器,而无需比编写常规函数更多的努力。


9.10. 生成器表达式

一些简单的生成器可以使用类似于列表推导式的语法简洁地编码为表达式,但使用括号而不是方括号。 这些表达式是为生成器立即被封闭函数使用的情况而设计的。 生成器表达式比完整的生成器定义更紧凑但通用性较差,并且比等效的列表推导式更易于内存使用。

例子:

>>> sum(i*i for i in range(10))                 # sum of squares
285

>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x,y in zip(xvec, yvec))         # dot product
260

>>> unique_words = set(word for line in page  for word in line.split())

>>> valedictorian = max((student.gpa, student.name) for student in graduates)

>>> data = 'golf'
>>> list(data[i] for i in range(len(data)-1, -1, -1))
['f', 'l', 'o', 'g']

脚注

1
除了一件事。 模块对象有一个名为 __dict__ 的秘密只读属性,它返回用于实现模块命名空间的字典; 名称 __dict__ 是一个属性,但不是全局名称。 显然,使用它违反了命名空间实现的抽象,应该仅限于事后调试器之类的东西。