Python 学习笔记之十——类

来源:互联网 发布:秦罗敷与刘兰芝 知乎 编辑:程序博客网 时间:2024/05/16 19:32

  与其他编程语言相比,Python 的类机制用最少的语法和语义引入了类。它是 C++ 和 Modula-3 类机制的混合。Python 的类提供了面向对象编程的所有标准功能:类继承机制允许有多个基类,继承的类可以覆盖其基类或类的任何方法,方法能够以相同的名称调用基类中的方法。对象可以包含任意数量和种类的数据。类同样具有Python的动态性质:它们在运行时创建,并可以在创建之后进一步修改
  用 C++ 术语来讲,通常情况下类成员(包括数据成员)是公有的(其它情况见下文私有变量),所有的成员函数都是虚 的。与 Modula-3 一样,在成员方法中没有简便的方式引用对象的成员:方法函数的声明用显式的第一个参数表示对象本身,调用时会隐式地引用该对象.与 Smalltalk 一样,类本身也是对象。这给导入类和重命名类提供了语义上的合理性。与 C++ 和 Modula-3 不同,用户可以用内置类型作为基类进行扩展。此外,像 C++ 一样,类实例可以重定义大多数带有特殊语法的内置操作符(算术运算符、 下标等)。

名称和对象

  对象是独立的,多个名字(在多个作用域中)可以绑定到同一个对象。在其他语言中称为别名(即同一个对象可以有多个别名).在处理不可变的基本类型(数字、字符串、元组)时,操作同一个对象的别名并不会修改这个对象的值,然而,在Python 代码涉及可变对象如列表、 字典和大多数其它类型时,别名通常有助于优化程序,因为别名的行为有些时候类似于指针。例如,传递一个对象的开销是很小的,因为在实现上只是传递了一个指针如果函数修改了参数传递的对象,调用者也将看到变化 —— 这就避免了类似 Pascal 中需要两个不同参数的传递机制。

Python作用域和命名空间

命名空间

  类的定义非常巧妙的运用了命名空间,要想理解类,首先理解作用域和命名空间的工作原理。
  命名空间是从名称到对象的映射。当前命名空间主要通过Python字典实现的。

例如以下的一些命名空间的例子:

  • 内置名称集(包括函数名列如abs()和内置异常的名称);
  • 模块中的全局名称;
  • 函数调用中的局部名称;
    在某种意义上说,一组对象的属性也形成一个命名空间。
    注: 不同命名空间的名称绝对没有任何关系。例如:两个不同模块中的一个函数都可以定义成maximize而不会产生混淆——模块的使用者必须以模块名为前缀引用它们。
    对模块中的名称的引用是属性引用 :例如,在表达式modname.funcname中,modname是一个模块对象,funcname是它的一个属性。在这种情况下,模块的属性和模块中定义的全局名称之间碰巧是直接的映射:它们共享统一命名空间。
    属性可以是只读的也可以是可写的。 当属性是可写的时候,可以对属性赋值。模块的属性都是可写的:你可以这样写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 语句将变量引入到全局作用域。)
global 语句可以用来指明某个特定的变量位于全局作用域并且应该在那里重新绑定;nonlocal语句表示特定的变量位于一个封闭的作用域并且应该在那里重新绑定。

作用域和命名空间示例

下面的示例演示如何访问不同作用域和命名空间,以及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 spams. In global scope: global spam

