<jupyter_start><jupyter_text>接口：从协议到抽象基类Python中仅实现部分接口通常是可以接收的，例如若一个类实现了\_\_getitem\_\_()方法，则该类的实例是可迭代的对象，而并不一定需要实现\_\_iter\_\_以及\_\_contains\_\_方法。Python的抽象基类则不同于上述灵活的应用，抽象基类有严格规定以及类型检查 Python中的接口和协议“我们把协议定义为非正式的接口，是让Python这种动态类型语言实现多态的方式”对于Python来说，只要在自定义类中实现特定方法或者继承特定属性就能够实现多态所描述的：一个接口，多种实现。接口：“对象公开方法的子集，该子集让对象在系统中扮演特定的角色”。一个类可能实现多个接口，从而让实例扮演多个角色。协议仅是非正式的接口，Python中的协议并不能像正式接口那样施加限制，同时Python允许一个类仅实现部分接口 —— KISS原则 Python对序列的偏爱正常来说，为了实现类实例中元素的迭代，需要实现\_\_iter\_\_方法；而为了支持“in”关键字，则需要实现\_\_contains\_\_方法。但是在仅实现\_\_getitem\_\_方法的情况下，Python也能正常实现元素的迭代和对“in”关键字的支持。并且由于Python本身的特性，这一自定义类也无需继承序列抽象基类abs.Sequence。这些特性正好体现了Python对序列类型（或者说对迭代这一操作）的偏爱，即Python解释器会尝试调用多种方法，以支持迭代操作。 Python神奇的动态性Python的动态性体现在方方面面，Python不仅允许在程序运行过程中为一个对象创建新属性，或者改变一个类的类属性，甚至允许为类的行为“打补丁” —— 动态实现类的方法以支持某种协议、接口或者应用要求。下述是一个例子下述的TestSeq类实现了\_\_getitem\_\_以及\_\_len\_\_方法，以支持最基础的序列操作。若想实现元素的打乱，即支持random.shuffle，还需要实现\_\_setitem\_\_方法，即支持可变的序列协议。下述例子中\_\_setitem\_\_方法是在程序运行过程中实现的。这一处理方法也被称为猴子补丁（Monkey Patch）。这一例子也反映了协议的动态性，在这个例子种shuffle不关心TestSeq如何实现的\_\_setitem\_\_方法，也不关心传入的参数类型，只要TestSeq实现了shuffle接口要求的方法即可。<jupyter_code>import random
random.seed(1024)

class TestSeq:

    def __init__(self, component):

        self._component = component
    
    def __getitem__(self, position):
        return self._component[position]
    
    def __len__(self):
        return len(self._component)

def set_item(obj, position, value):
    obj._component[position] = value

test_list = [1, 2, 3, 4, 5]
test_seq = TestSeq(test_list)
print(test_seq[:5])

# 动态注册__setitem__方法
TestSeq.__setitem__ = set_item

random.shuffle(test_seq)
print(test_seq[:5])<jupyter_output>[1, 2, 3, 4, 5]
[5, 3, 2, 4, 1]<jupyter_text>“表型”和“支序”鸭子类型实际上是反映了“表型”的观点，即从行为上判断多个实例是不是同一个类的实例。基于这一观点，在实际应用过程中，某一特定对象是哪个类的实例并不是非常重要的问题，更重要的是这个对象能不能实现特定的行为。在为类添加功能的过程中，有时会导致毫不相关的两个类有相似的方法和接口，但是并不能保证分属于不同类的相同方法有相类似的语义。实际上我们总是期望相同方法具有语义上的相似性。例如对于repr和print，我们期望repr输出有助于程序调试的信息（例如当前对象的类名），print输出用户友好的信息（当前对象存储的信息）。假设在实现一个自定义类时巧合对调了这两个方法的具体实现，毫无疑问，repr和print依然能正常使用，即行为上和内置类完全一致，但是语义上不一致 —— 这会导致使用中的困扰。上述问题实际上反映了仅关注“表型”的缺陷。另一方面，我们可以期待继承自同一抽象基类的多个类的同一方法不仅有相同的接口，更是有相似的语义。因此，对类的继承关系进行检查或许是不错的想法。这一思路反映了“支序”的观点。Python中使用isinstance和issubclass执行这一想法，<NAME>建议仅对检查是否继承自抽象基类时才使用这一语法。其理由是这样更灵活 —— 若当前测试的类没有继承自抽象基类，有多种方法可以让这个类的实例通过类型测试  定义抽象基类的子类继承自抽象基类的类需要实现特定方法。但是Python在加载时并不会检查这个类是否有实现特定方法，取而代之的是在实例化时进行检查。若没有实现特定方法则会抛出TypeError。下面假设上述定义的TestSeq继承自抽象基类collections.MutableSequence，则有如下实现。<jupyter_code>from collections import MutableSequence

