part8-1 Python 类的特殊方法(__repr__ 、__del__、 __dir__、 __dict__ 属性、__getattribute__、 __getattr__、__setattr__ 、__delattr__、hasattr、getattr、setattr、__call__),序列的特殊方法、生成器yield、send、throw、close


在 Python 中类有特殊方法名、属性名,这些名称的前后面都加有双下划线,可以重写这些方法或直接使用这些方法来实现特殊的功能。比如常见的构造方法 __init__ 就可重写来实现自己的初始化逻辑。

Python 类中的特殊方法、特殊属性有些需要开发者重写,有些可以直接调用,掌握这些常见的特殊方法、特殊属性是非常重要的。

一、 常见的特殊方法

下面的常见特殊方法对于 Python 类非常有用。

1、 重写 __repr__ 方法
__repr__() 是 Python 类中的一个特殊方法,由 object 类提供。由于所有的 Python 类都是 object 类的子类,因此所有的Python 对象都具有 __repr__() 方法。

__repr__() 这个特殊的方法是一个“自我描述”的方法,常用于实现这样功能:当程序直接打印该对象时,系统将会输出该对象的“自我描述”信息,用来告诉外界该对象具有的状态信息。

object 类提供的 __repr__() 方法返回的是该对象实现类的 “类名 + object at + 内存地址” 值,这个值并不能真正实现“自我描述”功能,因此,如果需要自定义类能实现“自我描述”的功能,就需要重写 __repr__() 方法。例如下面这段代码中的 Item 类,默认是继承 object 类,没有重写 __repr__() 方法,看看运行结果会是什么样。代码如下:
class Item:
    """没有重写 __repr__() 方法的类"""
    def __init__(self, name, price):
        self.name = name
        self.price = price

im = Item('apple', 5.8)     # 创建一个 Item 对象,赋值给 im 变量
# 两次打印 im 所引用的 Item 对象
print(im)
print(im.__repr__())

运行结果如下所示:
<__main__.Item object at 0x00000258C7F6F320>
<__main__.Item object at 0x00000258C7F6F320>
从运行结果的输出可知,打印 Item 类的 im 对象时,实际输出的是 __repr__() 这个方法的返回值,所以要将某个对象与其它字符串拼接时,可以这样拼接im对象 im.__repr__() + "stark"。现在重写 __repr__() 方法实现按照“自我描述”的功能返回,例如下面的 Apple 类代码所示,代码如下:
class Apple:
    """重写 __repr__() 方法的类"""
    def __init__(self, color, weight):
        self.color = color
        self.weight = weight
    def __repr__(self):
        """重写 __repr__() 方法,用于实现 Apple 对象的 自我描述 """
        return "Apple[color=%s, weight=%s]" % (self.color, self.weight)

a = Apple("红色", 5.99)
print(a)        # 打印 a 对象
print(a.__repr__())

运行结果如下所示:
Apple[color=红色, weight=5.99]
Apple[color=红色, weight=5.99]
从运行结果可知,重写 __repr__() 方法后返回了预期的结果。通常重写 __repr__() 方法可返回下面格式的字符串:
类名[field1=value1, field2=value2, ...]

2、 析构方法:__del__
与 __init__() 方法对应的是 __del__() 方法,该方法用于销毁 Python 对象,在任何 Python 对象将要被系统回收之时,系统都会自动调用该对象的 __del__() 方法。

当程序不再需要一个 Python 对象时,系统必须把对象所占用的内存空间释放出来,这个过程被称为垃圾回收(GC,Garbage Collector),Python 自动回收所有对象所占用的内存空间。自动回收内存空间采用的是引用计数(ARC)方式,当程序中有一个变量引用对象时,该对象的引用计数为1,有两个变量引用该对象时,该对象的引用计数为2 ··· 以此类推,如果一个对象的引用计数为0,则说明程序中不再有变量引用该对象,表明程序不再需要该对象,因此 Python 就会回收该对象。

引用计数(ARC)方式多数情况下都能准确、高效的回收系统中的每个对象,例外情况是循环引用,如对象a持有一个实例变量引用对象b,而对象b又持有一个实例变量引用对象a,此时两个对象的引用计数都是1,此时需要专门的循环垃圾回收器(Cyclie Garbage Collector)来检测并回收这种引用循环。

当一个对象被垃圾回收时,Python 会自动调用调用该对象的 __del__ 方法。对一个变量执行 del 操作,该变量所引用的对象不一定会被回收,只有当对象的引用计数变成 0 时,该对象才会被回收。当一个对象有多个变量引用时,对其中一个变量使用 del 时不会回收该对象。代码实例如下:
class Item:
    def __init__(self, name, price):
        self.name = name
        self.price = price
    # 定义析构函数
    def __del__(self):
        print("del 删除对象")
