python抽象基类

接口和协议

Python没有Interface关键字,而且除了抽象基类,每个类都有接口:类实现或继承的公开属性,包括特殊方法(如__getitem__或__add__)。按照约定,受保护的属性和私有属性不在接口中(即使受保护属性也只是采用命名约定实现的;私有属性也可以轻松的访问)不要违背这些约定。

接口实用的补充定义:对象公开方法的子集,让对象在系统中扮演特定角色。python文档中的"文件类对象"和"可迭代对象"就是这个意思,这种说法指的不是特定的类。一个类可能会实现多个借口,从而让实例扮演多个角色。

协议:协议是接口,但不是正式的(只由文档和约定定义),因此协议不能像正式接口那样施加限制。对象实现了协议某些部分,那个对象就叫做xxx类对象。其实对于Python,"X类对象","X协议","X接口"是同一个意思。

序列协议

序列协议是Python最基础的协议之一,即使对象只实现了那个协议最基本的部分,解释器也会负责任地处理。

抽象基类Sequence正式接口:

Foo类定义了__getitem__方法,只实现了序列协议的一部分,却能够访问,迭代和使用in运算符:

class Foo:
    def __getitem__(self, item):
        return range(0, 30, 10)[item]

if __name__ == '__main__':
    f = Foo()
    print(f[1])
    for i in f:
        print(i)
    print(20 in f)
    print(40 in f)

10
0
10
20
True
False

虽然没有__iter__方法,但Foo实例是可迭代对象,因为发现由__getitem__方法时,python会调用它,传入从0开始的整数索引,尝试迭代对象。同样,没有实现__contains__,python会迭代Foo实例,因此也能使用in运算符查看有无指定元素。

运行时实现协议

为下面这份扑克增加洗牌功能:

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, item):
        return self._cards[item]

一个可选的方法是实现洗牌方法shuffle。但是FrenchDeck实例是一个类序列对象,那么使用random就可以了,用法如下:

from random import shuffle
l = list(range(10))
shuffle(l)
print(l)

[9, 5, 6, 4, 7, 2, 0, 1, 8, 3]

尝试:

from random import shuffle
deck = FrenchDeck()
shuffle(deck)

TypeError: 'FrenchDeck' object does not support item assignment     #对象不支持为元素赋值

FrenchDeck只实现了不可变的序列协议。可变的序列协议还需要实现__setitem__方法。

python是动态语言,可以在运行时修正这个问题:

deck = FrenchDeck()
def set_card(deck ,position, card):     #定义一个函数
    deck._cards[position] = card
from random import shuffle
FrenchDeck.__setitem__ = set_card       #把这个函数赋给FrenchDeck的__setitem__属性
shuffle(deck)
print(deck[:5])

#乱序的扑克牌
[Card(rank='4', suit='diamonds'), Card(rank='8', suit='diamonds'), Card(rank='3', suit='spades'), Card(rank='9', suit='clubs'), Card(rank='A', suit='clubs')]

set_card函数要知道deck对象有一个名为_cards的属性,而且_cards的值必须是可变序列。然后把set_card函数赋值给特殊方法__setitem__,从而把它依附到FrenchDeck类上,这种技术叫做猴子补丁:运行时修改类或模块,而不改动源码。

抽象基类

 ABC,Abstract Base Class(抽象基类),主要定义了基本类和最基本的抽象方法,可以为子类定义共有的API,不需要具体实现。相当于是Java中的接口或者是抽象类。
抽象基类可以不实现具体的方法(当然也可以实现,只不过子类如果想调用抽象基类中定义的方法需要使用super())而是将其留给派生类实现。
抽象基类提供了逻辑和实现解耦的能力,即在不同的模块中通过抽象基类来调用,可以用最精简的方式展示出代码之间的逻辑关系,让模块之间的依赖清晰简单。同时,一个抽象类可以有多个实现,让系统的运转更加灵活。而针对抽象类的编程,让每个人可以关注当前抽象类,只关注其方法和描述,而不需要考虑过多的其他逻辑,这对协同开发有很大意义。极简版的抽象类实现,也让代码可读性更高。

抽象基类的使用

1:直接继承
直接继承抽象基类的子类就没有这么灵活,抽象基类中可以声明”抽象方法“和“抽象属性”,只有完全覆盖(实现)了抽象基类中的“抽象”内容后,才能被实例化,而虚拟子类则不受此影响。
2:虚拟子类
将其他的类”注册“到抽象基类下当虚拟子类(调用register方法),虚拟子类的好处是你实现的第三方子类不需要直接继承自基类,可以实现抽象基类中的部分API接口,也可以根本不实现,但是issubclass(), issubinstance()进行判断时仍然返回真值。

抽象基类的本质就是几个特殊方法。例如:

class Struggle:
    def __len__(self):
        return 20

from collections import abc
print(isinstance(Struggle(), abc.Sized))

#
True

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

如果类体现了numbers,collections.abc或其他框架中抽象基类的概念,要么继承相应的抽象基类,要么把类注册到相应的抽象基类中。开发程序时,不要使用提供注册功能的库或框架,要自己动手注册。