import random
random.seed(1024)

class TestSeq(MutableSequence):

    def __init__(self, component):
        self._component = list(component)
    
    def __getitem__(self, position):
        return self._component[position]
    
    def __len__(self):
        return len(self._component)
    
    def __setitem__(self, position, value):
        """
        collections.MutableSequence 要求实现的方法
        而TestSeq并需要这个类的功能
        """
        self._component[position] = value

    def __delitem__(self, position):
        """
        collections.MutableSequence 要求实现的方法
        而TestSeq并需要这个类的功能
        """
        del self._component[position]
    
    def insert(self, position, value):
        """
        collections.MutableSequence 要求实现的方法
        而TestSeq并需要这个类的功能
        """
        self._component.insert(position, value)

def set_item(obj, position, value):
    obj._component[position] = value

test_list = [1, 2, 3, 4, 5]
test_seq = TestSeq(test_list)
print(test_seq[:5])

random.shuffle(test_seq)
print(test_seq[:5])<jupyter_output>[1, 2, 3, 4, 5]
[5, 3, 2, 4, 1]<jupyter_text>标准库中的抽象基类 collections模块中的抽象基类Python的collections模块提供了一些抽象基类。本书提供了一个非常不错的UML图，用以表示该模块中各个抽象基类之间的关系。值得注意的是，本书给出的UML图和[文档](https://docs.python.org/3/library/collections.abc.htmlcollections-abstract-base-classes)中的继承关系有出入，但是大体上是一致的。* Iterable、Container和Sized这三个类提供了非常基础的协议。其中，Iterable提供了\_\_iter\_\_方法以支持迭代，Container提供了\_\_contains\_\_方法以支持in运算符，Sized提供了\_\_len\_\_方法以支持len()方法。* CollectionCollection类在本书中并未提及，但是在文档中有提及，并且起到“承上启下”的作用。具体来说，这个抽象基类继承自Iterable、Container和Sized，即整合了上述基础的三个类。下述各类如有需要实现上述三个协议，实际上都是继承自Collection，而不是直接继承自Iterable、Container和Sized。* Sequence、Mapping和Set这三个类是不可变集合。分别实现了三类常用类型，并且这三类均有可变的子类 —— MutableSequence、MutableMapping以及MutableSet。* MappingView视图类主要是创建一个动态随源数据改变而改变的数据结构。MappingView有三个重要的子类，其分别是ItemsView、KeysView以及ValuesView。映射方法.items()、.keys()以及.values()分别返回这三个类的实例。其中，ItemsView和KeysView还从set类继承了大量方法，ValuesView则仅另外继承自Collection。* Callable和Hashable这两个抽象基类相当的“孤立”，一方面既不继承自其他类，另一方面也很少被其他类继承。Callable类提供了\_\_call\_\_方法，Hashable类提供了\_\_hash\_\_方法。乍一看，这两个方法很常用，但是在使用中通常不会主动声明继承自这两个类以支持相应的方法。只要实现相应的方法，Python就能够将自定义的类识别为对应抽象基类的子类。例如对于Callable，一个自定义类只要实现了\_\_call\_\_方法，就能够通过isinstance的类型检查。这一特点源于\_\_subclasshook\_\_方法，具体细节可以参考[子类检查](https://hg.python.org/cpython/file/3.4/Lib/abc.py1194)，\_\_subclasscheck\_\_方法会调用\_\_subclasshook\_\_方法进行子类检查。* IteratorIterator继承自Iterable，在Iterable的基础上，Iterator添加了\_\_next\_\_方法。* 其他除了本书本章介绍的抽象基类外，从文档中可以得知collections模块中还有一些其他抽象基类。例如用于支持序列反转\_\_reversed\_\_方法的Reversible类，用于支持async/await语法的Awaitable、AsyncIterable、AsyncIterator以及AsyncGenerator等抽象基类。<jupyter_code>class TestCLS:

    def __len__(self): 
        return 1
    
    def __call__(self):
        return self.__len__()

from collections.abc import Sized, Callable

print("TestCLS类是否是Sized的子类？", isinstance(TestCLS(), Sized))
print("TestCLS类是否是Callable的子类？", isinstance(TestCLS(), Callable))<jupyter_output>TestCLS类是否是Sized的子类？ True
TestCLS类是否是Callable的子类？ True<jupyter_text>numbers中的抽象基类[numbers](https://docs.python.org/3/library/numbers.html)中的抽象基类均是和数字相关的类。相较于collections中的抽象基类，numbers中的抽象基类层次关系更为明显，因而被称为数字塔（The numeric tower）：* Number* Complex* Real* Rational* Integral上述结构层次比较明显，其中唯一有些疑惑的是Real和Integral中间的Rational（对于精度有限的计算机来说，真的有必要区分有理数和无理数吗）。在Real的基础上，Rational类进一步添加了numerator和denominator属性用于构造分式。  自定义抽象基类本书自定义了一个实现如下功能的抽象基类：随机从有限集合中挑选元素并且选出的物体没有重复，直到所有的元素被选完。本书将这一自定义抽象基类命名为Tombola，并且具有两个抽象方法以及两个具体方法：* .load()：把元素放入容器* .pick()：从容器中随机拿出一个元素并返回该元素* .loaded()：如果容器中至少有一个元素，则返回True* .inspect()：返回一个有序容器，由容器中的现有元素构成，不会修改容器的内容（对于本方法，在实际应用中，对于同一个容器，希望每次调用这个方法会返回相同的序列）这一抽象基类定义如下。下述定义中，loaded方法依赖于inspect方法，而inspect方法则依赖于抽象方法load和pick。对于这一抽象基类的子类，load和pick方法必然会实现，因此inspect方法必然能够正常工作，否则用户应当检查load方法和pick方法的实现。不同版本的Python使用不同的方法建立自定义的抽象基类。* python 3.4及以后：继承自abc.ABC -> class Tombola(abc.ABC)* python 3.4之前：class Tombola(metaclass=abc.ABCMeta)* python 2：\_\_metaclass\_\_ = abs.ABCMeta<jupyter_code>import abc

class Tombola(abc.ABC):
    
    @abc.abstractmethod
    def load(self, iterable):
        """
        从可迭代对象中添加元素
        """
    
    @abc.abstractmethod
    def pick(self):
        """
        随机选择元素并返回
        """
    
    def loaded(self):
        """
        委托给inspect返回的元组
        """
        return bool(self.inspect())
    
    def inspect(self):
        """
        返回一个由当前元素构成的有序元组
        """
        items = []
        while True:
            try:
                items.append(self.pick())
            except LookupError:
                break
        self.load(items)

        # sorted 保证每次返回的序列相同
        return tuple(sorted(items))<jupyter_output><empty_output><jupyter_text>自定义抽象基类的子类Tombola要求其子类实现load方法以及pick方法。本章实现了Tombola类的两个子类，分别为BingoCage和LotteryBlower。BingoCage实现了load和pick，此外还添加了\_\_init\_\_和\_\_call\_\_。LotteryBlower实现了load和pick，并根据实际情况覆写了loaded和inspect（具体来说，将相关方法的具体实现委托给list）。除了上述两个子类外，本章还定义了一个Tombola的虚拟子类 —— TomboList。TomboList不会从Tombola继承任何方法或属性，但是能够通过issubclass以及isinstance的类型检查。Python不会检查TomboList是否符合Tombola抽象基类的接口，但是实际上要求TomboList符合Tombola的接口，否则在实际应用中会报错。<jupyter_code>import random

class BingoCage(Tombola):

    def __init__(self, items):
        self._randomizer = random.SystemRandom()
        self._items = list()

        # 将初始加载委托给load
        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()

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)
    
    def loaded(self):
        # 委托给list
        return bool(self._balls)
    
    def inspect(self):
        # 委托给list
        return tuple(sorted(self._balls))


@Tombola.register
class TombolaList(list):

    def pick(self):
        if self:
            position = random.randrange(len(self))
            return self.pop(position)
        else:
            raise LookupError("pop from empty TomboList")
        
    load = list.extend

    def loaded(self):
        return bool(self)
    
    def inspect(self):
        return tuple(sorted(self))<jupyter_output><empty_output>
StarcoderdataJupyterStructuredCleanDedup