im = Item('apple', 5.8)     # 创建一个 Item 对象,赋值给 im 变量
x = im
# 打印 im 所引用的 Item 对象
del im
print("-" * 20)

运行结果如下所示:
--------------------
del 删除对象
这里在 Item 类中重写了 __del__() 方法,当系统将要回收 Item 时,系统会自动调用 Item 对象的 __del__() 方法。从输出可知,在执行 del im 删除 im 对象时,由于还有变量引用该 Item 对象,因此程序并不会回收 Item 对象,只有等到程序执行将要结束时(系统必须回收所有对象),系统才会回收 Item 对象。如果将 x = im 这行注释掉的话,则程序执行 del im 后,程序中不再有任何变量引用 Item 对象,此时系统会立即回收该对象,无须等到程序结束之前。注释掉 x = im 这行的输出结果如下:
del 删除对象
--------------------
要注意的是,如果父类提供有 __del__() 方法,在子类重写 __del__() 方法时必须显式调用父类的 __del__() 方法,这样才能保证合理的回收父类实例的部分属性。

3、 __dir__方法
对象的 __dir__() 方法可将该对象内部的所有属性名和方法名列出。对某个对象执行 dir(object) 函数时,就是将该对象的 __dir__() 方法返回值进行排序,然后包装成列表。示例如下:
class Item:
    def __init__(self, name, price):
        self.name = name
        self.price = price
    def foo(self):
        pass

im = Item('apple', 5.8)     # 创建一个 Item 对象,赋值给 im 变量
print(im.__dir__())         # 返回所有属性(包括方法)组成的列表
print(dir(im))              # 返回所有属性(包括方法)排序之后的列表
运行程序,从输出信息可以看到在程序为对象定义的属性名和方法名,还有大量系统内置的属性和方法,但是没有 __del__ 方法。

4、 __dict__ 属性
__dict__ 属性可查看对象内部存储的所有属性名和属性值组成的字典,在程序中直接使用该属性即可。使用 __dict__ 属性不仅可以查看对象的所有内部状态,也可通过字典语法来访问或修改指定属性的值。示例如下:
class Item:
    def __init__(self, name, price):
        self.name = name
        self.price = price
    def foo(self):
        pass

im = Item('apple', 5.8)     # 创建一个 Item 对象,赋值给 im 变量
print(im.__dict__)
# 通过 __dict__ 访问 name 属性和 price 属性
print(im.__dict__['name'])
print(im.__dict__['price'])
# 修改 name 和 price 属性的值
im.__dict__['name'] = 'pear'
im.__dict__['price'] = 9.99
print(im.name)
print(im.price)

运行程序,输出如下所示:
{'name': 'apple', 'price': 5.8}
apple
5.8
pear
9.99
从输出可知,对象调用 __dict__ 属性即可通过属性名访问属性值,也可修改属性值。直接使用 __dict__ 属性输出的是字典。

5、 __getattr__、__setattr__ 等
在执行对象的访问、设置、删除等属性操作时,会对这个对象执行特定的方法,这些方法有下面几个:
(1)、__getattribute__(self, name): 当程序访问对象的 name 属性时被自动调用。
(2)、__getattr__(self, name): 当程序访问对象的 name 属性且该属性不存在时被自动调用。
(3)、__setattr__(self, name, value): 当程序对对象的 name 属性赋值时被自动调用。
(4)、__delattr__(self, name): 当程序删除对象的 name 属性时被自动调用。

当对象的属性不存在时,程序会委托给上面的 __getattr__、__setattr__、__delattr__方法来实现,因此在程序中可重写这些方法来“合成”属性,示例如下:
 1 class Rectangle:
 2     def __init__(self, width, height):
 3         self.width = width
 4         self.height = height
 5     def __setattr__(self, key, value):
 6         print('----设置%s属性----' % key)
 7         if key == 'size':
 8             self.width, self.height = value
 9         else:
10             self.__dict__[key] = value
11     def __getattr__(self, item):
12         print('----读取%s属性----' % item)
13         if item == 'size':
14             return self.width, self.height
15         else:
16             raise AttributeError
17     def __delattr__(self, item):
18         print('----删除%s属性----' % item)
19         if item == 'size':
20             self.__dict__['width'] = 0
21             self.__dict__['height'] = 0
22 
23 rect = Rectangle(3, 4)
24 print(rect.size)
25 rect.size = 5, 6
26 print(rect.width)
27 del rect.size
28 print(rect.size)
29 
30 运行程序,输出如下所示:
31 ----设置width属性----
32 ----设置height属性----
33 ----读取size属性----
34 (3, 4)
35 ----设置size属性----
36 ----设置width属性----
37 ----设置height属性----
38 5
39 ----删除size属性----
40 ----读取size属性----
41 (0, 0)
在 Rectangle 类中实现了 __setattr__()和__getattr__()方法,在实现这两个方法时对 size 属性进行了判断,如果程序正在获取 size 属性,__getattr__() 方法将返回 self.width 和 self.height 组成的元组,如果获取其他属性则直接引发 AttributeError 异常;如果程序正在设置 size 属性,则转换为对 self.width、self.height 属性的赋值,如果是对其他属性赋值,则通过对象的__dict__属性进行赋值。

对于 __setattr__()和__getattr__() 方法的一些说明:
(1)、对于 __getattr__() 方法:只处理程序访问指定属性且该属性不存在的情形。比如这里访问 width和height 属性,Rectangle 对象本身包含该属性,因此该方法不会触发。所以重写该方法只需处理需要“合成”的属性(比如size),假如访问的是其他不存在的属性,就会引发 AttributeError 异常。

(2)、对于 __setattr__()方法,当对指定属性赋值时就会触发该方法,所以在程序中对 width、height、size 属性赋值时,该方法
都会被触发。所以重写该方法即要对 size 属性赋值的情形,也要处理对 width、height 属性赋值的情形。在对 width、height 属性赋值时,一定不要在 __setattr__() 方法中再次对 width、height 赋值,因为对这两个属性赋值会再次触发 __setattr__() 方法,这样会让程序陷入死循环中

可以在读取、设置属性之前进行规则限制(或者某种拦截)时,可重写 __setattr__() 或 __getattribute__ 方法来实现。示例如下:
 1 class User:
 2     def __init__(self, name, age):
 3         self.name = name
 4         self.age = age
 5     # 重写 __setattr__() 方法对设置的属性值进行检查
 6     def __setattr__(self, key, value):
 7         # 如果正在设置 name 属性
 8         if key == 'name':
 9             if 2 < len(value) <= 8:
10                 self.__dict__['name'] = value
11             else:
12                 raise ValueError('name 的长度必须在 2~8 之间')
13         elif key == 'age':
14             if 10 < value < 60:
15                 self.__dict__['age'] = value
16             else:
17                 raise ValueError('age 值必须在 10~60 之间')
18 
19 u = User('michael', 24)
20 print(u.name, u.age, sep=":")       # 输出:michael:24
21 u.name = 'me'       # 引发异常
22 u.age = 2           # 引发异常
上面的 User 类只重写了 __setattr__() 方法,并且对 name、age 属性设置的属性值进行了限制。上面代码中最后两行设置的属性值不符合条件,所以会引发 ValueError 异常。

二、 与反射相关的属性和方法

程序在运行时要动态判断是否包含某个属性(包括方法),甚至要动态设置某个属性值时,可通过反射支持来实现。

1、 动态操作属性
动态检查对象是否包含某些属性(包括方法)相关的函数有下面3个:
(1)、hasattr(obj, name): 检查 obj 对象是否包含名为 name 的属性或方法。
(2)、getattr(object, name[, default]): 获取 object 对象中名为 name 属性的属性值,未获取到则返回 default。
(3)、setattr(obj, name, value, /): 将 obj 对象的 name 属性设为 value。

关于这3个函数的用法示例如下:
 1 class Comment:
 2     def __init__(self, detail, view_times):
 3         self.detail = detail
 4         self.view_times = view_times
 5 
 6     def info(self):
 7         print("一条简单的评论内容是%s" % self.detail)
 8 
 9 c = Comment('Python 很强大', 1024)
