第九章 类 ——python导引编译之十

第九章 类 ——python导引编译之十

标题9. 类Classes

类提供一个把数据和其功能归集到一起的工具。创建一个新类,也就创建了一个新的对象类型,并且也允许该类型的新实例给衍生出来。每一个类实例都可以有附着在类身上的相关属性,以保持其属于该类的身份。类实例也可以有一些方法(被其类所定义的方法)用来修改类实例的状态。
与别的程序语言相比,python的类机制用较少的新型句法学和语义学来增加类别数量。它是一个绝妙的混合物,在C++语言和模块3中发现的类机制的混合物。python的类提供了所有面向对象程序的特征:类继承机制允许多个基础类,一个派生类可以覆盖其一个或多个基类的任何方法,并且,一个方法可以相同名称调用那些基类方法。对象可以包含任意数量和种类的数据。就像模块一样,类具有Python的动态特性:它们在运行时创建,并且可以在创建后进行进一步修改。
在C ++术语中,通常,类成员(包括数据成员)是公共的(除了下面的“私有变量”请看私有变元),所有成员函数都是虚拟的virtual。与Modula-3中一样,没有从其方法引用该对象成员的捷径:方法函数用表示对象的第一个参数来声称,该参数通过调用隐式提供。就像在Smalltalk中一样,类本身就是对象。这提供了导入和重命名的语义。与C ++和Modula-3不同,内置类型可以用作用户扩展的基类。同样,像在C ++中一样,大多数带有特殊语法的内置运算符(算术运算符,下标等)都可以为类实例重新定义。
(由于缺乏用于类的通用术语,我偶尔会使用Smalltalk和C ++术语。我将使用Modula-3术语,因为它的面向对象的语义比C ++更接近于Python,但是我希望读者很少听说过。)

标题9.1.关于名称和对象的一个词A Word About Names and Objects

对象具有多个性质,可以将多个名称(在多个作用域中)绑定到同一对象。这在其他语言中称为同一对象不同名称,即别名。乍一看,通常不会意识到这一点,在处理不可变的基本类型(数字,字符串,元组)时,可以安全地忽略它。但是,别名对涉及可变对象,如列表,字典和大多数其他类型的Python代码语义时,可能会产生令人惊讶的影响。这通常对程序编制有利,因为别名在某些方面的行为类似于指针。例如,传递一个对象很容易,因为借助操作,只传递了一个指针。
如果函数修改了作为参数传递的对象,则调用者将看到更改-这样就无需像Pascal中那样使用两种不同的参数传递机制。

标题9.2.Python范围和名称空间Python Scopes and Namespaces