第一个调用do_local()并没有改变任何内容,其函数体生成一个变量spam = "local spam",它与函数体外的
spam = "test spam"并不是一个变量,这个函数调完后其内部变量也自动释放了,所以第一个输出是
After local assignment: test spam
第二个调用 do_nonlocal() 其函数体 nonlocal spam声明 其上层函数的spam 变量是可写的,就是说可以修改,所以调用时spam = "nonlocal spam" 把其父函数的局部变量 spam 修改成了”nonlocal spam”,所以最终输出是
After nonlocal assignment: nonlocal spam
第三个调用 do_global(),函数体内部声明了一个模块的全局变量global spam与其父函数的spam 不是同一个变量,所以输出的还是第二次调用修改后的结果
After global assignment: nonlocal spams.
最后一个输出
print("In global scope:", spam)
是整个函数体外的输出,相当于是模块级别的一个输出,它输出的是模块的全局变量,所以输出结果是嵌套函数 do_global() 中定义的那个全局变量的值 global spam
注意: local赋值(默认行为)没有改变scope_test 中 spam的绑定。nonlocal赋值改变了scope_test对spam 的绑定,global赋值改变了模块级别的绑定。

初识类

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

类定义的语法

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

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

注意:类的定义就像函数定义(def语句),要先执行才能生效。(你当然可以把它放进if语句的某一分支,或者一个函数的内部。)
  实际应用中,类定义包含的语句通常是函数定义,不过其它语句也是可以的而且有时还会很有用,类中的函数定义通常有一个特殊形式的参数列表,这是由
方法调用的协议决定的.
   进入类定义部分后,会创建出一个新的命名空间,作为局部作用域——因此,所有的赋值成为这个新命名空间的局部变量。特别是这里的函数定义会绑定新函数的名字。
  类定义正常退出时,一个类对象(也就是类)也就创建了。基本上它是对类定义创建的命名空间进行了一个包装(就是类定义完毕后,其成员等都按照标准的规则创建好,并放到了相应的位置中),原始的局部作用域(类定义引入之前生效的那个)得到恢复,类对象在这里绑定到类定义头部的类名(例子中是ClassName)。

类对象

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

class MyClass:     """A simple example class"""     i = 12345     def f(self):         return 'hello world'

那么 MyClass.i 和 MyClass.f 是有效的属性引用,分别返回一个整数和一个方法对象。
也可以对类属性赋值,你可以通过给 MyClass.i 赋值来修改它。doc 也是一个有效的属性(类定义的时候Python底层默认添加的属性),返回类的文档字符串: “A simple example class”。
类的实例化和没有参数的函数调用的形式(表面形式)差不多,例如:

x = MyClass()

创建这个类的一个新实例,并将该对象赋给局部变量x。
如果类中没有初始化函数,实例化一个类将创建一个空对象,因此一般会给类定义一个名为init()的初始化函数:

def __init__(self):     self.data = [] 

self 代表类对象本身,相当于其他语言中的this, 当类定义了init()方法,类的实例化会为新创建的类实例**自动调用**__init__(), 其实和类的构造函数差不多。
当然,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)

实例对象

  实例对象唯一可用的操作就是属性引用。
有两种有效的属性名:数据属性和方法。
数据属性相当于 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 方法等等。) 实例对象的方法的有效名称依赖于它的类。类中的所有对象函数与类的实例对象中的方法是一一对应的。

方法对象

  因为Python中一切皆对象,那么类是对象,实例是对象,类中的函数也是对象,实例中的方法也是对象,方法对象就是类的实例对象的方法对象,听起来挺绕口,其实就是同一个概念,类中的函数和实例中的方法是一一对应的。
通常情况下,方法在绑定之后被直接调用:

x.f() 

在 MyClass 的示例中,这将返回字符串’hello world’。然而,也不是一定要直接调用方法:x.f是一个方法对象,可以存储起来以后调用(就是把返回值存储起来)。例如:

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

会不断地打印hello world。
  方法的特别之处在于实例对象被作为函数的第一个参数传给了函数。调用x.f()完全等同于MyClass.f(x)。一般情况下,以n 个参数的列表调用一个方法就相当于将方法所属的对象插入到列表的第一个参数的前面,然后以新的列表调用相应的函数。
  引用非数据属性的实例属性时,会搜索它的类。如果这个命名确认为一个有效的函数对象类属性,就会将实例对象和函数对象封装进一个抽象对象:这就是方法对象。以一个参数列表调用方法对象时,它被重新拆封,用实例对象和原始的参数列表构造一个新的参数列表,然后函数对象调用这个新的参数列表。

