9. 类¶
类提供了一种将数据和功能捆绑在一起的方法。创建新类会创建一个新的对象类型,从而允许创建该类型的新实例。每个类实例都可以附加属性来维护其状态。类实例还可以具有用于修改其状态的方法(由其类定义)。
与其他编程语言相比,Python 的类机制以最少的语法和语义添加了类。它是 C++ 和 Modula-3 中类机制的混合体。Python 类提供了面向对象编程的所有标准特性:类继承机制允许多个基类,派生类可以覆盖其基类或类的任何方法,并且方法可以调用具有相同名称的基类的方法。对象可以包含任意数量和种类的数据。与模块一样,类也具有 Python 的动态特性:它们是在运行时创建的,并且可以在创建后进一步修改。
在 C++ 术语中,通常类成员(包括数据成员)是公共的(除了下面 私有变量 中描述的例外情况),并且所有成员函数都是虚的。与 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
而不会造成混淆——模块的用户必须以模块名称为前缀。
顺便说一句,我使用 属性 来表示点之后的任何名称——例如,在表达式 z.real
中,real
是对象 z
的属性。严格来说,对模块中的名称的引用是属性引用:在表达式 modname.funcname
中,modname
是一个模块对象,funcname
是它的一个属性。在这种情况下,模块的属性和模块中定义的全局名称之间恰好存在直接映射:它们共享相同的命名空间! [1]
属性可以是只读的或可写的。在后一种情况下,可以对属性进行赋值。模块属性是可写的:您可以写入 modname.the_answer = 42
。可写属性也可以使用 del
语句删除。例如,del modname.the_answer
将从 modname
命名的对象中删除属性 the_answer
。
命名空间在不同的时刻创建,并且具有不同的生命周期。包含内置名称的命名空间在 Python 解释器启动时创建,并且永远不会删除。模块的全局命名空间在读取模块定义时创建;通常,模块命名空间也会持续到解释器退出。由解释器的顶层调用执行的语句(从脚本文件读取或以交互方式读取)被认为是名为 __main__
的模块的一部分,因此它们有自己的全局命名空间。(内置名称实际上也存在于一个模块中;这称为 builtins
。)
函数的局部命名空间在函数被调用时创建,并在函数返回或引发未在函数内处理的异常时删除。(实际上,忘记是描述实际发生情况的更好方式。)当然,递归调用各自都有自己的局部命名空间。
作用域是 Python 程序的一个文本区域,在该区域中可以直接访问命名空间。“直接访问”在这里意味着对名称的非限定引用会尝试在命名空间中查找该名称。
尽管作用域是静态确定的,但它们是动态使用的。在执行期间的任何时间,都有 3 个或 4 个嵌套作用域,其命名空间可以直接访问
最内层作用域,首先搜索,包含局部名称
任何封闭函数的作用域,从最近的封闭作用域开始搜索,包含非局部但也是非全局的名称
倒数第二个作用域包含当前模块的全局名称
最外层作用域(最后搜索)是包含内置名称的命名空间
如果一个名称被声明为全局的,那么所有引用和赋值都直接转到包含模块全局名称的倒数第二个作用域。要重新绑定在最内层作用域之外找到的变量,可以使用 nonlocal
语句;如果未声明为 nonlocal,则这些变量是只读的(尝试写入此类变量只会最内层作用域中创建一个新的局部变量,而保持同名的外部变量不变)。
通常,局部作用域引用(文本上)当前函数的局部名称。在函数外部,局部作用域引用与全局作用域相同的命名空间:模块的命名空间。类定义将另一个命名空间置于局部作用域中。
重要的是要意识到作用域是文本上确定的:在模块中定义的函数的全局作用域是该模块的命名空间,无论从哪里或通过什么别名调用该函数。另一方面,对名称的实际搜索是在运行时动态完成的——但是,语言定义正在朝着在“编译”时进行静态名称解析的方向发展,因此不要依赖动态名称解析!(事实上,局部变量已经静态确定了。)
Python 的一个特殊之处在于,如果没有任何 global
或 nonlocal
语句生效,则对名称的赋值总是进入最内层的作用域。赋值不会复制数据,它们只是将名称绑定到对象。删除也是如此:语句 del x
从局部作用域引用的命名空间中移除 x
的绑定。事实上,所有引入新名称的操作都使用局部作用域:特别是 import
语句和函数定义会将模块或函数名称绑定到局部作用域中。
global
语句可以用来指示特定的变量存在于全局作用域中,并且应该在那里重新绑定;nonlocal
语句指示特定的变量存在于封闭作用域中,并且应该在那里重新绑定。
9.2.1. 作用域和命名空间示例¶
这是一个演示如何引用不同的作用域和命名空间,以及 global
和 nonlocal
如何影响变量绑定的示例
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
请注意,局部赋值(默认情况下)没有改变 scope_test 中 spam 的绑定。nonlocal
赋值改变了 scope_test 中 spam 的绑定,而 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.i
和 MyClass.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
另一种实例属性引用是方法。方法是“属于”对象的一个函数。
实例对象的有效方法名称取决于其类。根据定义,类的所有作为函数对象的属性都定义了其实例的相应方法。因此,在我们的示例中,x.f
是一个有效的方法引用,因为 MyClass.f
是一个函数,但 x.i
不是,因为 MyClass.i
不是。但是,x.f
与 MyClass.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
现在 f
、g
和 h
都是类 C
的属性,它们引用函数对象,因此它们都是 C
实例的方法——h
与 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)
只有当obj.__class__
是int
或从int
派生的某个类时,才为True
。使用
issubclass()
来检查类继承:issubclass(bool, int)
为True
,因为bool
是int
的子类。但是,issubclass(float, int)
为False
,因为float
不是int
的子类。
9.5.1. 多重继承¶
Python 也支持一种形式的多重继承。具有多个基类的类定义如下所示:
class DerivedClassName(Base1, Base2, Base3):
<statement-1>
.
.
.
<statement-N>
在大多数情况下,对于最简单的情况,您可以将从父类继承的属性搜索视为深度优先、从左到右,在层次结构中存在重叠的情况下,不会在同一个类中搜索两次。因此,如果在 DerivedClassName
中未找到某个属性,则会在 Base1
中搜索,然后在 Base1
的基类中(递归地)搜索,如果仍然没有找到,则会在 Base2
中搜索,依此类推。
实际上,情况比这稍微复杂一些;方法解析顺序会动态更改,以支持对 super()
的协作调用。这种方法在其他一些多重继承语言中被称为调用下一个方法,并且比单继承语言中的 super 调用更强大。
动态排序是必要的,因为所有多重继承的情况都表现出一个或多个菱形关系(其中至少一个父类可以通过从最底层类开始的多条路径访问)。例如,所有类都继承自 object
,因此任何多重继承的情况都会提供多条到达 object
的路径。为了防止基类被访问多次,动态算法会以一种保留每个类中指定的从左到右的顺序,并且每个父类只调用一次的单调方式来线性化搜索顺序(这意味着可以在不影响其父类的优先级顺序的情况下对类进行子类化)。综上所述,这些属性使得设计具有多重继承的可靠且可扩展的类成为可能。有关更多详细信息,请参阅 Python 2.3 方法解析顺序。
9.6. 私有变量¶
在 Python 中不存在“私有”实例变量,即除了对象内部之外无法访问的变量。但是,大多数 Python 代码都遵循一个约定:以一个下划线为前缀的名称(例如 _spam
)应被视为 API 的非公共部分(无论是函数、方法还是数据成员)。它应被视为实现细节,并可能在不另行通知的情况下进行更改。
由于类私有成员存在有效的用例(即避免子类定义的名称与名称冲突),因此对这种机制(称为名称修饰)提供了有限的支持。任何形式为 __spam
的标识符(至少两个前导下划线,最多一个尾部下划线)都将被文本替换为 _classname__spam
,其中 classname
是当前类名,并去除了前导下划线。这种修饰是在不考虑标识符语法位置的情况下进行的,只要它出现在类的定义中。
另请参阅
有关详细信息和特殊情况,请参阅私有名称修饰规范。
名称修饰有助于让子类覆盖方法,而不会破坏类内部的方法调用。例如:
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
类中被替换为 _MappingSubclass__update
。
请注意,修饰规则的设计主要是为了避免意外;仍然可以访问或修改被认为是私有的变量。这在特殊情况下甚至可能很有用,例如在调试器中。
请注意,传递给 exec()
或 eval()
的代码不会将调用类的类名视为当前类;这类似于 global
语句的效果,其效果同样仅限于一起字节编译的代码。同样的限制也适用于 getattr()
、setattr()
和 delattr()
,以及直接引用 __dict__
时。
9.7. 杂项¶
有时,拥有一个类似于 Pascal “record” 或 C “struct” 的数据类型很有用,它可以将一些命名的数据项捆绑在一起。惯用的方法是为此目的使用 dataclasses
。
from dataclasses import dataclass
@dataclass
class Employee:
name: str
dept: str
salary: int
>>> john = Employee('john', 'computer lab', 1000)
>>> 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
<str_iterator object at 0x10c90e650>
>>> 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
了解了迭代器协议背后的机制后,很容易将迭代器行为添加到你的类中。定义一个返回带有 __next__()
方法的对象的 __iter__()
方法。如果该类定义了 __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. 生成器¶
生成器 是创建迭代器的简单而强大的工具。它们的编写方式与常规函数类似,但在想要返回数据时使用 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.index
和 self.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']
脚注