在介绍类之前,我首先必须向您介绍一些有关Python范围规则的一些知识。类定义在使用名称空间时会起到一些巧妙的作用,并且您需要了解作用域和名称空间的工作方式以完全了解正在发生的事情。顺便说一句,有关此主题的知识对于任何高级Python程序员都是有用的。
让我们从一些定义开始。名称空间是从名称到对象的映射。
目前,大多数名称空间都是作为Python字典实现的,但通常不会以任何方式引起注意(性能除外),并且将来可能会发生变化。
名称空间的示例是:一组内置名称的集合(包含诸如函数abs()之类和内置异常名称);
一个模块中的全局名称;以及函数调用中的本地名称。从某种意义上说,对象的属性集也形成一个名称空间。了解名称空间的重要一点是,不同名称空间中的名称之间绝对没有任何关系。例如,两个不同的模块都可以定义一个最大化功能而不会引起混淆-模块的用户必须在模块名称前加上前缀。
顺便说一下,我对点后面的任何名称都使用单词属性这个词-例如,在表达式z.real中,real是对象z的属性。严格来说,对模块中名称的引用是属性引用:在表达式modname.funcname中,modname是模块对象,而funcname是其属性。在这种情况下,模块的属性与模块中定义的全局名称之间会发生直接映射:它们共享相同的名称空间!
属性可以是只读或者可写。在后一种情况下,可以分配属性。模块属性是可写的:您可以编写modname.the_answer =42。可写属性也可以使用del语句删除。例如,这个指令府del modname.the_answer,将从用modname命名的对象中,删除掉属性the_answer。
命名空间是在不同的时刻创建的,并且具有不同的生存期。包含内置名称的名称空间是在Python解释器启动时创建的,并且永远不会删除。读入模块定义后,将创建模块的全局名称空间。通常,模块名称空间也将持续到解释器退出为止。从解释器的顶层调用执行的语句(从脚本文件中读取或以交互方式读取)被视为称为__main__的模块的一部分,因此它们具有自己的全局名称空间。 (内置名称实际上也存在于模块中;这称为内置名称。)
函数的本地名称空间是在调用函数时创建的,并在函数返回或引发函数中未处理的异常时删除。
(实际上,忘记是描述实际情况的更好方法。)当然,递归调用每次都有它自己的本地名称空间。
范围指的是可直接访问名称空间的Python程序的文本区域。
这里的“直接访问”是指对名称的不合法引用试图在名称空间中找到该名称。
尽管范围是静态确定的,但它们是动态使用的。
在执行期间的任何时段,都有3或4个嵌套作用域,其名称空间可直接访问:
最里面的作用域,它首先被搜索,这个范围包含了本地名称
任何封闭函数的范围,它在最临近的封闭范围开始搜索,这个范围都包含非本地名称,但也包含非全局名称。
倒数第二个范围包含当前模块的全局名称
最外面的范围(最后搜索)是包含内置名称的名称空间
如果名称被声明为全局名称,则所有引用和赋值将直接转到包含模块全局名称的中间范围。
要重新绑定最内层作用域之外的变量,可以使用nonlocal语句。
如果未声明为非局部变量,则这些变量是只读的(尝试写入此类变量只会在最内部的作用域中创建一个新的局部变量,而使相同命名的外部变量保持不变)。
通常,本地作用域引用(按文本形式)当前函数的本地名称。
在外部函数中,本地范围引用与全局范围相同的名称空间:模块的名称空间。
类定义在本地范围内放置了另一个名称空间。
重要的是要认识到范围是由文本决定的:模块中定义的函数的全局范围是模块的名称空间,无论调用该函数的位置或名称是什么。
另一方面,实际的名称搜索是在运行时动态完成的-但是,语言定义正在“编译”时向静态名称解析发展,所以不要依赖动态名称解析! (实际上,局部变量已经是静态确定的。)
Python的一个特殊怪癖是-如果没有全局或非本地语句生效,则对名称的分配总是进入最内部的范围。分配不复制数据,它们只是将名称绑定到对象。删除也是如此:语句del x从本地作用域引用的名称空间中删除x的绑定。实际上,所有引入新名称的操作都使用本地范围:尤其是import语句和函数定义在本地范围内绑定模块或函数名称。
全局语句可用于指示特定变量位于全局范围内,应在该范围内反弹;非局部语句表示特定变量位于封闭范围内,应在该范围内反弹。

标题9.2.1.范围和命名空间示例Scopes and Namespaces Example

这是一个示例,演示如何引用不同的作用域和名称空间,以及全局和非本地如何影响变量绑定:

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的垃圾邮件绑定。
非本地分配更改了scope_test的垃圾邮件绑定,而全局分配则更改了模块级别的绑定。
您还可以看到,在全局分配之前,以前没有垃圾邮件绑定。

标题9.3.初识类A First Look at Classes

类引入了一些新的语法,引入三种新的对象类型以及一些新的语义。

标题9.3.1.类定义语法Class Definition Syntax

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

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