类和实例变量

  一般来说,实例变量用于对每一个实例都是唯一的数据(就是说实例自己的变量只能这个实例用),类变量用于类的所有实例共享的属性和方法(类变量,它的每个实例都可以用)

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

即为每一个实例对象创建一个空的tricks 列表

补充说明

  数据属性(即属性而非方法)会覆盖同名的方法属性;为了避免意外的命名冲突,这在大型程序中可能带来极难发现的 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的实例的方法因此它们都是c的实例的方法—— h完全等同于g。但一般不这样写代码,因为这种做法通常只会使阅读程序的人产生困惑。

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) 

self.data = [] 类在实例化的时候会创建一个data 的实例对象变量。
方法可以像普通函数那样引用全局命名。与方法关联的全局作用域是包含这个方法的定义的模块(类)。(类本身永远不会作为全局作用域使用。)一般很少在方法中使用全局数据,但全局作用域有很多合法的用途:其一是方法可以调用导入全局作用域的函数和方也可以调用定义在其中的类和函数。通常,包含此方法的类也会定义在这个全局作用域。
每个值都是一个对象,因此每个值都有一个类(也称它的类型)。它存储为object._class_

继承

  当然,一个语言特性不支持继承是配不上“类”这个名字的。派生类定义的语法如下所示:

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

BaseClassName必须与派生类定义在一个作用域内。用其他任意表达式代替基类的名称也是允许的。例如,当基类定义在另一个模块中时:

class DerivedClassName(modname.BaseClassName):

派生类定义的执行过程和基类是相同的。类对象创建后,基类会被保存。这用于解析属性的引用:如果在派生类中找不到请求的属性,会在基类中继续进行搜索。如果基类本身是由别的类派生而来,这个规则会递归应用(即继续向上搜索)。派生类的实例化没有什么特殊之处:DerivedClassName()创建类的一个新的实例。方法的引用按如下规则解析: 搜索对应的类的属性,必要时沿基类链逐级搜索,如果找到了函数对象这个方法引用就是合法的。
  派生的类可能重写其基类的方法。
  派生类中的覆盖方法可能是想要扩充而不是简单的替代基类中的重名方法。有一个简单的方法可以直接调用基类方法:只要调用BaseClassName.methodname(self, arguments)(要注意只有BaseClassName在同一全局作用域定义或导入时才能这样用。)
使用isinstance()来检查确定实例的类型,例如: isinstance(obj, int),这个函数调用的作用是只有obj.class是int或者是从int派生的类时才为True。
使用issubclass()来检查一个类是否继承于另一个类,例如:
issubclas(bool, int) 来确定bool类是否是继承于int 类,这里返回True,因为bool是int的子类。然而,issubclass(float, int)为False,因为float不是int的子类.

多继承

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

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

  对于大多数用途,在最简单的情况下,你可以认为继承自父类的属性搜索是从左到右的深度优先搜索,不会在同一个类中搜索两次,即使层次会有重叠。因此,如果在DerivedClassName中找不到属性,它搜索Base1,然后(递归)基类中的Base1,如果没有找到,它会搜索base2,依此类推。
  事实要稍微复杂一些;为了支持合作调用super(),方法解析的顺序会动态改变。这种方法在某些其它多继承的语言中也有,叫做call-next-method,它比单继承语言中的super调用更强大。
  动态调整顺序是必要的,因为所有的多继承都会有一个或多个菱形关系(从最底部的类向上,至少会有一个父类可以通过多条路径访问到)。例如,所有的类都继承自 object,所以任何多继承都会有多条路径到达 object.为了防止基类被重复访问,动态算法线性化搜索顺序(使搜索路径线性化),每个类都按从左到右的顺序特别指定了顺序,每个父类只调用一次,这是单调的。所有这些特性使得设计可靠并且可扩展的多继承类成为可能。