10 # 使用 hasattr 判断是否包含指定的属性或方法
11 print(hasattr(c, 'detail'))         # 输出:True
12 print(hasattr(c, 'view_times'))     # 输出:True
13 print(hasattr(c, 'info'))           # 输出:True
14 
15 # 获取指定属性的属性值
16 print(getattr(c, 'detail'))         # 输出:Python 很强大
17 print(getattr(c, 'view_times'))     # 输出:1024
18 
19 # getattr 不能获取方法的属性,所以下面代码提示错误
20 # print(getattr(c, info, '默认值'))  # 提示:NameError: name 'info' is not defined
21 # 但是对方法用字符串表示时,getattr 获取到的是这个方法
22 print(getattr(c, 'info', '默认值'))  # 输出:<bound method Comment.info of <__main__.Comment object at 0x000001E1E580F908>>
23 
24 # 为指定属性设置属性值
25 setattr(c, 'detail', 'java 同样很强大')
26 setattr(c, 'view_times', 2048)
27 
28 # 输出重新设置后的属性值
29 print(c.detail)                 # java 同样很强大
30 print(c.view_times)             # 2048
31 
32 运行程序输出如下:
33 True
34 True
35 True
36 Python 很强大
37 1024
38 <bound method Comment.info of <__main__.Comment object at 0x000001F5932BF9B0>>
39 java 同样很强大
40 2048
从输出可知 hasattr() 函数可以判断属性,也可判断方法,getattr() 获取的是属性的值,当对获取方法的属性值时返回的是方法的内存对象。setattr() 可以改变对象的属性值,如果对象的属性不存在时,就会为该对象添加属性。例如下面两行代码所示:
# 设置的属性在对象中不存时,就为对象添加属性
setattr(c, 'test', '新增测试属性')
print(getattr(c, 'test'))       # 新增测试属性
setattr()函数可以为对象添加方法,新添加的方法是未绑定方法,所以在定义方法时不能定义 self 参数,否则需要显式为参数传入参数值,系统不会自动为该参数绑定参数值。例如下面代码将 Comment 对象的 info() 方法设置为 bar() 函数,并且调用 info() 方法,示例如下:
def bar():
    print("测试为对象动态添加方法")
# 将对象 c 的 info 方法设置为 bar 函数
setattr(c, 'info', bar)
c.info()        # 这里实际执行的就是 bar() 函数
setattr()函数还可将 info() 方法设置成普通值,这样将会把 info 变成一个属性,而不是方法,示例如下:
# 将对象 c 的 info 方法设置为字符串 python
setattr(c, 'info', 'python')
c.info()        # 调用报错:TypeError: 'str' object is not callable

2、 __call__属性
hasattr()函数可判断指定的属性或方法是否存在,还可进一步对属性或方法进行判断,判断它是否可调用。通过判断该属性或方法是否包含 __call__ 属性来确定它是否可调用。示例如下:
class User:
    def __init__(self, name, passwd):
        self.name = name
        self.passwd = passwd
    def validLogin(self):
        print("验证%s的登录" % self.name)
u = User('michael', 'abcdefg')

# 判断 u.name 是否包含 __call__ 方法,即判断它是否可调用
print(hasattr(u.name, '__call__'))              # 输出:False
# 判断 u.passwd 是否包含 __call__ 方法,即判断它是否可调用
print(hasattr(u.passwd, '__call__'))            # 输出:False
# 判断 u.validLogin 是否包含 __call__ 方法,即判断它是否可调用
print(hasattr(u.validLogin, '__call__'))        # 输出:True
上面代码中判断了 User 对象的 name、passwd、validLogin 是否包含 __call__ 方法,如果有这个方法就表明它是可调用的,否则就不可调用。从输出可知,validLogin() 方法是可调用的,因此可判断出它包含有 __call__ 方法。

实际上,一个函数或对象是否能执行,关键在于是否有 __call__() 方法。比如函数调用 foo(x, y, z, ...) 只是 foo.__call__(x, y, z, ...) 的快捷写法。在定义类时,可以为类添加 __call__ 方法使得该类的实例也变成可调用的。示例如下:
1 class Role:
2     def __init__(self, name):
3         self.name = name
4     def __call__(self):
5         print("执行 Role 对象")
6 r = Role('root')
7 # 直接调用 r 对象,实际调用该对象的 __call__ 方法
8 r()         # 输出:执行 Role 对象
上面这段代码在 Role 类中定义的 __call__ 方法,这行代码 r() 是在调用对象,本质就是执行该对象的 __call__ 方法。

对于函数而言,可以用调用函数的语法来调用,也可把函数当成对象,调用它的 __call__ 方法。示例如下:
1 def foo():
2     print("调用 foo 函数")
3 # 下面使用两种方式调用 foo() 函数,输出结果是一样的
4 foo()
5 foo.__call__()

三、 与序列相关的特殊方法

序列有多个元素,实现符合序列要求的特殊方法,就可实现自己的序列。

