《python基础教程(第三版)》第九章 魔法方法、特性和迭代器

9.1 构造函数

我们要介绍的第一个魔法方法是构造函数。你可能从未听说过构造函数(constructor),它其实就是本书前面一些示例中使用的初始化方法,只是命名为init。然而,构造函数不同于普通方法的地方在于,将在对象创建后自动调用它们。因此,无需采用本书前面一直使用的做法:

>>> f = FooBar()
>>> f.init() 

构造函数让你只需像下面这样做:

>>> f = FooBar() 

在Python中,创建构造函数很容易,只需将方法init的名称从普通的init改为魔法版init即可。

class FooBar:
    def __init__(self):
          self.somevar = 42 
>>> f = FooBar()
>>> f.somevar
42 

到目前为止一切顺利。但你可能会问,如果给构造函数添加几个参数,结果将如何呢?请看下面的代码:

class FooBar: 
         def __init__(self, value=42):
               self.somevar = value

9.1.1 重写普通方法和特殊的构造函数

第7章介绍了继承。每个类都有一个或多个超类,并从它们那里继承行为。对类B的实例调用方法(或访问其属性)时,如果找不到该方法(或属性),将在其超类A中查找。请看下面两个类:

class A: 
   def hello(self): 
   print("Hello, I'm A.") 
class B(A): 
   pass

类A定义了一个名为hello的方法,并被类B继承。下面的示例演示了这些类是如何工作的:

>>> a = A() 
>>> b = B() 
>>> a.hello() 
Hello, I'm A. 
>>> b.hello() 
Hello, I'm A.

构造函数用于初始化新建对象的状态,而对大多数子类来说,除超类的初始化代码外,还需要有自己的初始化代码。虽然所有方法的重写机制都相同,但与重写普通方法相比,重写构造函数时更有可能遇到一个特别的问题:重写构造函数时,必须调用超类(继承的类)的构造函数,否则可能无法正确地初始化对象。
请看下面的Bird类:

class Bird: 
    def __init__(self): 
         self.hungry = True 
    def eat(self):
         if self.hungry: 
            print('Aaaah ...') 
            self.hungry = False 
         else: 
            print('No, thanks!')

这个类定义了所有鸟都具备的一种基本能力:进食。下面的示例演示了如何使用这个类:

>>> b = Bird() 
>>> b.eat() 
Aaaah ... 
>>> b.eat() 
No, thanks!

从这个示例可知,鸟进食后就不再饥饿。下面来看子类SongBird,它新增了鸣叫功能。

class SongBird(Bird): 
   def __init__(self): 
        self.sound = 'Squawk!' 
   def sing(self): 
        print(self.sound)

SongBird类使用起来与Bird类一样容易:

>>> sb = SongBird() 
>>> sb.sing() 
Squawk!

9.1.2 调用未关联的超类构造函数

class SongBird(Bird): 
   def __init__(self): 
         Bird.__init__(self) 
         self.sound = 'Squawk!' 
   def sing(self): 
         print(self.sound)

在SongBird类中,只添加了一行,其中包含代码Bird.init(self)。

>>> sb = SongBird() 
>>> sb.sing() 
Squawk! 
>>> sb.eat() 
Aaaah ... 
>>> sb.eat() 
No, thanks!

对实例调用方法时,方法的参数self将自动关联到实例(称为关联的方法),这样的示例你见过多个。然而,如果你通过类调用方法(如Bird.init),就没有实例与其相关联。在这种情况下,你可随便设置参数self。这样的方法称为未关联的。
通过将这个未关联方法的self参数设置为当前实例,将使用超类的构造函数来初始化SongBird对象。这意味着将设置其属性hungry。

9.1.3 使用函数super

调用这个函数时,将当前类和当前实例作为参数。对其返回的对象调用方法时,调用的将是超类(而不是当前类)的方法。因此,在SongBird的构造函数中,可不使用Bird,而是使用super(SongBird, self)。另外,可像通常那样(也就是像调用关联的方法那样)调用方法init
下面是前述示例的修订版本:

class Bird: 
     def __init__(self): 
          self.hungry = True 
     def eat(self): 
           if self.hungry: 
              print('Aaaah ...') 
              self.hungry = False 
           else: 
              print('No, thanks!')

class SongBird(Bird): 
     def __init__(self): 
           super().__init__()
           self.sound = 'Squawk!'
     def sing(self): 
           print(self.sound)

这个新式版本与旧式版本等效:

>>> sb = SongBird() 
>>> sb.sing() 
Squawk! 
>>> sb.eat() 
Aaaah ... 
>>> sb.eat() 
No, thanks!

9.2 元素访问

虽然init无疑是你目前遇到的最重要的特殊方法,但还有不少其他的特殊方法,让你能够完成很多很酷的任务。

9.2.1 基本的序列和映射协议

序列和映射基本上是元素(item)的集合,要实现它们的基本行为(协议),不可变对象需要实现2个方法,而可变对象需要实现4个。

len(self):这个方法应返回集合包含的项数,对序列来说为元素个数,对映射来说为键值对数。如果len返回零(且没有实现覆盖这种行为的nonzero),对象在布尔上下文中将被视为假(就像空的列表、元组、字符串和字典一样)。
getitem(self, key):这个方法应返回与指定键相关联的值。对序列来说,键应该是0~n -1的整数(也可以是负数,这将在后面说明),其中n为序列的长度。对映射来说,键可以是任何类型。
setitem(self, key, value):这个方法应以与键相关联的方式存储值,以便以后能够使用getitem来获取。当然,仅当对象可变时才需要实现这个方法。
delitem(self, key):这个方法在对对象的组成部分使用del语句时被调用,应删除与key相关联的值。同样,仅当对象可变(且允许其项被删除)时,才需要实现这个方法。

对于这些方法,还有一些额外的要求。

 对于序列,如果键为负整数,应从末尾往前数。换而言之,x[-n]应与x[len(x)-n]等效。
 如果键的类型不合适(如对序列使用字符串键),可能引发TypeError异常。
 对于序列,如果索引的类型是正确的,但不在允许的范围内,应引发IndexError异常。

要了解更复杂的接口和使用的抽象基类(Sequence),请参阅有关模块collections的文档。
下面来试一试,看看能否创建一个无穷序列。

def check_index(key): 
 """ 
指定的键是否是可接受的索引?
键必须是非负整数,才是可接受的。如果不是整数,
将引发TypeError异常;如果是负数,将引发Index 
 Error异常(因为这个序列的长度是无穷的)
 """ 
    if not isinstance(key, int): raise TypeError #判断一个对象是否是一个已知的类型
    if key < 0: raise IndexError 

class ArithmeticSequence: 
    def __init__(self, start=0, step=1): 
 """ 
初始化这个算术序列
 start -序列中的第一个值
 step -两个相邻值的差
 changed -一个字典,包含用户修改后的值
 """ 
       self.start = start # 存储起始值
       self.step = step # 存储步长值
       self.changed = {} # 没有任何元素被修改
    def __getitem__(self, key): 
""" 
从算术序列中获取一个元素
 """ 
       check_index(key) 
       try: return self.changed[key] # 修改过?
       except KeyError: # 如果没有修改过,
               return self.start + key * self.step # 就计算元素的值
    def __setitem__(self, key, value): 
 """ 
修改算术序列中的元素
 """ 
         check_index(key) 
         self.changed[key] = value # 存储修改后的值

这些代码实现的是一个算术序列,其中任何两个相邻数字的差都相同。第一个值是由构造函数的参数start(默认为0)指定的,而相邻值之间的差是由参数step(默认为1)指定的。你允许用户修改某些元素,这是通过将不符合规则的值保存在字典changed中实现的。如果元素未被修改,就使用公式self.start + key * self.step来计算它的值。
下面的示例演示了如何使用这个类:

>>> s = ArithmeticSequence(1, 2) 
>>> s[4] 
9 
>>> s[4] = 2 
>>> s[4] 
2 
>>> s[5] 
11

9.2.2 从 list、dict 和 str 派生

来看一个简单的示例——一个带访问计数器的列表。