私有变量

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

这里把父类的update 函数对象保存了一个复本,当子类重写时修改的时候父类同名方法的复本,对基类中原始函数并没有修改。
注意: 名称改编的目的是避免发生意外;访问或者修改私有变量仍然是可能的。这在特殊情况下,例如调试的时候,还是很有用的。
   注意传递给 exec 或 eval()的代码没有考虑要将调用类的类名当作当前类;这类似于 global语句的效果,影响只限于一起进行字节编译的代码。相同的限制适用于getattr()、 setattr()和delattr(),以及直接引用dict时。

零碎的说明

  有时候类似于C的”struct”(结构体)的数据类型比较有用,它们把几个已命名的数据项目绑定在一起(属性初始化)。在Python中一个空的类定义可以很好的做到:

class Employee:     passjohn = Employee() # Create an empty employee record # Fill the fields of the record john.name = 'John Doe' john.dept = 'computer lab' john.salary = 1000

如果在写代码的时候需要一个特殊的抽象数据结构的话,通常可以传入一个类来模拟该数据类型。例如:如果你有一个用于从文件中格式化数据的函数,可以定义一个带有read()和readline() 方法的类,以此从字符串缓冲读取数据,然后将该类的对象作为参数传入前述的函数。
实例的方法对象也有属性:m.self 是具有方法m() 的实例对象,m.func是方法的函数对象。

异常也是类

用户定义的异常类也由类标识。利用这个机制可以创建可扩展的异常层次。
raise语句有两种新的有效的(语义上的)形式:
raise Class (引发一个类异常)
raise Instance (引发一个实例异常)
第一种形式中,Class 必须是type或者它的子类的一个实例。第一种形式是一种简写:
raise Class()
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") 

把上面的代码写到exec_test.py 中,执行:

python exec_test.pyBCD

请注意,如果except 子句的顺序倒过来(except B在最前面),它就会打印B,B,B ——第一个匹配的异常被触发。
修改后:

class B(Exception):     pass class C(B):     pass class D(C):     pass for cls in [B, C, D]:     try:         raise cls()     except B:        print("B")     except C:         print("C")     except D:         print("D") 

执行代码输出:

python exec_test.pyBBB

因为B 是C、D 的基类,B被触发后每次打印出的异常都是B。
打印一个异常类的错误信息时,先打印类名,然后是一个空格、一个冒号,然后是用内置函数 str() 将类转换得到的完整字符串。

迭代器

大多数容器对象都可以用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='')

在后台,for 语句调用传入了容器对象的iter()。该函数返回一个定义了next()方法的迭代器对象,它在容器中逐一访问元素。没有后续的元素时,next()会引发Stoplteration异常,告诉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 

生成器

生成器是简单且功能强大的工具,用于创建迭代器。它们写起来就像是正规的函数,需要返回数据的时候使用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() 方法是自动创建的。
另一个关键特征生成器在调用中本地变量和执行状态会自动保存。
除了自动创建方法和保存程序的状态,当生成器终止时,它们会自动引发StopIteration。结合这些特点,创建迭代器就和写一个普通函数一样简单。

生成器表达式

  一些简单的生成器可以简洁地使用表达式,语法类似于列表格式,但用圆括号()而不是方括号。下面是一些内置的生成器:

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

关于生成器个一个详细介绍:
Python中生成器和yield语句的用法详解

生成器的一些关键思想:
1. generator 是用来产生一系列值的
2. yield 则像是generator 函数的返回结果
3. yield 唯一所做的另一件事就是保存一个generator 函数的状态
4. generator 就是一个特殊类型的迭代器(iterator)
5. 和迭代器相似,我们可以通过使用next() 来从generator中获取下一个值
6. 通过隐式地调用next()来忽略一些值

0 0