必须先执行类定义,如同函数定义(def语句)一样,然后才能生效。 (可以想象,可以将类定义放在if语句的分支中或函数内部。)
实际上,类定义中的语句通常是函数定义,但是其他语句也是允许的,并且有时是有用的——我们稍后会再讨论。类内部的函数定义通常具有特殊形式的参数列表,这由方法的调用约定所指定——稍后再对此进行说明。
输入类定义时,将创建一个新的名称空间,并将其用作本地作用域——因此,所有对本地变量的分配都将进入该新的名称空间。特别是,函数定义在此处绑定新函数的名称。
当正常保留类定义时(通过结尾),将创建一个类对象。基本上,这是对由类定义创建的名称空间内容的包装。我们将在下一节中学到更多有关类对象的东西。原初的本地作用域(在输入类定义之前即生效的作用域)恢复了,并且类对象绑定到类定义标头中给出的类名称(示例中为ClassName)。

标题9.3.2.类对象Class Objects

类对象支持两l类操作:属性引用和实例化。属性引用使用Python中所有属性引用使用的标准语法:obj.name。有效属性名称是创建类对象时在类名称空间中的所有名称。因此,如果类定义为如下所示:

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

    def f(self):
        return 'hello world'

那么MyClass.i和MyClass.f 就是有效的属性引用,分别返回整数和函数对象。也可以将类属性分配给它,因此您可以通过分配来更改MyClass.i的值。 __doc__也是有效的属性,它返回属于以下类的文档字符串:“简单示例类”。
类实例化使用函数表示法。只需假装类对象是无参数函数即可返回该类的新实例。例如(假设上面的类):

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.实例对象Instance Objects

现在我们该如何处理实例对象?实例对象只能理解的操作是属性引用。有效的属性名称有两种:数据属性和方法。
数据属性对应于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.方法对象Method Objects

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

x.f()

在MyClass示例中,这将返回字符串“ hello world”。但是,不必立即调用方法:x.f是方法对象,可以将其存储起来并在以后调用。例如:

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

将持续地打印问候世界hello world,直到地老天荒。
当一个方法被调用时到底发生了什么?您可能已经注意到,即使f()的函数定义指定了一个参数,函数x.f()被调用并没有用到上述参数。该参数发生了什么?确信地,当一个需要参数的函数被调用时,即使这个参数并没有实际上使用过,Python肯定也会引发异常。
实际上,您可能已经猜到了答案:方法的特殊之处在于,实例对象作为函数的第一个参数传递。
在我们的示例中,调用x.f()完全等同于调用函数MyClass.f(x)。通常,调用具有n个参数列表的方法等同于调用带有参数列表的函数,该参数列表是通过在第一个参数之前插入方法的实例对象而创建的。
如果您仍然不了解方法如何进行工作的原理,那么看一下操作实现implementation,可能可以澄清问题。当引用实例的非数据属性时,实例的类将被搜索。如果名称表示作为函数对象的有效类属性,则通过将实例对象和刚在抽象对象中一起找到的函数对象打包(指向)来创建方法对象:这是方法对象。当使用参数列表调用方法对象时,将从实例对象和参数列表中构造一个新的参数列表,并使用该新的参数列表来调用那个函数对象。

标题9.3.5.类和实例变量Class and Instance Variable

一般而言,实例变量对于每个实例而言,数据唯一,而类变量对于属性和方法而言,所有类的实例共享这些属性和方法:

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'

正如“关于名称和对象的文字”中所讨论的那样,共享数据在涉及诸如列表和字典之类的可变对象时可能会产生令人惊讶的效果。例如,以下代码中的技巧列表不应用作类变量,因为所有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.随机评论Random Remarks

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

>>> 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.继承Inheritance

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

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

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

class DerivedClassName(modname.BaseClassName):

