Python 基础 -- Tutorial(四)

9. 类(Classes)

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

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

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

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

9.1 关于名称和对象的介绍

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

9.2 Python作用域和命名空间

在介绍类之前,我首先要告诉你一些Python的作用域规则。类定义对名称空间发挥了一些巧妙的作用,您需要知道作用域和名称空间是如何工作的,才能完全理解发生了什么。顺便说一下,关于这个主题的知识对任何高级Python程序员都很有用

让我们从一些定义开始。

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

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

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

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

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

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

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

  • 首先搜索的最内层作用域包含本地名称
  • 任何外围函数的作用域都是从最近的外围作用域开始搜索的,它包含非局部名称,但也包含非全局名称
  • 倒数第二个作用域包含当前模块的全局名称
  • 最外层的作用域(最后搜索)是包含内置名称的名称空间

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

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

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

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 assignment,默认值)没有改变scope_test对spam的绑定。nonlocal赋值改变了scope_test对spam的绑定,global 赋值改变了模块级绑定。

您还可以看到,global赋值之前没有针对spam的先前绑定

9.3 初识类

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

9.3.1 类定义语法

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

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

类定义和函数定义(def语句)一样,必须在产生任何效果之前执行。(可以将类定义放在if语句的分支中,或者放在函数中。)

在实践中,类定义中的语句通常是函数定义,但允许使用其他语句,有时还会很有用——我们将在后面讨论这个问题。类中的函数定义通常有一种特殊形式的参数列表,由方法的调用约定决定——同样,这将在后面解释。

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

当类定义正常离开时(通过结束),将创建一个类对象(class object)。这基本上是类定义创建的命名空间内容的包装器;我们将在下一节学习更多关于类对象的知识。恢复了原始的局部作用域(在进入类定义之前生效的作用域),并且类对象在这里绑定到类定义头中给出的类名(在示例中为ClassName)。

9.3.2 类对象

类对象支持两种操作:属性引用(attribute references)和实例化(instantiation)。

属性引用使用Python中所有属性引用使用的标准语法:obj.name。有效的属性名是创建类对象时在类名称空间中的所有名称。所以,如果类定义看起来像这样:

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

    def f(self):
        return 'hello world'

然后 MyClass.iMyClass.f 是有效的属性引用,分别返回一个整数和一个函数对象。类属性也可以被赋值,因此您可以通过赋值__doc__更改MyClass.i 的值。也是一个有效的属性,返回属于类的文档字符串:"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 实例对象(instance objects)

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

数据属性(data attributes )对应于Smalltalk中的“实例变量”和c++中的“数据成员”。不需要声明数据属性;与局部变量一样,它们在第一次被赋值时就会出现。例如,如果x是上面创建的MyClass的实例,下面的代码段将打印值16,而不留下跟踪:

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

另一种实例属性引用是方法。方法是一个“属于”一个对象的函数。(在Python中,术语“方法”并不是类实例所独有的:其他对象类型也可以有方法。例如,列表对象有附加、插入、删除、排序等方法。然而,在下面的讨论中,除非另有明确说明,否则我们将专门使用术语方法(method)来表示类实例对象的方法。)

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

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 类和实例变量

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

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程序员来说可能可读性较差,并且可以想象,可能会编写依赖于此类约定的类浏览器(class browser)程序。

任何作为类属性的函数对象都为该类的实例定义了一个方法没有必要将函数定义文本地包含在类定义中:将函数对象赋值给类中的局部变量也是可以的。例如:

# 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, gh都是类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)

方法可以以与普通函数相同的方式引用全局名称。与方法关联的全局作用域是包含其定义的模块。(类永远不会用作全局作用域。)虽然很少有好的理由在方法中使用全局数据,但全局作用域有许多合法的用途:首先,方法可以使用导入到全局作用域的函数和模块,也可以使用其中定义的函数和类。通常,包含方法的类本身就是在这个全局作用域中定义的,在下一节中,我们将找到方法为什么要引用它自己的类的一些很好的理由。

每个值都是一个对象,因此有一个类(class,也称为它的类型,type)。它被存储为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,因为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是去掉前导下划线的当前类名。只要发生在类的定义中,这种混淆就不会考虑标识符的语法位置。

名称混淆有助于让子类在不破坏类内方法调用的情况下重写方法。例如:

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”的数据类型,将几个命名的数据项捆绑在一起是有用的。一个空的类定义就可以了:

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
<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

了解了迭代器协议背后的机制后,很容易将迭代器行为添加到类中。定义一个__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 生成器

生成器创建迭代器的一个简单而强大的工具。它们像普通函数一样编写,但是在需要返回数据时使用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']

猜你喜欢

转载自blog.csdn.net/chinusyan/article/details/132279668