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 程序中可以直接访问一个命名空间的代码区域。这里的“直接访问”的意思是用没有前缀的引用在命名空间中找到的相应的名称。
虽然作用域是静态确定的,但是使用它们时是动态的。程序执行过程中的任何时候,至少有三个嵌套的作用域,它们的命名空间是可以直接访问的:
- 首先搜索最里面包含局部命名的作用域
- 从最近的封闭范围开始搜索的任何封闭函数的作用域都包含非本地名称,但也包含非全局名称
- 倒数第二个作用域包含当前模块的全局名称
- 最后搜索的作用域是最外面包含内置命名的命名空间
如果一个命名声明为全局的,那么对它的所有引用和赋值会直接搜索包含这个模块全局命名的作用域。如果要重新绑定最里层作用域之外的变量,可以使用 nonlocal
语句;如果不声明为nonlocal,这些变量将是只读的(对这样的变量赋值会在最里面的作用域创建一个 新的 局部变量,外部具有相同命名的那个变量不会改变)。
通常情况下,局部作用域引用当前函数的本地命名。函数之外,局部作用域引用的命名空间与全局作用域相同:模块的命名空间。类定义在局部命名空间中创建了另一个命名空间。
认识到作用域是由代码确定的是非常重要的:函数的全局作用域是函数的定义所在的模块的命名空间,与函数调用的位置或者别名无关。另一方面,命名的实际搜索过程是动态的,在运行时确定的——然而,Python 语言也在不断发展,以后有可能会成为静态的“编译”时确定,所以不要依赖动态解析!(事实上,本地变量是已经确定静态。)
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,它比单继承语言中的super调用更强大。
动态调整顺序是必要的,因为所有的多继承都会有一个或多个菱形关系(从最底部的类向上,至少会有一个父类可以通过多条路径访问到)。例如,所有的类都继承自 object
,所以任何多继承都会有多条路径到达 object
。为了防止基类被重复访问,动态算法线性化搜索顺序,每个类都按从左到右的顺序特别指定了顺序,每个父类只调用一次,这是单调的(也就是说一个类被继承时不会影响它祖先的次序)。所有这些特性使得设计可靠并且可扩展的多继承类成为可能。有关详细信息,请参阅https://www.python.org/download/releases/2.3/mro/。
9.6. 私有变量¶
Python中并不存在所谓只能在对象内部才能访问的“私有”实例变量。然而,有一项大多数 Python 代码都遵循的习惯:带有下划线(例如_spam
)前缀的名称应被视为非公开的 API 的一部分(无论是函数、 方法还是数据成员)。它应被视为实现细节,如有更改,恕不另行通知。
由于存在一种合理的类私有成员使用场景(例如为了避免名称与子类所定义名称的冲突),Python 对这种机制提供了简单的支持,即所谓“名称重整” (name mangling)。__spam
形式的任何标识符(前面至少两个下划线,后面至多一个下划线)将被替换为 _classname__spam
, classname
是当前类的名字。这种替换是在不考虑标识符句法位置的情况下完成的,只要它出现在类的定义内即可。
Name mangling 有利于子类重写父类的方法而不会破坏类内部的方法调用。例如:
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__ 是一个属性但不是一个全局的名称。显然,使用它违反了命名空间实现的抽象原则,应该被严格限制于调试中。 |