派生类定义的执行与基类的执行相同。构造类对象时,将记住基类。这用于解析属性引用:如果在类中找不到请求的属性,则搜索将继续查找基类。如果基类本身是从其他类派生的,则递归应用此规则。
派生类的实例化没有什么特别的:DerivedClassName()创建该类的新实例。方法引用的解析如下:搜索相应的类属性,必要时沿基类的链降序;如果这样做产生了一个函数对象,则该方法引用有效。
派生类可以覆盖其基类的方法。因为方法在调用同一对象的其他方法时没有特殊特权,所以基类的方法,这个基类方法调用同一基类中定义的另一个方法,可能终止于调用这样的派生类,该派生类的方法覆盖了该方法。 (对于C ++程序员:Python中的所有方法实际上都是虚拟的。)
实际上,派生类中的重写方法可能想扩展而不是简单地替换相同名称的基类方法。有一种直接调用基类方法的简单方法:只需调用BaseClassName.methodname(self,arguments)。这有时对客户也很有用。(请注意,这仅在基类可以在全局范围中作为BaseClassName访问时有效。)
Python具有两个可用于继承的内置函数:
第一个:使用isinstance()检查实例的类型:仅当obj .__ class__是int或从int派生的某个类时,isinstance(obj,int)才为True。
第二个:使用issubclass()来检查类的继承:由于bool是int的子类,所以issubclass(bool,int)为True。但是,由于float不是int的子类,所以issubclass(float,int)为False。

标题9.5.1.多重继承Multiple Inheritance

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

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

对大多数目的而言,在最简单的情况下,您可以把从父类继承属性的搜索视为深度优先,从左到右,而不是在层次结构重叠的同一个类中进行两次搜索。因此,如果在DerivedClassName中找不到属性,则在Base1中搜索该属性,然后(递归地)在Base1的基类中搜索,如果在该属性中找不到,则在Base2中搜索该属性,依此类推。
实际上,它比这稍微复杂一些。方法的解析顺序会动态更改,以支持对super()的协作调用。
这种方法在某些其他多继承语言中称为“调用下一方法”,并且比在单继承语言中找到的超级调用更强大。
动态排序是必需的,因为所有多重继承的情况都显示一个或多个菱形关系(其中至少一个父类可以通过从最底层类访问的多个路径进行访问)。例如,所有类都从对象继承,因此任何多重继承的情况都提供了到达对象的多个路径。
为了防止基类被多次访问,动态算法以一种线性化搜索顺序的方式,该方式保留了每个类中指定的从左到右的顺序,仅调用了每个父类一次,并且这是单调的(这意味着
一个类可以被子类化而不会影响其父级的优先顺序)。综合考虑这些属性,可以设计具有多重继承的可靠且可扩展的类。更多信息详见,https://www.python.org/download/releases/2.3/mro/。

标题9.6.私有变量Private Variables

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()的代码不将调用类的类名视为当前类。这类似于全局语句的效果,全局语句的效果同样仅限于字节编译在一起的代码。相同的限制适用于getattr(),setattr()和delattr()以及直接引用__dict__时。

标题9.零碎补充Odds and Ends

有时,将类似于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.迭代器Iterators

到目前为止,您可能已经注意到,大多数容器对象都可以使用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.index和self.data的方法相比,这使函数更易于编写且更清晰。
除了自动创建方法和保存程序状态外,生成器终止时,它们还会自动引发StopIteration。组合起来,这些特性使得创建一个迭代器更为容易一些,无需花费更多的精力,比编写一个常规函数省事很多。

标题9.10. 生成器表达式Generator Expressions

某些简单的生成器可以使用类似于列表推导的语法简洁地编码为表达式,但是使用园括号而不是方括号。这些表达式设计用于封闭函数立即使用生成器的情况。生成器表达式比完整的生成器定义更紧凑,但用途更少,并且比等效的列表理解更易于存储。
例子:

>>> 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__是属性,而不是全局名称。显然,使用此方法违反了名称空间实现的抽象,并且应仅限于事后调试器之类。

猜你喜欢

转载自blog.csdn.net/weixin_41670255/article/details/109256936
今日推荐