9. 类¶
与其他编程语言相比,Python的类机制添加了具有最少新语法和语义的类。它是在C ++和Modula-3中发现的类机制的混合。Python 的类提供了面向对象编程的所有标准功能: 类继承机制允许多重继承,派生类可以覆盖其基类的任意方法或嵌套类,方法能够以相同的名称调用基类中的方法。对象可以包含任意数量和种类的数据。就像模块一样,类也具有Python的动态特性:它们在运行时创建,并且可以在创建后进一步修改。
用 C++ 术语来讲,通常情况下类成员(包括数据成员)是 公有 的(其它情况见下文 私有变量),所有的成员函数都是虚 的。与Modula-3中一样,没有用于从其方法引用对象成员的shorthands:方法函数使用表示对象的显式第一个参数声明,这是由调用隐式提供的。和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程序的文本区域,其中可直接访问命名空间。“直接可访问”在这里意味着对名称的非限定引用尝试在命名空间中查找名称。
尽管范围是静态确定的,但它们是动态使用的。在执行期间的任何时间,至少有三个嵌套的作用域,其命名空间可以直接访问:
- 最先搜索的最内层包含本地名称
- 从最近的封闭范围开始搜索的任何封闭函数的作用域都包含非本地名称,但也包含非全局名称
- 倒数第二个作用域包含当前模块的全局名称
- 最外层的范围(最后搜索)是包含内建名称的命名空间
如果一个名称被声明为全局的,那么所有的引用和赋值直接到包含模块的全局名称的中间作用域。如果要重新绑定最里层作用域之外的变量,可以使用 nonlocal
语句;如果不声明为nonlocal,这些变量将是只读的(对这样的变量赋值会在最里面的作用域创建一个 新的 局部变量,外部具有相同命名的那个变量不会改变)。
通常,本地作用域引用(文本)当前函数的本地名称。外部函数,本地作用域引用与全局作用域相同的命名空间:模块的命名空间。类定义在本地作用域中放置另一个命名空间。
重要的是要意识到范围是文本决定的:在模块中定义的函数的全局范围是该模块的命名空间,不管从什么别名,函数被调用。另一方面,实际搜索名称是在运行时动态完成的 - 然而,语言定义正在向“静态名称解析”发展,在“编译”时间,因此不要依赖动态名称解析!(事实上,局部变量已经静态确定。)
Python的一个特别之处在于——如果没有使用 global
语法——其赋值操作总是在最里层的作用域。分配不复制数据 - 它们只是将名称绑定到对象。删除也是如此: 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
注意,local 的赋值(默认行为)没有改变 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
另一种实例属性引用是方法。方法是“属于”对象的函数。(在Python中,术语方法对类实例不是唯一的:其他对象类型也可以有方法。例如,列表对象有称为append,insert,remove,sort等方法。但是,在下面的讨论中,除非另有明确说明,否则我们将使用术语方法专用于表示实例类对象的方法。
实例对象的有效方法名称取决于它的类。根据定义,作为函数对象的类的所有属性定义其实例的相应方法。所以在我们的示例中, 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'
正如在 名称和对象 讨论的, 可变 对象,例如列表和字典,的共享数据可能带来意外的效果。例如,以下代码中的trick列表不应用作类变量,因为所有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. 补充说明¶
数据属性会覆盖同名的方法属性;为了避免意外的命名冲突,这在大型程序中可能带来极难发现的 bug,使用一些约定来减少冲突的机会是明智的。可能的约定包括大写方法名称,使用小的唯一字符串(可能只是下划线)为数据属性名称添加前缀,或者对数据属性使用方法和名词的动词。
数据属性可以由方法以及对象的普通用户(“客户端”)引用。换句话说,类不能用于实现纯抽象数据类型。事实上,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 中的所有方法实际上都是 虚
的。)
派生类中的重写方法实际上可能希望扩展而不是简单地替换同名的基类方法。有一个简单的方法可以直接调用基类方法:只要调用 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()
,方法解析的顺序会动态改变。这种方法在一些其他多继承语言中称为call-next-method,比单继承语言中的超级调用更强大。
动态排序是必要的,因为多重继承的所有情况表现出一个或多个菱形关系(其中至少一个父类可以通过从最底层的多个路径访问)。例如,所有的类都继承自 object
,所以任何多继承都会有多条路径到达 object
。为了保持基类不被多次访问,动态算法以保持每个类中指定的从左到右顺序的方式线性化搜索顺序,每个类只调用一次,并且是单调的(意思是一个类可以被子类化而不影响其父类的优先级顺序)。总之,这些属性使得可以设计具有多重继承的可靠和可扩展类。有关详情,请参阅https://www.python.org/download/releases/2.3/mro/。
9.6. 私有变量¶
Python中并不存在所谓只能在对象内部才能访问的“私有”实例变量。然而,有一项大多数 Python 代码都遵循的习惯:带有下划线(例如_spam
)前缀的名称应被视为非公开的 API 的一部分(无论是函数、 方法还是数据成员)。它应被视为实现细节,如有更改,恕不另行通知。
由于存在一种合理的类私有成员使用场景(例如为了避免名称与子类所定义名称的冲突),Python 对这种机制提供了简单的支持,即所谓“名称重整” (name mangling)。__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)
请注意名称改编的目的主要是避免发生意外;访问或者修改私有变量仍然是可能的。这在特殊情况下甚至是有用的,例如在调试器中。
注意传递给 exec
或 eval()
的代码没有考虑要将调用类的类名当作当前类;这类似于 global
语句的效果,影响只限于一起进行字节编译的代码。相同的限制适用于 getattr()
、setattr()
和 delattr()
,以及直接引用 __dict__
时。
9.7. 零碎的说明¶
有时,使用类似于Pascal“record”或C“struct”的数据类型,捆绑几个命名的数据项是有用的。一个空类定义会很好:
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. 异常也是类¶
用户定义的异常也由类标识。使用此机制,可以创建异常的可扩展层次结构。
raise
语句有两种新的有效的(语义上的)形式:
raise Class
raise Instance
第一种形式中,Class
必须是 type
或者它的子类的一个实例。第一种形式是一种简写:
raise Class()
except
子句中的类如果与异常是同一个类或者是其基类,那么它们就是相容的(但是反过来是不行的——except子句列出的子类与基类是不相容的)。例如,以下代码将按顺序打印B,C,D:
class B(Exception):
pass
class C(B):
pass
class D(C):
pass
for cls in [B, C, D]:
try:
raise cls()
except D:
print("D")
except C:
print("C")
except B:
print("B")
请注意,如果except 子句的顺序倒过来 (except B
在最前面)。它就会打印 B,B,B —— 第一个匹配的异常被触发。
打印一个异常类的错误信息时,先打印类名,然后是一个空格、一个冒号,然后是用内置函数 str()
将类转换得到的完整字符串。
9.9. 迭代器¶
现在你可能注意到大多数容器对象都可以用 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 ?
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.10. 生成器¶
生成器是一种可以简单有效的创建迭代器的工具。它们像常规函数一样撰写,但是在需要返回数据时使用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.11. 生成器表达式¶
一些简单的生成器可以简洁地编码成表达式,语法类似于列表推导式,但是用圆括号而不是方括号。这些生成器表达式的设计是用于生成器被一个函数立刻用到的情形。生成器表达式,比完整的生成器定义紧凑但是功能上不及,比等价的列表推导式更节省内存方面。
例子:
>>> 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
>>> from math import pi, sin
>>> sine_table = {x: sin(x*pi/180) for x in range(0, 91)}
>>> 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__ 是一个属性但不是一个全局的名称。显然,使用它违反了命名空间实现的抽象,并且应该局限于事后调试器之类的东西。 |