python中的协议和抽象基类

从协议到抽象基类

看一个序列协议的例子

""" 定义__getitem__方法,只实现序列协议的一部分,
这样就足够访问元素,迭代和使用in运算符"""
>>> class Foo:
... 	def __getitem__(self, pos):
... 		return range(0, 30, 10)[pos]
...
>>> f = Foo()
>>> f[1]
10
>>> for i in f:
		print(i)
...
0
10
20
>>> 20 in f
True
>>> 15 in f
False

虽然没有__iter__ 方法,但是Foo实例是可迭代的对象,因为发现有__getitem__ 方法时,Python 会调用它,传入从0开始的整数索引,尝试迭代对象(这是一种后备机制)。尽管没有实现__contains__ 方法,但是Python足够智能,能迭代Foo 实例,因此也能使用in运算符:Python 会做全面检查,看看有没有指定的元素

猴子补丁

看一个使用序列协议实现的FrenchDeck类

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 suit in self.suits
                        for rank in self.ranks]
                        
    def __len__(self):
        return len(self._cards)
        
    def __getitem__(self, position):
        return self._cards[position]

# shuffle问题
>>> from random import shuffle
>>> l = list(range(10))
>>> shuffle(l)
>>> l
[5, 2, 9, 7, 8, 3, 1, 4, 0, 6]
>>> deck = FrenchDeck()
>>> shuffle(deck)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File ".../python3.3/random.py", line 265, in shuffle
x[i], x[j] = x[j], x[i]
TypeError: 'FrenchDeck' object does not support item assignment

deck不能进行shuffle。这个问题的原因是,shuffle函数要调换集合中元素的位置,而FrenchDeck只实现了不可变的序列协议。可变的序列还必须提供__setitem__ 方法

>>> def set_card(deck, position, card): 
...     deck._cards[position] = card
...
>>> FrenchDeck.__setitem__ = set_card 
"""random.shuffle 函数不关
心参数的类型,只要那个对象实现了部分可变序列协议即可"""
>>> shuffle(deck) 
>>> deck[:5]
[Card(rank='3', suit='hearts'),
 Card(rank='4', suit='diamonds'), 
 Card(rank='4',suit='clubs'), 
 Card(rank='7', suit='hearts'),
 Card(rank='9', suit='spades')]

这里的关键是,set_card 函数要知道deck 对象有一个名为_cards 的属性,而且_cards 的值必须是可变序列。然后,我们把set_card 函数赋值给特殊方法 __setitem__,从而把它依附到FrenchDeck 类上。这种技术叫猴子补丁:在运行时修改类或模块,而不改动源码。
猴子补丁很强大,但是打补丁的代码与要打补丁的程序耦合十分紧密,而且往往要处理隐藏和没有文档的部分。

抽象基类

抽象基类的本质就是几个特殊方法

class Struggle:
    def __len__(self):
        print(str(self.__class__))
        return 23
        
from collections import abc
isinstance(Struggle(), abc.Sized)
len(Struggle())  # 23

可以看出,无需注册,abc.Sized 也能把Struggle识别为自己的子类,只要实现了特殊方法__len__ 即可

import collections
Card = collections.namedtuple('Card', ['rank', 'suit'])

# 把FrenchDeck2声明为collections.MutableSequence的子类
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 suit in self.suits
                        for rank in self.ranks]
    def __len__(self):
        return len(self._cards)
        
    def __getitem__(self, position):
        return self._cards[position]
        
    # support shuffle
    def __setitem__(self, position, value): 
        self._cards[position] = value
        
    """ 继承MutableSequence 的类必须实现__delitem__方法,
     这是MutableSequence 类的一个抽象方法"""
    def __delitem__(self, position): 
        del self._cards[position]
    
    # 还要实现insert 方法,这是MutableSequence 类的第三个抽象方法
    def insert(self, position, value): 
        self._cards.insert(position, value)
标准库中的抽象基类

https://docs.python.org/3/library/collections.abc.html#collections-abstract-base-classes
对各个抽象基类做了总结,说明了相互之间的关系,以及各个基类提供的抽象方法和具体方法