标准库中的抽象基类

标准库中提供了抽象基类,大多数抽象基类在collections.abc模块中定义,numbersio包中也有一些。

collections.abc模块中的抽象基类

16个抽象基类

Iterable、Container 和 Sized

各个集合应该继承这三个抽象基类,或者至少实现兼容的协议。Iterable 通过 __iter__ 方法支持迭代,Container 通过__contains__ 方法支持 in 运算符,Sized 通过 __len__ 方法支持len() 函数。

Sequence、Mapping 和 Set

这三个是主要的不可变集合类型,而且各自都有可变的子类

MappingView

在 Python 3 中,映射方法 .items()、.keys() 和 .values() 返回的对象分别是 ItemsView、KeysView 和 ValuesView 的实例。前两个类还从 Set 类继承了丰富的接口。

Callable 和 Hashable

这两个抽象基类与集合没有太大的关系,只不过因为collections.abc 是标准库中定义抽象基类的第一个模块,而它们又太重要了,因此才把它们放到 collections.abc 模块中。我从未见过Callable 或 Hashable 的子类。这两个抽象基类的主要作用是为内置函数 isinstance 提供支持,以一种安全的方式判断对象能不能调用或散列。

Iterator

注意它是 Iterable 的子类。

numbers包中的抽象基类

下面各个抽象类结构是线性的

Number

Complex

Real

Rational

Intergral

如果想检查一个数是不是整数,可以使用 isinstance(x,numbers.Integral),这样代码就能接受 int、bool(int 的子类),或者外部库使用 numbers 抽象基类注册的其他类型。为了满足检查的需要,你或者你的 API 的用户始终可以把兼容的类型注册为numbers.Integral 的虚拟子类。

与之类似,如果一个值可能是浮点数类型,可以使用 isinstance(x,numbers.Real) 检查。这样代码就能接受bool、int、float、fractions.Fraction,或者外部库(如NumPy,它做了相应的注册)提供的非复数类型。

定义一个抽象基类

我们把这个抽象基类命名为 Tombola,这是宾果机和打乱数字的滚动容器的意大利名。有四个方法

.load(...):把元素放入容器。                            //抽象方法

.pick():从容器中随机拿出一个元素,返回选中的元素             //抽象方法

.loaded():如果容器中至少有一个元素,返回 True。         //普通方法

.inspect():返回一个有序元组,由容器中的现有元素构成,不会修改容器的内容(内部的顺序不保留)。   //普通方法

import abc
class Tombola(abc.ABC):    #自己定义的抽象基类要继承自abc.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))

子类不完全覆盖抽象基类的抽象方法看看是否可行:

class Fake(Tombola):  #声明为Tombola的子类
    def pick(self):
        return 13

print(Fake)
f = Fake()

#结果
<class '__main__.Fake'>   #创建Fake类没有错误
TypeError: Can't instantiate abstract class Fake with abstract methods load #实例化时抛出错误,没有实现load方法,认为Fake是抽象类

使用这个抽象基类

用法一:直接继承

class BingoCage(Tombola):

    def __init__(self, items):
        self._randomizer = random.SystemRandom()        #操作系统生成随机数
        self._items = []
        self.load(items)           #委托load实现初始加载

    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, *args, **kwargs):
        self.pick()

使用了更好的随机数生成器,继承了loaded方法,覆盖了inspect,实现了__call__方法。

另一种实现:

class BingoCage(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):            #覆盖loaded方法
        return bool(self._balls) 
    
    def inspect(self):            #覆盖inspect方法
        return tuple(sorted(self._balls))

用法二:虚拟子类

即使不继承,也有办法把一个类注册为抽象基类的虚拟子类。这样做时,需要保证注册的类忠实地实现了抽象基类定义的接口,python不做检查,但如果没有实现,可能会产生运行时异常。

注册虚拟子类的方式是在抽象基类上调用register方法(register可以当作类装饰器使用)。这么做之后,注册的类会变为抽象基类的虚拟子类,而且isinstance和issubclass都能识别,但注册的类不会从抽象基类中继承任何方法或属性

@Tombola.register       #把TomboList注册为Tombola的虚拟子类
class TomboList(list):   #扩展list

    def pick(self):
        if self:            #从list中继承bool方法,不为空时返回True
            position = random.randrange(len(self))   #获取一个随机元素索引
            return self.pop(position)
        else:
            raise LookupError('pop from empty TomboList')

    load = list.extend    #load方法和list.extend方法一样

    def loaded(self):
        return bool(self)

    def inspect(self):
        return tuple(sorted(TomboList))

判断是否为Tombola的子类:

print(issubclass(TomboList, Tombola))
t = TomboList(range(100))
print(isinstance(t, Tombola))

#结果
True
True

查看方法解析顺序:

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

发现它只列出"真实的"超类。

以上来自《流畅的python》

原文地址:https://www.cnblogs.com/lht-record/p/10311479.html