流畅的python,Fluent Python 第十一章笔记
接口:从协议到抽象基类。
这一章,不知道是翻译问题,还是我能力问题,看的不是很懂,只能简单记录一下自己的理解。一段时间以后再回看了。
首先协议是非正式的接口,每个类(除了抽象基类),都有接口。
受保护的属性与私有属性不在接口中。
接口是对象公开方法的子集,让对象在系统中扮演特定的角色
接口是实现特定角色的方法集合,协议与继承没有关系,一个类可能实现多个接口,从而扮演对个角色。
序列协议是Python最基础的协议之一。
In [29]: from collections.abc import Sequence In [30]: class Foo: ...: def __getitem__(self, pos): ...: return range(0, 30, 10)[pos] ...: In [31]: f = Foo() In [32]: f[1] Out[32]: 10 In [33]: for i in f:print(i) 0 10 20 In [34]: 20 in f Out[34]: True In [35]: isinstance(f, Sequence) Out[35]: False In [36]:
上面定义了__getitem__实现了序列协议的一部分,很明显,它不是Sequence的子类,尽管也没有__iter__与__Container__但还是能被for循环调用,以及能使用in
In [35]: isinstance(f, Sequence) Out[35]: False In [36]: from collections import Iterable /usr/local/bin/ipython:1: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working #!/usr/local/opt/python/bin/python3.7 In [37]: isinstance(f, Iterable) Out[37]: False
记得以前老师说过,能被for循环的调用的都是可迭代对象,很明显,f能被for循环调用,但不是可迭代对象,严谨的还是具有__iter__的才是可迭代对象。
11.3使用猴子布丁实现协议。
猴子补丁以前了解过一下,也就知道这个名字,现在书中很详细的介绍了,也实际使用了。
就是已经实例对象一开始没有这个属性,通过对它实例的类添加属性,然后它就有了这个方法,一般要少用。
In [46]: import collections ...: ...: Card = collections.namedtuple('Card', 'rank suit') ...: ...: class Frenchdeck: ...: ranks = [str(n) for n in range(2, 11)] + list('JQKA') # 把牌的数字与花色赋值给类属性 ...: ...: suits = 'spades diamonds clubs hearts'.split() ...: ...: def __init__(self): # 用列表生成式制作一副牌 ...: self._cards = [Card(rank, suit) for rank in self.ranks ...: for suit in self.suits] ...: ...: def __len__(self): ...: return len(self._cards) ...: ...: def __getitem__(self, item): # 定义这个[]取值会用到。 ...: return self._cards[item] ...: ...: def __repr__(self): ...: return f'{self._cards!r}' ...: In [47]: desk = Frenchdeck() In [48]: desk[:5] Out[48]: [Card(rank='2', suit='spades'), Card(rank='2', suit='diamonds'), Card(rank='2', suit='clubs'), Card(rank='2', suit='hearts'), Card(rank='3', suit='spades')] In [49]: shuffle(desk) --------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-49-1b00429a849a> in <module> ----> 1 shuffle(desk) /usr/local/Cellar/python/3.7.4/Frameworks/Python.framework/Versions/3.7/lib/python3.7/random.py in shuffle(self, x, random) 276 # pick an element in x[:i+1] with which to exchange x[i] 277 j = randbelow(i+1) --> 278 x[i], x[j] = x[j], x[i] 279 else: 280 _int = int TypeError: 'Frenchdeck' object does not support item assignment In [50]: def set_card(deck, position, card): ...: deck._cards[position] = card ...: ...: In [51]: Frenchdeck.__setitem__ = set_card In [52]: shuffle(desk) In [53]: desk[:5] Out[53]: [Card(rank='Q', suit='clubs'), Card(rank='4', suit='diamonds'), Card(rank='10', suit='hearts'), Card(rank='9', suit='spades'), Card(rank='5', suit='diamonds')]
通过对第一章的扑克牌添加猴子布丁,前面由于缺少__setitem__属性,所以shuffle无法对齐进行使用,通过后面添加__setitem__对象实现了部分可变序列的协议既可。
所谓的鸭子类型就是无需关注对象的具体类型,只要实现了特定的协议既可,不需要继承。
11.5定义抽象基类的子类
In [54]: from collections.abc import MutableSequence In [55]: class Mu(MutableSequence): ...: ... ...: In [56]: mu = Mu() --------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-56-c234f3d4f57f> in <module> ----> 1 mu = Mu() TypeError: Can't instantiate abstract class Mu with abstract methods __delitem__, __getitem__, __len__, __setitem__, insert
collections.abc里面有16个抽象基类,这里我继承了MutableSequence,从报错可以看出__delitem__, __getitem__, __len__, __setitem__, insert,属于抽象方法,必须子类重新定义。
书中按照FranchDeck继承MutalbeSquence
import collections.abc Card = collections.namedtuple('Card', 'rank suit') class Frenchdeck2(collections.abc.MutableSequence): ranks = [str(n) for n in range(2, 11)] + list('JQKA') # 把牌的数字与花色赋值给类属性 suits = 'spades diamonds clubs hearts'.split() def __init__(self): # 用列表生成式制作一副牌 self._cards = [Card(rank, suit) for rank in self.ranks for suit in self.suits] def __len__(self): # 继承类的抽象方法 return len(self._cards) def __getitem__(self, item): # 定义这个[]取值会用到。 return self._cards[item] # 继承类的抽象方法 def __delitem__(self, key): # 继承类的抽象方法 del self._cards[key] def __setitem__(self, key, value): # 继承类的抽象方法 self._cards[key] = value def insert(self, index:int, value) -> None: # 继承类的抽象方法 self._cards.insert(index, value) def __repr__(self): return f'{self._cards!r}' if __name__ == '__main__': deck = Frenchdeck2() deck.insert(0, 1)
把所有的抽象方法都定义了,不管你用不用到。
同时你也进程了抽象类MutableSequence中的一些方法,比如remove,pop,extend,__iadd__
In [78]: deck[0] Out[78]: 1 In [79]: deck.pop() Out[79]: Card(rank='A', suit='hearts') In [80]: deck.pop() Out[80]: Card(rank='A', suit='clubs') In [81]: deck.extend('123') In [82]: deck.remove('2')
collections.abc16个基类里面我比较又兴趣的是Callable与Hashable是来可以测试对象是否可以调用与哈希。
In [83]: from collections.abc import Callable, Hashable In [84]: isinstance(print, Callable) Out[84]: True In [85]: isinstance(print, Hashable) Out[85]: True In [86]: isinstance(list, Hashable) Out[86]: True In [87]: isinstance(list(), Hashable) Out[87]: False In [88]: callable(list) Out[88]: True In [89]:
Callable抽象基类可以用callable函数一样的效果,Hashable只能用这个抽象类测试了。
11.6.2 抽象基类的数字塔
numbers的包
官方解释链接:
https://docs.python.org/zh-cn/3.7/library/numbers.html#numbers.Rational.numerator
Number
Complex
Real
Rational (是Intergal的子类)
Integral
In [103]: from numbers import Integral In [104]: isinstance(1,Integral) Out[104]: True In [105]: from fractions import Fraction In [106]: f = Fraction(3,4) In [107]: isinstance(f,Integral) Out[107]: False In [108]: from numbers import Rational In [109]: isinstance(f,Integral) Out[109]: False In [110]: isinstance(f,Rational) Out[110]: True In [111]:
Integral的实例包括int,bool(int的子类)
Rational多了包含分数
11.7定义并使用一个抽象基类。
为了更好的学习抽象基类,书中定义了一个抽象基类,并继承了两个子类,一个虚拟子列。
import abc class Tombila(abc.ABC): @abc.abstractmethod def load(self, iterable): """从可迭代对象添加元素""" @abc.abstractmethod def pick(self): print(type(self).__name__ + 'pick is working') """随机删除元素并返回 如果实例为空,这个方法抛出"LookupError" """ def loaded(self): """如果至少有一个元素,返回True,否则返回False""" return bool(self.inspect()) def inspect(self): """返回一个有序的元祖, 由当前元素构成""" items = [] while True: try: items.append(self.pick()) except LookupError: break self.load(items) # 返回读取 return tuple(sorted(items))
上面的是抽象类,我在pick里面定义了一些功能,继承的时候还是可以通过super调用。
11.7.1抽象基类的语法解释。
Python3.4之后可以这样,就上面写的直接继承abc.ABC就可以了。
3.4之前
class Demo(metaclass=abc.ABCMeta): ...
Python2这样写:
class Demo(object): __metaclass__ = abc.ABCMeta
在Python3.3之前还有:
@abc.abstractclassmethod @abc.abstractproperty @abc.abstractstaticmethod
新在都不用了,根据多层装饰器的原理,你只要把@abstractmethod放在最里层,上面再写上你需要装饰的,同样可以继承给子类。
11.7.2 定义Tombola抽象基类的子类。
先上抽线基类:
import abc class Tombila(abc.ABC): @abc.abstractmethod def load(self, iterable): """从可迭代对象添加元素""" @abc.abstractmethod def pick(self): print(type(self).__name__ + 'pick is working') """随机删除元素并返回 如果实例为空,这个方法抛出"LookupError" """ def loaded(self): """如果至少有一个元素,返回True,否则返回False""" return bool(self.inspect()) def inspect(self): """返回一个有序的元祖, 由当前元素构成""" items = [] while True: try: items.append(self.pick()) except LookupError: break self.load(items) # 返回读取 return tuple(sorted(items)) if __name__ == '__main__': print(dir(Tombila))
下面分别用了三种继承的方式来实现这个功能。
第一种改动最少,两个方法都是继承了基类,第二个改懂比较多,把基类的方法都改了,第三个直接是虚拟子类,不继承基类的任何方法。
# binggo import random from tombola import Tombila class BingoCage(Tombila): def __init__(self, item): self._randomizer = random.SystemRandom() self._items =[] self.load(item) # 调用load方法来实现初始化 def load(self, iterable): self._items.extend(iterable) self._randomizer.shuffle(self._items) def pick(self): try: return self._items.pop() except IndexError: # 没有数据可以弹出报错,接收IndexError,上报Look错误 raise LookupError('pick from empty BingoCage') def __call__(self, *args, **kwargs): # 对象变成可调用的 return self.pick() # 书中没有return,我自己加的,要不然执行对象没有返回值 if __name__ == '__main__': bingo = BingoCage(range(10)) print(bingo()) print(bingo.inspect())
/usr/local/bin/python3.7 /Users/shijianzhong/study/Fluent_Python/第十一章/bingo.py 6 (0, 1, 2, 3, 4, 5, 7, 8, 9) Process finished with exit code 0
第二种继承:
import random from tombola import Tombila class LotteryBlower(Tombila): def __init__(self, iterable): self._balls = list(iterable) def load(self, iterable): self._balls.extend(iterable) def pick(self): try: position = random.randrange(len(self._balls)) # 内部随机选一个数字 return self._balls.pop(position) except ValueError: # 如果len里面为0,ValueError: empty range for randrange() raise LookupError('pick from empty LotteryBlower') # 抓取前面的错误返回需要的错误 def loaded(self): return bool(self._balls) def inspect(self): return tuple(sorted(self._balls)) if __name__ == '__main__': lottery = LotteryBlower(range(10)) print(lottery.pick()) print(lottery.inspect())
/usr/local/bin/python3.7 /Users/shijianzhong/study/Fluent_Python/第十一章/lotto.py 4 (0, 1, 2, 3, 4, 5, 6, 7, 8, 9) Process finished with exit code 0
11.7.3 Tombola的虚拟子类
虚拟子类不会继承注册的抽象基类,换句话说,直接点,就是虚拟子类跟抽象基类其实一点关系都没有。
你可以不继承或者修改抽象基类的抽象方法,但为了避免运行错误,没有继承么,所以要把基类的所有方法重写一遍。
先上一个极端的不继承任何基类的例子。
from random import randrange from tombola import Tombila class A: ... Tombila.register(A) a = A() print(isinstance(a, Tombila)) print(issubclass(A, Tombila))
/usr/local/bin/python3.7 /Users/shijianzhong/study/Fluent_Python/第十一章/tombolist.py True True Process finished with exit code 0
可以看到代码,虚拟子类的效果。
这是书中的代码:
from random import randrange from tombola import Tombila @Tombila.register class TomboList(list): def pick(self): # self就是列表本身了 if self: position = randrange(len(self)) return self.pop(position) else: raise LookupError('pop from empty Tombolist') load = list.extend # load 直接等于list.extend方法 def loader(self): return bool(self) def inspect(self): return tuple(sorted(self)) # Tombila.register(TomboList) 这是Python3.3之前的写法 if __name__ == '__main__': tombo = TomboList(range(10)) print(tombo.pick()) print(tombo.inspect()) print(tombo.load(range(3))) # 等同与tombo.extend(range(3)) print(tombo.inspect()) print(TomboList.__mro__)
/usr/local/bin/python3.7 /Users/shijianzhong/study/Fluent_Python/第十一章/tombolist.py 1 (0, 2, 3, 4, 5, 6, 7, 8, 9) None (0, 0, 1, 2, 2, 3, 4, 5, 6, 7, 8, 9) (<class '__main__.TomboList'>, <class 'list'>, <class 'object'>) Process finished with exit code 0
可以看出来,再__mro__里面没有看到基类Tombila。
我后来想了一下,如果基类与list方法没有重名(本来也没有),可以双继承。
from random import randrange from tombola import Tombila class TomboList(Tombila,list): def pick(self): # self就是列表本身了 if self: position = randrange(len(self)) return self.pop(position) else: raise LookupError('pop from empty Tombolist') load = list.extend # load 直接等于list.extend方法 def loader(self): return bool(self) def inspect(self): return tuple(sorted(self)) # Tombila.register(TomboList) 这是Python3.3之前的写法 if __name__ == '__main__': tombo = TomboList(range(10)) print(tombo.pick()) print(tombo.inspect()) print(tombo.load(range(3))) # 等同与tombo.extend(range(3)) print(tombo.inspect()) print(TomboList.__mro__)
/usr/local/bin/python3.7 /Users/shijianzhong/study/Fluent_Python/第十一章/tombolist_1.py 9 (0, 1, 2, 3, 4, 5, 6, 7, 8) None (0, 0, 1, 1, 2, 2, 3, 4, 5, 6, 7, 8) (<class '__main__.TomboList'>, <class 'tombola.Tombila'>, <class 'abc.ABC'>, <class 'list'>, <class 'object'>) Process finished with exit code 0
这样也不是为一种方法,但虚拟基类后面书中介绍了,Python中的一些用法。
11.8 Tombola子类的测试方法。
测试用的doctest模块,我没怎么用过,所以也不写了,包括代码中有用到查寻虚拟子类。
Tombila._abc_registry
但我的运行失败,报错没有这个属性。
11.9 Python使用register的方式。
Python会把tuple,str,range,memoryview注册为虚拟子类。
Squence.regiser(tuple)
....
包括字典这种,也会注册给MutableMapple
MutableMapple.register(dict)
11.10 鹅的行为由可能像鸭子
class Sruggle: def __len__(self): return 23 print(issubclass(Sruggle, Sized)) print(Sized.__subclasscheck__(Sruggle))
返回的都是true
这个Sruggle不是SIzed的虚拟子类,更不可能是子类。
但因为Sized有__subclasscheck__特殊的类方法。
下面我按照上面的代码抄写的Sized的原码:
import abc class Sized(abc.ABC): __slots__ = () @abc.abstractmethod def __len__(self): return 0 @classmethod def __subclasshook__(cls, C): if cls is Sized: if any("__len__" in B.__dict__ for B in C._mro__): return True return NotImplemented
书中没有介绍issubclass运行的原理,我认为如果有__subclasshook__应该会优先调用该类方法。
但一般__subclasshook__用很少,自己用的机会更加少。