【Python】【元编程】【从协议到抽象基类】

时间:2023-03-09 12:46:23
【Python】【元编程】【从协议到抽象基类】
"""
class Vector2d:
typecode = 'd' def __init__(self,x,y):
self.__x = float(x)
self.__y = float(y)
@property
def x(self):
return self.__x
@property
def y(self):
return self.__y
def __iter__(self):
return (i for i in (self.x,self.y))
from random import shuffle l = list(range(10))
shuffle(l)
print(l) #[1, 7, 0, 8, 5, 2, 9, 4, 6, 3]
tuple1 = (1,2)
#shuffle(tuple1)
'''
Traceback (most recent call last):
File "C:/Users/wangxue1/PycharmProjects/fluentPython/fromxieyiToChouxiangjilei/__init__22.py", line 21, in <module>
shuffle(tuple1)
File "C:\Python36\lib\random.py", line 274, in shuffle
x[i], x[j] = x[j], x[i]
TypeError: 'tuple' object does not support item assignmen
'''
#这个错误信息相当明确。问题原因是,shuffle函数要调换集合中元素的位置,而tuple只实现了不尅版序列协议。可变的序列还必须提供__setitem__方法。
#Python是动态语言,因此我们可以在运行时修正这个问题,甚至还可以在交互式控制台中,
#栗子11-6 为FrenchDeck打猴子补丁(运行时修改,不改源码),把他变成可变的,让random.shuffle函数能处理
from random import shuffle
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 set_card(deck,position,card):
deck._cards[position] = card FrenchDeck.__setitem__ = set_card
deck = FrenchDeck()
shuffle(deck)
print(deck[:5])
"""
#11.5 定义抽象基类的子类
#例子11-8 FrenchDeck2,collections.MutableSequence的子类
import collections
Card = collections.namedtuple('Card',['rank','suit']) class FrenchDeck2(collections.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 __setitem__(self, key, value): #为了支持洗牌,只需支持这个方法
self._cards[key] = value
def __delitem__(self, key): #继承MutableSequence必须实现这个方法,这是基类的一个抽象方法
del self._cards[key]
#def insert(self, index, value): #这是继承的类的第三个抽象方法
# self._cards.insert(index,value)
#
#a = FrenchDeck2() #如果将上方任意一个实现的基类的抽象方法注释掉,则会报错;TypeError: Can't instantiate abstract class FrenchDeck2 with abstract methods insert。
#导入时(加载并编译),python不会检查抽象方法的实现,在运行时实例化FrenchDeck2类时才会真正检查。
#因为这个原因,即使FrenchDeck2类不需要__delitem__和insert提供的行为,也要实现,因为MulableSequence抽象基类需要它们
#要想实现子类,可以覆盖从抽象基类中继承的方法,以更高效的方式重新实现。例如,__contains__方法会全面扫描序列,可是,如果你定义的序列按顺序保存元素,那就可以重新定义__contains__方法,使用bisect函数做二分查找,从而提升搜索速度 #11.6 标准库中的抽象基类
#大多数抽象基类在collections.abc模块中定义,不过其他地方也有。例如,numbers和io包中有一些抽象基类。但是,前者更常用
#11.6.1 collections.abc模块中的抽象基类,这个模块中定义了16个抽象基类
#简述这16个抽象基类
#Iterable Container Sized :各个集合应该继承这三个抽象基类,或者至少实现兼容的协议。Iterable通过__iter__方法支持迭代,Container通过__contains__方法支持in运算符、Sized痛殴__len__方法支持len()函数
#Sequence Mapping Set:主要是不可变类型,而且各自都有可变的子类。MutableSequence MutableMapping MutableSet
#MappingView :映射方法 .item() .keys() .values()返回的对象分别是ItemsView KeysView ValuesView 。前两个类还从Set 类继承了丰富的接口
#Callable Hashble :这两个抽象类与集合没有太大关系,只不过因为Collections.abc是标准库中定义抽象基类的第一个模块,而它们又太重要了,因此才把他们放在Collections.abc模块中。这两个主要为内置函数isinstance提供支持,以一种安全
#...的方式判断对象能不能被调用或散列。
# Iterator :注意他是Iterable 的子类。
#11.6.2 抽象基类的数字塔
#numbers包定义的数字塔,从塔尖到塔底分别是:Number Complex Real Rational Integral
#想要检查一个数是不是整数,可以使用isinstance(x,numbers.Integral),这样代码就能接收int bool (int的子类),或者外部库使用numbers抽象基类注册的其他类型。
#若要检查一个数是不是浮点数,可以使用isinstance(x,numbers.Real)。这样代码就能接收bool int float fractions.Fraction ,或者外部库(NumPy,它作了注册)提供的非复数类型
#...decimal.Decimal没有注册为numbers.Real的虚拟子类,这有点奇怪。没注册的原因是,如果你的程序需要Decimal的精度,要防止与其他低精度数字类型混淆,尤其是浮点数 #下面我们将从零开始实现一个抽象基类,然后实际使用,以此实践白鹅类型呢。这么做的目的不是鼓励每个人都立即开始定义抽象基类,而是教授怎么阅读标准库和其他包中的抽象基类源码 #11.7 定义并使用一个抽象基类
#例子11-9 抽象类,有两个抽象方法和两个具体方法
import abc class Tombola(abc.ABC): #自己定义的抽象类要继承abc.ABC
@abc.abstractmethod
def load(self,iterable): #抽象方法,定义体中通常只有文本字符串
"""从可迭代对象中添加元素""" @abc.abstractmethod
def pick(self):
"""随机删除元素,然后将其返回 如果实例为空,应抛出LookupError
""" def loaded(self):
"""如果至少有一个元素,应该返回True,否则返回False"""
return bool(self.inspect()) #抽象基类中的具体方法只能一来抽象基类定义的接口(即只能使用抽象基类中的其他具体方法、抽象方法或特性) def inspect(self):
"""返回一个有序元祖,由当前元素构成"""
items = []
while True: #我们不清楚具体子类如何存储元素,不过为了得到inpect结果,可以不断调用.pick(),把Tombla清空......
try:
items.append(self.pick())
except LookupError:
break
self.load(items) #......然后再使用.load()把所有元素放进去
return tuple(sorted(items))
#抽象方法可以有实现代码,即便实现了,子类也必须覆盖抽象方法,但是在子类中可以使用super()函数调用抽象方法,为它添加功能,而不是从头开始实现。
#这里的.inspect()实现的方式有些笨拙,不过此例是像强调抽象基类可以提供具体方法,只要一来接口中的其他方法就系那个。
#.loaded()方法没那么笨拙,但是耗时,
#选择使用LookupError异常的原因,在python的异常层次关系中,它与IndexError 和 KeyError有关,这两个是具体实现Tombola所用的数据结构最有可能抛出的异常。据此,实现代码可能会抛出LookupError IndexError KeyError.
#...后两者是前者的子类,所以直接抛出LookupError
#IndexError是LookupError的子类,尝试从序列中获取索引超过最后位置的元素时抛出
#使用不存在的键从映射中获取元素时,抛出KeyError
#除了@abstractmethod之外,abc模块还定义了@abstractclassmethod @abstractstaticmethod @abstractproperty 三个装饰器。然而,后三个装饰器从python3.3起废弃了,因为装饰器可以在@abstractmethod上堆叠,那三个就显得多余了。
#...例如,声明抽象类方法的推荐方式
'''
class MyABC(abc.ABC):
@classmethod
@abc.abstractmethod
def an_abstract_classmethod(cls,...):
pass
'''
#在函数上堆叠装饰器的顺序通常很重要,和其他方法描述符仪器使用时,abstractmethod()应放在最里层,也就是说,在@abstractmethod和def语句中间不能有其他装饰器 #11.7.2 定义Tombola抽象基类的子类
#;哦走11-12 BingoCage是Tomlola的具体子类
import random
class BingoCage(Tombola):
def __init__(self,items):
self._randomizer = random.SystemRandom() #假设我们将在线上游戏中使用这个。random.SystemRandom 使用os.urandom()函数实现random API。os.urandom()生成"适合用于加密"的随机字节序列
self._items= []
self.load(items) def load(self,items):
self._items.extend(items)
self._randomizer.shuffle(self._items) # 没有使用random.shuffle()函数,而是使用SystemRandom实例的.shuffle()方法 def pick(self):
try:
return self._items.pop()
except IndexError:
raise LookupError('pick from empty BingoCage') def __call__(self):
self.pick() #例子11-13 LotterBlower是Tombola的具体子类,覆盖了inspect 和 loaded方法
class LotteryBlower(Tombola):
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)) #如果范围为空,random.randrange()函数抛出ValueError,为了兼容Tombola,我们捕获它,抛出LookupError
except ValueError:
raise LookupError('pick from empty LotterBlower')
return self._balls.pop(position)
def loaded(self): #覆盖loaded方法,避免调用inspect方法。可以直接处理self._balls而不必构建整个有序元祖,从而提升速度
return bool(self._balls) def inspect(self):
return tuple(sorted(self._balls))
#上方例子,有个习惯做法值得指出:在__init__方法中,self._balls保存的是list(iterable),而不是iterable的引用(即没有直接把iterable赋值给self._balls),前面说过,这样使这个类更灵活,因为iterable参数可以是任何可迭代的类型
#...把元素存入列表中还能确保取出元素。就算iterable参数之中传入列表,list(iterable)会创建参数的副本。这依然是好的做法,因为我们要从中删除元素,而客户可能不希望自己提供的列表被修改。 #11.7.3 Tombola的虚拟子类
#白鹅类型的一个基本特性:即便不继承,也有办法把一个类注册为抽象基类的虚拟子类。这样做时,我们保证注册的类忠实的实现了抽象基类定义的接口,而python会相信我们,从而不做检查。如果我们说谎了,那么常规的运行实践异常会把我们捕获
#注册虚拟子类是在抽象基类上调用register方法。这么做之后,注册的类会变成抽象基类的虚拟子类,而且issbuclass isinstance 等函数都能识别,但是注册的类不会从抽象基类中继承任何方法或属性
#虚拟子类不会继承注册的抽象基类,而且任何时候都不会检查它是否符合抽象基类的接口,即便在实例化时也不会检查。为了避免运行时错误,虚拟子类要实现所需的全部方法
#例子11-14
from random import randrange
@Tombola.register
class TomboList(list):
def pick(self):
if self: #从list中继承__bool__方法,列表不为空时返回True
position = randrange(len(self))