You are here:  Home » Python » 类的定义、参数声明、数据成员使用详解(9)python入门教程(原版)

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

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

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

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

 

9.1。一个关于名称和对象的词

对象具有个性,多个名称(在多个范围内)可以绑定到同一个对象。这在其他语言中称为别名。乍一看Python时通常不会理解这一点,在处理不可变的基本类型(数字,字符串,元组)时可以安全地忽略它。但是,别名对涉及可变对象(如列表,字典和大多数其他类型)的Python代码的语义可能会产生惊人的影响。这通常用于程序的好处,因为别名在某些方面表现得像指针。例如,传递一个对象很便宜,因为实现只传递一个指针; 如果函数修改了作为参数传递的对象,调用者将看到更改 – 这消除了对Pascal中两个不同的参数传递机制的需要。

 

9.2。Python范围和命名空间

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

让我们从一些定义开始。

一个命名空间是从名字到对象的映射。大多数名称空间当前都是作为Python字典实现的,但这通常不会以任何方式显着(性能除外),并且可能在将来发生变化。命名空间的示例是:内置名称集(包含诸如abs()和内置异常名称之类的函数); 模块中的全局名称; 和函数调用中的本地名称。在某种意义上,对象的属性集也形成了命名空间。关于命名空间的重要一点是,不同命名空间中的名称之间绝对没有关系; 例如,两个不同的模块都可以定义函数maximize而不会产生混淆 – 模块的用户必须在其前面加上模块名称。

顺便说一句,我对word 后面的任何名称使用word 属性 – 例如,在表达式中z.realreal是对象的属性 z。严格地说,对模块中名称的引用是属性引用:在表达式中modname.funcnamemodname是模块对象并且funcname是它的属性。在这种情况下,模块的属性和模块中定义的全局名称之间恰好有一个直接的映射:它们共享相同的命名空间![1]

属性可以是只读的或可写的。在后一种情况下,可以分配属性。模块属性是可写的:你可以写 。可写属性也可以使用语句删除 。例如,将从名为的对象中删除该属性。modname.the_answer = 42deldelmodname.the_answerthe_answermodname

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

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

范围是Python程序,其中一个命名空间是可直接访问的文本区域。这里的“可直接访问”意味着对名称的非限定引用会尝试在命名空间中查找名称。

尽管范围是静态确定的,但它们是动态使用的。在执行期间的任何时候,至少有三个嵌套的作用域,其名称空间可以直接访问:

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

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

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

重要的是要意识到范围是以文本方式确定的:模块中定义的函数的全局范围是模块的命名空间,无论从何处或通过调用函数的别名。另一方面,名称的实际搜索是在运行时动态完成的 – 但是,语言定义在“编译”时朝着静态名称解析发展,所以不要依赖于动态名称解析!(事实上​​,局部变量已经静态确定。)

Python的一个特殊之处在于 – 如果没有global语句生效 – 对名称的赋值总是进入最里面的范围。分配不复制数据 – 它们只是将名称绑定到对象。删除也是如此:该语句删除了本地范围引用的命名空间的绑定。实际上,引入新名称的所有操作都使用本地范围:特别是,语句和函数定义绑定本地范围中的模块或函数名称。del xximport

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

 

请注意本地分配(默认)如何不会更改scope_test垃圾邮件的绑定。该nonlocal分配改变了scope_test垃圾邮件的绑定,并且该global分配更改了模块级绑定。

您还可以在分配之前 看到之前没有垃圾邮件绑定global

 

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.iMyClass.f是有效的属性的引用,分别返回一个整数和一个功能对象。也可以为类属性分配,因此您可以MyClass.i通过赋值更改值。 __doc__也是一个有效的属性,返回属于该类的docstring : ."A simple exampleclass"

实例化使用函数表示法。只是假装类对象是一个无参数函数,它返回一个新的类实例。例如(假设上述类):

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 ++中的“数据成员”。不需要声明数据属性; 就像局部变量一样,它们在第一次被分配时就会存在。例如,如果 xMyClass上面创建的实例,则下面的代码将打印该值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'

 

正如在关于名称和对象的单词中所讨论的,共享数据可能具有令人惊讶的效果,涉及可变对象,例如列表和字典。例如,以下代码中的技巧列表不应该用作类变量,因为所有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。随机备注

数据属性覆盖具有相同名称的方法属性; 为了避免可能导致大型程序中难以发现的错误的意外名称冲突,使用某种最小化冲突机会的约定是明智的。可能的约定包括大写方法名称,使用小的唯一字符串(可能只是下划线)为数据属性名称添加前缀,或者使用动词作为数据属性的方法和名词。

数据属性可以由方法以及对象的普通用户(“客户端”)引用。换句话说,类不可用于实现纯抽象数据类型。事实上,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

 

现在fg并且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)Trueobj.__class__intint
  • 使用issubclass()检查类继承: 是,因为是的子类。然而, 就是因为不是一个子类。issubclass(bool,int)Trueboolintissubclass(float, int)Falsefloatint

 

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的非公共部分(无论是函数,方法还是数据成员)。它应被视为实施细节,如有更改,恕不另行通知。

由于类私有成员有一个有效的用例(即为了避免名称与子类定义的名称冲突),因此对这种称为名称修改的机制的支持有限。表单的任何标识符 __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__update在 Mapping类和_MappingSubclass__updateMappingSubclass 分别类。

请注意,修剪规则主要是为了避免事故; 它仍然可以访问或修改被认为是私有的变量。这甚至可以在特殊情况下使用,例如在调试器中。

请注意,代码传递给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。迭代器

到目前为止,您可能已经注意到大多数容器对象都可以使用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。发电机

Generator是一个用于创建迭代器的简单而强大的工具。它们像常规函数一样编写,但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

>>> 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__是属性,但不是全局名称。显然,使用它会违反命名空间实现的抽象,并且应该仅限于事后调试器之类的东西。