class CounterList(list): 
   def __init__(self, *args): 
        super().__init__(*args) 
        self.counter = 0 
   def __getitem__(self, index): 
        self.counter += 1 
        return super(CounterList, self).__getitem__(index)

CounterList类深深地依赖于其超类(list)的行为。CounterList没有重写的方法(如append、extend、index等)都可直接使用。在两个被重写的方法中,使用super来调用超类的相应方法,并添加了必要的行为:初始化属性counter(在init中)和更新属性counter(在getitem中)。
下面的示例演示了CounterList的可能用法:

>>> cl = CounterList(range(10)) 
>>> cl 
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 
>>> cl.reverse() 
>>> cl
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0] 
>>> del cl[3:6] 
>>> cl 
[9, 8, 7, 3, 2, 1, 0] 
>>> cl.counter 
0 
>>> cl[4] + cl[2] 
9 
>>> cl.counter 
2

如你所见,CounterList的行为在大多数方面都类似于列表,但它有一个counter属性(其初始值为0)。每当你访问列表元素时,这个属性的值都加1。执行加法运算cl[4] + cl[2]后,counter的值递增两次,变成了2。

9.3 特性

如果访问给定属性时必须采取特定的措施,那么像这样封装状态变量(属性)很重要。例如,请看下面的Rectangle类:

class Rectangle: 
     def __init__(self): 
           self.width = 0 
           self.height = 0 
     def set_size(self, size): 
           self.width, self.height = size 
     def get_size(self): 
           return self.width, self.height

下面的示例演示了如何使用这个类:

>>> r = Rectangle() 
>>> r.width = 10 
>>> r.height = 5 
>>> r.get_size() 
(10, 5) 
>>> r.set_size((150, 100)) 
>>> r.width 
150

get_size和set_size是假想属性size的存取方法,这个属性是一个由width和height组成的元组。(可随便将这个属性替换为更有趣的属性,如矩形的面积或其对角线长度。)这些代码并非完全错误,但存在缺陷。使用这个类时,程序员应无需关心它是如何实现的(封装)。如果有一天你想修改实现,让size成为真正的属性,而width和height是动态计算出来的,就需要提供用于访问width和height的存取方法,使用这个类的程序也必须重写。应让客户端代码(使用你所编写代码的代码)能够以同样的方式对待所有的属性。

9.3.1 函数 property

函数property使用起来很简单。如果你编写了一个类,如前一节的Rectangle类,只需再添加一行代码。

class Rectangle: 
     def __init__(self): 
           self.width = 0 
           self.height = 0 
     def set_size(self, size): 
           self.width, self.height = size 
     def get_size(self): 
           return self.width, self.height
     size = property(get_size, set_size)

在这个新版的Rectangle中,通过调用函数property并将存取方法作为参数(获取方法在前,设置方法在后)创建了一个特性,然后将名称size关联到这个特性。这样,你就能以同样的方式对待width、height和size,而无需关心它们是如何实现的。

>>> r = Rectangle() 
>>> r.width = 10 
>>> r.height = 5 
>>> r.size 
(10, 5) 
>>> r.size = 150, 100 
>>> r.width 
150

如你所见,属性size依然受制于get_size和set_size执行的计算,但看起来就像普通属性一样。
实际上,调用函数property时,还可不指定参数、指定一个参数、指定三个参数或指定四个参数。如果没有指定任何参数,创建的特性将既不可读也不可写。如果只指定一个参数(获取方法),创建的特性将是只读的。第三个参数是可选的,指定用于删除属性的方法(这个方法不接受任何参数)。第四个参数也是可选的,指定一个文档字符串。这些参数分别名为fget、fset、fdel和doc。如果你要创建一个只可写且带文档字符串的特性,可使用它们作为关键字参数来实现。

9.3.2 静态方法和类方法

静态方法和类方法是这样创建的:将它们分别包装在staticmethod和classmethod类的对象中。静态方法的定义中没有参数self,可直接通过类来调用。类方法的定义中包含类似于self的参数,通常被命名为cls。对于类方法,也可通过对象直接调用,但参数cls将自动关联到类。下面是一个简单的示例:

class MyClass: 
     def smeth():
          print('This is a static method') 
     smeth = staticmethod(smeth) 
     def cmeth(cls): 
          print('This is a class method of', cls) 
     cmeth = classmethod(cmeth)

像这样手工包装和替换方法有点繁琐。在Python 2.4中,引入了一种名为装饰器的新语法,可用于像这样包装方法。(实际上,装饰器可用于包装任何可调用的对象,并且可用于方法和函数。)可指定一个或多个装饰器,为此可在方法(或函数)前面使用运算符@列出这些装饰(指定了多个装饰器时,应用的顺序与列出的顺序相反)。

class MyClass: 
     @staticmethod 
     def smeth(): 
           print('This is a static method') 
     @classmethod 
     def cmeth(cls): 
           print('This is a class method of', cls)

定义这些方法后,就可像下面这样使用它们(无需实例化类):

>>> MyClass.smeth() 
This is a static method 
>>> MyClass.cmeth() 
This is a class method of <class '__main__.MyClass'>

9.3.3 getattrsetattr等方法

可以拦截对对象属性的所有访问企图,其用途之一是在旧式类中实现特性(在旧式类中,函数property的行为可能不符合预期)。要在属性被访问时执行一段代码,必须使用一些魔法法。下面的四个魔法方法提供了你需要的所有功能(在旧式类中,只需使用后面三个)。

getattribute(self, name):在属性被访问时自动调用(只适用于新式类)。
getattr(self, name):在属性被访问而对象没有这样的属性时自动调用。
setattr(self, name, value):试图给属性赋值时自动调用。
delattr(self, name):试图删除属性时自动调用。

相比函数property,这些魔法方法使用起来要棘手些(从某种程度上说,效率也更低),但它们很有用,因为你可在这些方法中编写处理多个特性的代码。然而,在可能的情况下,还是使用函数property吧。
再来看前面的Rectangle示例,但这里使用的是魔法方法:

class Rectangle: 
     def __init__ (self): 
           self.width = 0 
           self.height = 0 
     def __setattr__(self, name, value): 
           if name == 'size': 
           self.width, self.height = value 
           else: 
                 self. __dict__[name] = value 
     def __getattr__(self, name): 
           if name == 'size': 
                return self.width, self.height 
           else: 
                raise AttributeError()

如你所见,这个版本需要处理额外的管理细节。对于这个代码示例,需要注意如下两点。


9.4 迭代器

9.4.1 迭代器协议

迭代(iterate)意味着重复多次,就像循环那样。本书前面只使用for循环迭代过序列和字典,但实际上也可迭代其他对象:实现了方法iter的对象。
方法iter返回一个迭代器,它是包含方法next的对象,而调用这个方法时可不提供任何参数。当你调用方法next时,迭代器应返回其下一个值。如果迭代器没有可供返回的值,应引发StopIteration异常。你还可使用内置的便利函数next,在这种情况下,next(it)与it.next()等效。
这有什么意义呢?为何不使用列表呢?因为在很多情况下,使用列表都有点像用大炮打蚊子。例如,如果你有一个可逐个计算值的函数,你可能只想逐个地获取值,而不是使用列表一次性获取。这是因为如果有很多值,列表可能占用太多的内存。但还有其他原因:使用迭代器更通用、更简单、更优雅。下面来看一个不能使用列表的示例,因为如果使用,这个列表的长度必须是无穷大的!
这个“列表”为斐波那契数列,表示该数列的迭代器如下:

class Fibs: 
     def __init__(self): 
           self.a = 0 
           self.b = 1 
     def __next__(self): 
           self.a, self.b = self.b, self.a + self.b 
           return self.a 
     def __iter__(self): 
           return self

注意到这个迭代器实现了方法iter,而这个方法返回迭代器本身。在很多情况下,都在另一个对象中实现返回迭代器的方法iter,并在for循环中使用这个对象。但推荐在迭代器中也实现方法iter(并像刚才那样让它返回self),这样迭代器就可直接用于for循环中。
首先,创建一个Fibs对象。

>>> fibs = Fibs()
>>> for f in fibs: 
... if f > 1000: 
... print(f) 
... break 
... 
1597