1、序列相关方法
序列有多个元素,其相关的方法有下面几个:
(1)、__len__(self):返回值决定序列中的元素个数。
(2)、__getitem__(self,key):获取指定索引对应的元素。key 参数是整数值或slice(切片)对象,否则引发 KeyError 异常。
(3)、__contains__(self, item):判断序列是否包含 item 参数指定的元素。
(4)、__setitem__(self,key,value):设置指定索引对应的元素。参数 key 是整数或切片对象,否则引发 KeyError 异常。
(5)、__delitem__(self,key):删除指定索引对应的元素。

对于不可变序列(如字符串、元组等不能修改的序列),只需要实现上面3个方法;对于可变序列,需要实现上面的5个方法。

下面代码实现一个字符串序列,在字符串序列中默认每个字符串的长度都是3,该序列的元素按 AAA、AAB、AAC......这种格式排列。代码如下所示:
 1 def check_key(key):
 2     """
 3     该函数负责检查序列的索引,该索引必须是整数值,否则引发 TypeError 异常。
 4     且要求索引必须为非负整数值,否则引发 IndexError 异常。
 5     """
 6     if not isinstance(key, int): raise TypeError("索引值必须是整数")
 7     if key < 0: raise IndexError('索引值必须是非负数')
 8     if key >= 26 ** 3: raise IndexError('索引值不能超过%d' % 26 ** 3)
 9 
10 
11 class StringSeq:
12     def __init__(self):
13         # 用于存储被修改的数据
14         self.__changed = {}
15         # 用于存储已删除元素的索引
16         self.__deleted = []
17     def __len__(self):
18         return 26 ** 3
19     def __getitem__(self, key):
20         """
21         根据索引获取序列中的元素
22         """
23         check_key(key)
24         # 如果在 self.__changed 中找到修改后的数据
25         if key in self.__changed:
26             return self.__changed[key]
27         # 如果 key 在 self.__deleted 中,说明元素已被删除
28         if key in self.__deleted:
29             return None
30         # 否则根据计算规则返回元素
31         three = key // (26 * 26)
32         two = (key - three * 26 * 26) // 26
33         one = key % 26
34         return chr(65 + three) + chr(65 + two) + chr(65 + one)
35     def __setitem__(self, key, value):
36         """
37         根据索引修改序列中元素
38         """
39         check_key(key)
40         # 将修改的元素以 key-value 对的形式保存在 __changed 中
41         self.__changed[key] = value
42     def __delitem__(self, key):
43         """
44         根据索引删除序列中元素
45         """
46         check_key(key)
47         # 如果 __deleted 列表中没有包含被删除的 key,则添加被删除的 key
48         if key not in self.__deleted: self.__deleted.append(key)
49         # 如果 __changed 中包含被删除的 key,则删除它
50         if key in self.__changed: del self.__changed[key]
51 # 创建序列(创建对象)
52 sq = StringSeq()
53 # 获取序列的长度,就是 __len__() 方法的返回值
54 print(len(sq))      # 输出:17576
55 # 根据索引获取值,就是 __getitem__() 方法的返回值
56 print(sq[26*26])    # 输出:BAA
57 # 打印修改之前的 sq[1],就是 __getitem__() 方法的返回值
58 print(sq[1])        # 输出:AAB
59 # 修改 sq[1] 元素,就是在调用 __setitem__() 方法
60 sq[1] = 'python'
61 # 打印修改之后的 sq[1],就是 __getitem__() 方法的返回值
62 print(sq[1])        # 输出:python
63 # 删除 sq[1],就是在调用 __delitem__() 方法
64 del sq[1]
65 # 再次获取 sq[1] 的值,此时是 None
66 print(sq[1])        # 输出:None
67 # 再次对 sq[1] 赋值,此时调用的是 __setitem__() 方法
68 sq[1] = 'michael'
69 print(sq[1])        # 输出:michael
70 
71 运行程序,输出结果如下所示:
72 17576
73 BAA
74 AAB
75 python
76 None
77 michael
上面代码中的 StringSeq 类实现了 __len__()、__getitem__()、__setitem__()和__delitem__()方法,其中 __len__() 方法返回序列包含的元素个数,__getitem__() 方法根据索引返回元素,__setitem__()方法根据索引修改元素的值,__delitem__() 方法根据索引删除元素。

序列对象 sq 本身不保存序列元素,而是根据索引动态计算序列元素,所以序列对象 sq 需要保存被修改、被删除的元素。该序列使用__changed 实例变量(字典)保存被修改的元素,使用 __deleted 实例变量(列表)保存被删除的索引。

通过创建的序列对象 sq 来调用序列方法测试该工具类(StringSeq),从输出可知,序列的第二个元素 sq[1] 正好是 AAB,所以该工具类不仅可对序列元素赋值,也可删除、修改序列元素,这就是一功能完备的序列。