自定义抽象基类
import abc
# 自己定义的抽象基类要继承abc.ABC
class Tombola(abc.ABC): 
    """抽象方法使用@abstractmethod 装饰器标记,
    而且定义体中通常只有文档字符串"""
    @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: 
            try:
                items.append(self.pick())
            except LookupError:
                break
        self.load(items) 
        return tuple(sorted(items))
# 不符合要求的子类无法蒙混过关
>>> from tombola import Tombola
>>> class Fake(Tombola): 
... 	def pick(self):
... 	return 13
...
>>> Fake 
<class '__main__.Fake'>
>>> f = Fake() 
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class Fake with abstract methods load

抽象基类的语法

  1. 声明抽象基类最简单的方式是继承abc.ABC 或其他抽象基类
  2. 在@abstractmethod 和def 语句之间不能有其他装饰器
  3. 声明抽象基类的推荐方式
class MyABC(abc.ABC):
	@classmethod
	@abc.abstractmethod
	def an_abstract_classmethod(cls, ...):
	pass

定义Tombola抽象基类的子类

import random
from tombola import Tombola
"""BingoCage 实现了所需的抽象方法load 和pick,从Tombola
中继承了loaded 方法,覆盖了inspect 方法,还增加了__call__ 方法
"""
class BingoCage(Tombola): 
    def __init__(self, items):
        self._randomizer = random.SystemRandom() 
        self._items = []
        self.load(items) 
        
    def load(self, items):
        self._items.extend(items)
        self._randomizer.shuffle(self._items) 
        
    def pick(self): 
        try:
            return self._items.pop()
        except IndexError:
            raise LookupError('pick from empty BingoCage')
    def __call__(self): 
        self.pick()
        
"""BingoCage 从Tombola 中继承了耗时的loaded 方法和笨拙的inspect 方法。
这两个方法都可以覆盖,下面中速度更快的一行代码。
这里想表达的观点是:我们可以偷懒,直接从抽象基类中继承不是那么理想的具体方法
"""
import random
from tombola import Tombola
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)) 
        except ValueError:
            raise LookupError('pick from empty LotteryBlower')
        return self._balls.pop(position) 
    
    # 覆盖loaded方法
    def loaded(self): 
        return bool(self._balls)
    
    # 覆盖inspect方法
    def inspect(self): 
        return tuple(sorted(self._balls))

Tmobola的虚拟子类
注册虚拟子类的方式是在抽象基类上调用register 方法。这么做之后,注册的类会变成抽象基类的虚拟子类,而且issubclass 和isinstance 等函数都能识别,但是注册的类不会从抽象基类中继承任何方法或属性。
这样做时,我们保证注册的类忠实地实现了抽象基类定义的接口,而Python 会相信我们,从而不做检查。如果我们说谎了,那么常规的运行时异常会把我们捕获。

from random import randrange
from tombola import Tombola

@Tombola.register 
# 把Tombolist 注册为Tombola 的虚拟子类
class TomboList(list): 
    # Tombolist 扩展list
    def pick(self):
        if self: 
            position = randrange(len(self))
            return self.pop(position) 
        else:
            raise LookupError('pop from empty TomboList')
    # Tombolist.load 与list.extend 一样
    load = list.extend 
    
    def loaded(self):
        return bool(self) 
        
    def inspect(self):
        return tuple(sorted(self))
        
>>> issubclass(TomboList, Tombola)
True
>>> t = TomboList(range(100))
>>> isinstance(t, Tombola)
True

然而,类的继承关系在一个特殊的类属性中指定__mro__,即方法解析顺序(Method Resolution Order)。这个属性的作用很简单,按顺序列出类及其超类,Python 会按照这个顺序搜索方法。
查看TomboList 类的_mro_ 属性,你会发现它只列出了“真实的”超类,即list 和object

>>> TomboList.__mro__
(<class 'tombolist.TomboList'>, <class 'list'>, <class 'object'>)

python使用register的方式
在collections.abc 模块的源码中, 是这样把内置类型tuple、str、range 和
memoryview 注册为Sequence 的虚拟子类的:

Sequence.register(tuple)
Sequence.register(str)
Sequence.register(range)
Sequence.register(memoryview)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值