python数列类基类_python抽象基类

接口和协议

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

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

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

序列协议

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

抽象基类Sequence正式接口:

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

classFoo:def __getitem__(self, item):return range(0, 30, 10)[item]if __name__ == '__main__':

f=Foo()print(f[1])for i inf:print(i)print(20 inf)print(40 inf)10010

20True

False

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

运行时实现协议

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

importcollections

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

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 inself.suitsfor rank inself.ranks]def __len__(self):returnlen(self._cards)def __getitem__(self, item):return self._cards[item]

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

from random importshuffle

l= list(range(10))

shuffle(l)print(l)

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

尝试:

from random importshuffle

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] =cardfrom random importshuffle

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()进行判断时仍然返回真值。

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

classStruggle:def __len__(self):return 20

from collections importabcprint(isinstance(Struggle(), abc.Sized))#True

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

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

标准库中的抽象基类

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

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():返回一个有序元组,由容器中的现有元素构成,不会修改容器的内容(内部的顺序不保留)。   //普通方法

importabcclass Tombola(abc.ABC): #自己定义的抽象基类要继承自abc.ABC

@abc.abstractmethod #抽象方法使用@abc.abstractmethod标记,而且定义体中通常只有文档字符串

defload(self, iterable):"""从可迭代对象中添加元素"""@abc.abstractmethoddefpick(self):"""随机删除元素,然后将其返回

如果实例为空,应该抛出LookupError"""

defloaded(self):"""如果有一个元素返回True,否则返回False"""

returnbool(self.inspect())def inspect(self): #抽象基类中的具体方法只能依赖抽象基类定义的接口(即只能使用抽象基类中其他具体方法,抽象方法和特性)

"""返回一个有序元组,由当前元素构成"""items=[]whileTrue:try:

items.append(self.pick())exceptLookupError:breakself.load(items)return tuple(sorted(items))

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

class Fake(Tombola): #声明为Tombola的子类

defpick(self):return 13

print(Fake)

f=Fake()#结果

#创建Fake类没有错误

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

使用这个抽象基类

用法一:直接继承

classBingoCage(Tombola):def __init__(self, items):

self._randomizer= random.SystemRandom() #操作系统生成随机数

self._items =[]

self.load(items)#委托load实现初始加载

defload(self, items):

self._items.extend(items)

self._randomizer.shuffle(self._items)defpick(self):try:returnself._items.pop()exceptIndexError:raise LookupError('pick from empty BingoCage')def __call__(self, *args, **kwargs):

self.pick()

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

另一种实现:

classBingoCage(Tombola):def __init__(self, iterable):

self._balls= list(iterable) #创建列表

defload(self, iterable):

self._balls.extend(iterable)defpick(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方法

returnbool(self._balls)def inspect(self): #覆盖inspect方法

return tuple(sorted(self._balls))

用法二:虚拟子类

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

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

@Tombola.register #把TomboList注册为Tombola的虚拟子类

class TomboList(list): #扩展list

defpick(self):if self: #从list中继承bool方法,不为空时返回True

position = random.randrange(len(self)) #获取一个随机元素索引

returnself.pop(position)else:raise LookupError('pop from empty TomboList')

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

defloaded(self):returnbool(self)definspect(self):return tuple(sorted(TomboList))

判断是否为Tombola的子类:

print(issubclass(TomboList, Tombola))

t= TomboList(range(100))print(isinstance(t, Tombola))#结果

True

True

查看方法解析顺序:

print(TomboList.__mro__)

(, , )

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

以上来自《流畅的python》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值