2、 实现迭代器
字符串、列表、元组、字典都是可迭代的,属于迭代器。如果要实现迭代器,只要实现下面两个方法即可:
(1)、__iter__(self):该方法返回一个迭代器(iterator),迭代器必须包含一个 __next__()方法,该方法返回迭代器的下一个元素。
(2)、__reversed__(self):该方法为内建的 reversed() 反转函数提供支持,当调用 reversed() 函数对指定迭代器执行反转时,实际上是由该方法实现的。所以不让迭代器反转的话,不实现这个方法即可。

下面是斐波那契数列迭代器的定义示例,斐波那契数列的元素等于前两个元素之和:f(n+2)=f(n+1)+f(n),示例如下:
 1 class Fibs:
 2     def __init__(self, len):
 3         self.first = 0
 4         self.sec = 1
 5         self.__len = len
 6     # 定义迭代器需要 __next__() 方法
 7     def __next__(self):
 8         # 如果 __len 属性的值为 0,则结束迭代
 9         if self.__len == 0:
10             raise StopIteration
11         # 完成数列计算
12         self.first, self.sec = self.sec, self.first + self.sec
13         # 数列长度减 1
14         self.__len -= 1
15         return self.first
16     # 定义 __iter__()方法,返回的是迭代器
17     def __iter__(self):
18         return self
19 
20 fb = Fibs(10)       # 创建对象 fb
21 # 获取迭代器的下一个元素
22 print(next(fb), end=' ')
23 # 使用循环遍历迭代器
24 for e in fb:
25     print(e, end=' ')
26 
27 运行程序,输出如下所示:
28 1 1 2 3 5 8 13 21 34 55
这里定义的 Fibs 类实现了一个 __iter__() 方法,该方法返回 self,所以在 Fibs 类中必须提供 __next__() 方法,该方法会返回数列的下一个值,通过使用 __len 属性控制数列的剩余长度,当 __len 为0时,程序停止遍历。

接下来创建一个长度为10的数列 fb,可使用内置的 next()函数获取迭代器的下一个元素,next()函数通过迭代器的 __next__()方法来实现的。还可以使用循环来遍历该数列(或迭代器)。运行结果如上面的输出所示。

另外,也可使用内置的 iter() 函数将列表、元组等转换成迭代器。
# 将列表转为迭代器
my_iter = iter(['python', 'linux', 'michael'])
# 依次获取迭代器的下一个元素
print(my_iter.__next__())       # python
print(my_iter.__next__())       # linux

3、 扩展列表、元组和字典
在实际运用中,可能需要一个特殊的列表、元组或字典类,此时有两种选择:
(1)、自己实现序列、迭代器等各种方法,以及实现这个特殊的类。
(2)、扩展已有的列表、元组或字典类。

第一种选择需要开发人员把所有方法都实现一遍,实现起来有些烦琐;第二种方式可继承已有的列表、元组或字典类,然后重写或新增方法即可。
下面代码是一个开发新的字典类示例,这个字典类可根据 value 来获取 key 。在字典中 value 是可以重复的,因此通过 value 获取
对应的全部 key 组成的列表。示例如下:
 1 class ValueDict(dict):
 2     def __init__(self, *args, **kwargs):
 3         # 调用父类的构造函数
 4         super().__init__(*args, **kwargs)
 5     def getkeys(self, val):
 6         """根据 val 获取字典的 key 组成的列表"""
 7         result = []
 8         for key, value in self.items():
 9             if val == value: result.append(key)
10         return result
11 
12 my_dict = ValueDict(py = 75, ja = 88, linux = 75)
13 # 获取 75 对应的所有 key
14 print(my_dict.getkeys(75))      # 输出:['py', 'linux']
15 my_dict['ja'] = 75
16 print(my_dict.getkeys(75))      # 输出:['py', 'ja', 'linux']
在上面的 ValueDict 类继承了 dict 类,并且新增了一个 getkeys 方法,通过这个方法可根据字典的 value 来获取字典的 key 组成的列表。这样就实现了对字典类(dict)的扩展,要对其他类进行扩展,方法类似。

四、生成器

生成器和迭代器功能相似,也会提供 __next__() 方法,在程序中也可以调用内置的 next() 函数来获取生成器的下一个值,也可使用 for 循环遍历生成器。

生成器与迭代器区别:迭代器是先定义一个迭代器类,通过创建实例来创建迭代器;生成器是先定义一个包含 yield 语句的函数,然后通过调用该函数来创建生成器。生成器的语法优秀,可使程序变得优雅。