这个循环之所以会停止,是因为其中包含break语句;否则,这个for循环将没完没了地执行。

9.4.2 从迭代器创建序列

除了对迭代器和可迭代对象进行迭代(通常这样做)之外,还可将它们转换为序列。在可以使用序列的情况下,大多也可使用迭代器或可迭代对象(诸如索引和切片等操作除外)。一个这样的例子是使用构造函数list显式地将迭代器转换为列表。

>>> class TestIterator: 
...          value = 0 
...          def __next__(self): 
...                self.value += 1 
...                if self.value > 10: raise StopIteration 
...                return self.value 
...          def __iter__(self): 
...                return self 
... 
>>> ti = TestIterator() 
>>> list(ti) 
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

9.5 生成器

生成器是一种使用普通函数语法定义的迭代器。生成器的工作原理到底是什么呢?通过示例来说明最合适。下面先来看看如何创建和使用生成器,然后再看看幕后的情况。

9.5.1 创建生成器

生成器创建起来与函数一样简单。你现在肯定厌烦了老套的斐波那契数列,所以下面换换口味,创建一个将嵌套列表展开的函数。这个函数将一个类似于下面的列表作为参数:

nested = [[1, 2], [3, 4], [5]]

换而言之,这是一个列表的列表。函数应按顺序提供这些数字,下面是一种解决方案:

def flatten(nested): 
     for sublist in nested: 
        for element in sublist: 
           yield element

这个函数的大部分代码都很简单。它首先迭代所提供嵌套列表中的所有子列表,然后按顺序迭代每个子列表的元素。
在这里,你没有见过的是yield语句。包含yield语句的函数都被称为生成器。这可不仅仅是名称上的差别,生成器的行为与普通函数截然不同。差别在于,生成器不是使用return返回一值,而是可以生成多个值,每次一个。每次使用yield生成一个值后,函数都将冻结,即在此停止执行,等待被重新唤醒。被重新唤醒后,函数将从停止的地方开始继续执行。
为使用所有的值,可对生成器进行迭代。

>>> nested = [[1, 2], [3, 4], [5]] 
>>> for num in flatten(nested): 
... print(num) 
... 
1 
2 
3 
4 
5

>>> list(flatten(nested)) 
[1, 2, 3, 4, 5]

9.5.2 递归式生成器

前一节设计的生成器只能处理两层的嵌套列表,这是使用两个for循环来实现的。如果要处理任意层嵌套的列表,该如何办呢?例如,你可能使用这样的列表来表示树结构(也可以使用特定的树类,但策略是相同的)。对于每层嵌套,都需要一个for循环,但由于不知道有多少层嵌套,你必须修改解决方案,使其更灵活。该求助于递归了。

def flatten(nested): 
   try: 
       for sublist in nested: 
            for element in flatten(sublist): 
                 yield element 
   except TypeError: 
               yield nested

调用flatten时,有两种可能性(处理递归时都如此):基线条件和递归条件。在基线条件下,要求这个函数展开单个元素(如一个数)。在这种情况下,for循环将引发TypeError异常(因为你试图迭代一个数),而这个生成器只生成一个元素。

9.5.3

如果你按前面的例子做了,就差不多知道了如何使用生成器。你知道,生成器是包含关键字yield的函数,但被调用时不会执行函数体内的代码,而是返回一个迭代器。每次请求值时,都将执行生成器的代码,直到遇到yield或return。yield意味着应生成一个值,而return意味着生成器应停止执行(即不再生成值;仅当在生成器调用return时,才能不提供任何参数)。
换而言之,生成器由两个单独的部分组成:生成器的函数和生成器的迭代器。生成器的函数是由def语句定义的,其中包含yield。生成器的迭代器是这个函数返回的结果。用不太准确的话说,这两个实体通常被视为一个,通称为生成器。

>>> def simple_generator(): 
 yield 1 
... 
>>> simple_generator 
<function simple_generator at 153b44> 
>>> simple_generator() 
<generator object at 1510b0>

对于生成器的函数返回的迭代器,可以像使用其他迭代器一样使用它。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容