1、 创建生成器
创建生成器需要两步操作:
(1)、定义一个包含 yield 语句的函数。
(2)、调用(1)步创建的函数得到生成器。

yield 语句的作用有两点:
(1)、每次返回一个值,类似于 return 语句。
(2)、冻结执行,程序每次执行到 yield 语句时就会被暂停。

在程序被 yield 语句冻结后,再次调用 next() 函数获取生成器的下一个值时,程序才会继续向下执行。要注意的是,调用包含 yield 语句的函数时并不会立即执行,它只返回一个生成器。只有在程序中通过 next() 函数调用生成器或遍历生成器时,函数才会真正执行。例如下面代码所示:
 1 def test(val, step):
 2     print("-------函数开始执行-------")
 3     cur = 0
 4     for i in range(val):
 5         # cur 每次加 i*step
 6         cur += i * step
 7         yield cur
 8 
 9 # 执行函数,返回的是生成器
10 t = test(10, 2)
11 print("=" * 20)
12 # 获取生成器的第一个值
13 print(next(t))      # 0,生成器被冻结在 yield 处
14 print(next(t))      # 2,生成器再一次被冻结在 yield 处
15 
16 输出如下所示:
17 ====================
18 -------函数开始执行-------
19 0
20 2
从输出可知,在执行 t = test(10, 2) 调用函数时,test()函数并没有开始执行;当第一次调用 next(t) 时,test() 函数才开始执行。在第二次调用时,冻结被解除,此时循环计数器 i 的值是1,cur 的是 1*2 等于 2,所以第二次调用输出的是2,并且程序再次被冻结在了 yield 语句处。

在 Python 2.x 版本中,不使用 next() 函数来获取生成器的下一个值,而是直接使用 t.next() 方式获取。

生成器可使用 for 循环来遍历,相当于不断是使用 next() 函数获取生成器的下一个值。例如下面这段代码所示:
for e in t:
    print(e, end=" ")

输出如下所示:
6 12 20 30 42 56 72 90
由于前面两次使用 next() 函数获取了生成器的前两个值,所以这次使用 for 循环第一次输出的值就是6.

除了可以使用 next() 函数、for循环方式获取生成器外,还可使用 list() 函数、tuple() 函数将生成器能生成的所有值转换成列表或元组。例如下面代码所示:
# 继续使用前面定义的生成器函数 test()
# 再次创建生成器
t2 = test(10, 1)
print(list(t2))     # 将生成器转换成列表
# 再一次创建生成器
t3 = test(10, 3)
print(tuple(t3))    # 将生成器转换成元组

运行结果如下所示:
-------函数开始执行-------
[0, 1, 3, 6, 10, 15, 21, 28, 36, 45]
-------函数开始执行-------
(0, 3, 9, 18, 30, 45, 63, 84, 108, 135)
创建生成器的方式除了在定义函数时使用 yield 语句外,在前面的第四部分使用 for 循环同样可以创建生成器,将 for 表达式放在圆括号里就能创建生成器。所以创建生成器有下面两种方式:
(1)、使用 for 循环的生成器推导式。
(2)、调用带 yield 语句的生成器函数

Python 的生成器是一个特色功能,一种非常优秀的机制,实际运用中,使用生成器有以下几个优势:
(1)、当用生成器生成多个数据时,在程序中是按需获取数据的,不会一开始就把所有数据都生成出来,而是每次调用 next() 获取下一个数据时,生成器才会执行一次,这样使得代码的执行次数减少。如 test() 函数中的for循环一开始并没有全部执行。

(2)、当函数需要返回多个数据时,不使用生成器的话,就需要使用列表或元组来收集函数返回的多个值,当函数要返回的数据量较大时,
这些列表、元组会带来一定的内存开销;使用生成器则可避免这个问题,因为生成器是按需、逐个返回数据。

(3)、使用生成器的代码更加简洁。


2、 生成器的方法
使用生成器的 send() 方法可以让生成器与“外部程序”动态交换数据,send() 方法的功能与 next() 函数的功能相似,都用于获取生成器的下一个值,并将生成器“冻结”在 yield 语句处;但 send() 方法可以接收一个参数,该参数值会被发送给生成器函数。

在生成器函数内部,通过 yield 表达来获取 send() 方法发送的值,这就要求程序应该使用一个变量来接收 yield 语句的值。如果仍然使用 next() 函数获取生成器所生成的下一值,那么 yield 语句返回 None。总结起来就是两点:
(1)、外部程序通过 send() 方法发送数据。
(2)、生成器函数使用 yield 语句接收数据。

要说明的是,只有等到生成器被“冻结”后,外部程序才能使用 send() 方法向生成器发送数据。要获取生成器第一次生成的值,应使用next() 函数;如果使用 send() 方法获取生成器第一次生成的值,也不能向生成器发送数据,只能为该方法传入 None 参数。

下面代码使用 send() 方法向生成器发送数据,依次计算每个整数的平方值,当生成器接收到外部数据之后会生成外部数据的平方值。示例如下:
 1 def square_gen():
 2     i = 0
 3     out_val = None
 4     while True:
 5         # 使用 yield 语句生成值,使用 out_val 接收 send() 方法发送的参数值
 6         out_val = (yield out_val ** 2) if out_val is not None else (yield i ** 2)
 7         # 如果使用 send() 方法获取下一值,out_val 会获取 send() 方法的参数值
 8         if out_val is not None: print("=====%d" % out_val)
 9         i += 1
10 
11 sd = square_gen()
12 # 第一次调用 send() 方法获取值,只能传入 None 作为参数,传其它参数报 TypeError 错误
13 print(sd.send(None))        # 0
14 print(next(sd))             # 1
15 print("-" * 20)
16 # 调用 send() 获取生成器的下一值,参数 9 会被发送给生成器
17 print(sd.send(8))           # 64
18 # 再次调用 next() 函数获取生成器的下一个值,没有收到外部数数,计算 i 的平方值
19 print(next(sd))             # 9
20 
21 运行代码,输出如下所示:
22 0
23 1
24 --------------------
25 =====8
26 64
27 9
在这段代码中,重要的是下面这行代码:
out_val = (yield out_val ** 2) if out_val is not None else (yield i ** 2)
这行代码中,yield 语句放在 if 表达式中,整个表达式只会返回一个 yield 语句,在 yield 语句左边(就是等号左边)的变量 out_val 用于接收生成器 send() 方法所发送的值。

运行程序后,第一次使用 send() 方法获取生成器的下一个值时,只能为 send() 方法传入 None 参数。当程序执行到 if 表达式处,此时的 out_val 为 None,因此 if 表达式执行 yied i**2,生成器返回0(执行到 i+=1 后 i 的值是 1),并且程序被冻结,此时并未对 out_val 变量赋值,所以第一次获取生成器的值是0。

接下来用 next(sd) 获取生成器的下一个值,程序从“冻结”处(对 out_val 赋值)向下执行。此时调用的是next() 函数获取生成器的下一个值,因此 out_val 被赋值为 None,所以程序执行 yield i**2(生成器返回1),程序再次被“冻结”,此时 i 的值是 2。

紧接着调用 sd.send(8) 获取生成器的下一个值,程序又从“冻结”处(对 out_val 赋值)向下执行。此时 out_val 被赋值为 8,所以程序执行 yield out_val**2(生成器返回64),此时 i 的值是 3。

紧接着再一次调用 next(sd) 获取生成器的下一个值,程序又从“冻结”处(对 out_val 赋值)向下执行。此时 out_val 被赋值为 None,所以执行 yield i**2(此时 i 的值是3),因此生成器返回的是 9,程序再次被“冻结”后,i 的值又增加1,此时 i 等于 4。所以得到上面的输出结果。

此外,生成器还提供了下面两个方法:
(1)、close():该方法用于停止生成器。
(2)、throw():该方法用于在生成器内部(yield语句内)引发一个异常。

生成器的 throw() 方法使用示例如下:
......
sd.throw(ValueError) # 让生成器引发异常 关于这行 throw() 代码的输出如下所示: Traceback (most recent call last): File "send_test.py", line 25, in <module> sd.throw(ValueError) File "send_test.py", line 10, in square_gen out_val = (yield out_val ** 2) if out_val is not None else (yield i ** 2) ValueError
从输出可知,当程序调用生成器的 throw() 方法引发异常后,就在 yield 语句中引发异常。要注意的是,throw() 方法必须要传递一个参数,这个参数通常是异常类型。在代码中将这行 throw() 语句注释掉,增加下面两代码来了解下 close() 方法的用法。close() 方法是被调用后就关闭生成器,此时再去获取生成器的下一个值,就会引发 StopIteration 异常。示例如下:
......
# sd.throw(ValueError)
sd.close() # 关闭生成器 print(next(sd)) # StopIteration 运行代码,会得到如下的异常输出: Traceback (most recent call last): File "send_test.py", line 28, in <module> print(next(sd)) StopIteration
posted @ 2019-11-07 15:53  远方那一抹云  阅读(427)  评论(0编辑  收藏  举报