前言
抽象类表示接口 ————Bjarne Stroustrup(C++ 之父)
本章讨论的主题是“鸭子类型”:对象的类型无关紧要,只要实现了特定的协议即 可。
Python文化中的接口和协议
Python语言没有interface
关键字,而且除了抽象基类,每个类都有接口:类实现或继承的公开属性(方法或数据属性),包括特殊方法,如__getitem__
或__add__
。
按照定义
,受保护的属性
和私有属性
不在
接口中,即使“受保护的”属性也只是采用命名约定实现的(单个前导下划线);私有属性可以轻松地访问。不要违背这些约定
关于接口,这里有个实用的补充定义:对象公开方法的子集,让对象在系统中扮演特定的角色
。接口是实现特定角色的方法集合,这样理解正是Smalltalk程序员所说的协议,其他动态语言社区都借鉴了这个术语。协议与继承没有关系。一个类可能会实现多个接口,从而让实例扮演多个角色。
协议是接口
,但不是正式的
(只由文档和约定定义),因此协议不能像正式接口那样施加限制,但抽象基类对接口一致性是强制的。一个类可能只实现部分接口,这是允许的。
对Python程序员来说,X 类对象
, X 协议
和X 接口
都是一个意思。
python喜欢序列
|
|
虽然没有__iter__
方法,但是Foo
实例是可迭代的对象,因为发现有__getitem__
方法时,Python会调用它,传入从 0 开始的整数索引,尝试迭代对象(这是一种后备机制)。尽管没有实现__contains__
方法,但是 Python足够智能,能迭代Foo
实例,因此也能使用in
运算符
使用猴子补丁在运行时实现协议
|
|
要在命令行下编写上述程序?单文件会TypeError: 'Sequence' object does not support item assignment
后无法往下运行
Alex Martelli
的水禽
鸭子类型:忽略对象的真正类型,转而关注对象有没有实现所需的方法、签名和语义
对 Python 来说,这基本上是指避免使用isinstance
检查对象的类型(更别提type(foo) is bar
这种更糟的检查方式了,这样做没有任何好处,甚至禁止最简单的继承方式)
Python的抽象基类还有一个重要的实用优势:可以使用register
类方法在终端用户的代码中把某个类“声明”为一个抽象基类的“虚拟”子类(为此,被注册的类必须满足抽象基类对方法名称和签名的要求,最重要的是要满足底层语义契约
;但是,开发那个类时不用了解抽象基类,更不用继承抽象基类)。这大大地打破了严格的强耦合,与面向对象编
程人员掌握的知识有很大出入,因此使用继承时要小心
。
有时,为了让抽象基类识别子类,甚至不用注册(如下面这个例子,实现了__len__()
即可)
|
|
但最重要的一点
是,句法和语义要保证按照规定,即符合约定
如果实现的类体现了numbers
、collections.abc
或其他框架中抽象基类的概念,要么继承相应的抽象基类(必要时),要么把类注册到相应的抽象基类中。
开始开发程序时,不要使用提供注册功能的库或框架,要自己动手注册;如果必须检查参数的类型(这是最常见的),例如检查是不是“序列”,那就这样做
|
|
即便是抽象基类,也不能滥用 isinstance 检查,用得多了可能导致代码异味,即表明面向对象设计得不好。在一连串 if/elif/elif 中使用 isinstance 做检查,然后根据对象的类型执行不同的操作,通常是不好的做法;此时应该使用多态
,即采用一定的方式定义类,让解释器把调用分派给正确的方法,
标准库中的抽象基类
Iterable
、Container
和Sized
各个集合应该继承这三个抽象基类,或者至少实现兼容的协议。Iterable
通__iter__
方法支持迭代,Container
通过__contains__
方法支持in
运算符,Sized
通过__len__
方法支持len()
函数
Sequence
、Mapping
和Set
这三个是主要的不可变集合类型,而且各自都有可变的子类MutableSequence
,MutableMapping
和MutableSet
MappingView
在Python 3
中,映射方法.items()
、.keys()
和.values()
返回的对象分别是
ItemsView
、KeysView
和ValuesView
的实例。前两个类还从Set
类继承了丰富的接口
Callable
和Hashable
这两个抽象基类的主要作用是为内置函数isinstance
提供支持,以一种安全的方式判断对象能不能调用或散列
Iterator
后续的章节会提到
numbers
python包numbers
定义的是“数字塔”(即各个抽象基类的层次结构是线性的),其中Number
是位于最顶端的超类,随后Complex
子类,依次往下,最底端是Integral
类:
Number
Complex
Real
Rational
Integral
因此,如果想检查一个数是不是整数,可以使用isinstance(x,numbers.Integral)
。为了满足检查的需要,你或者你的API的用户始终可以把兼容的类型注册为numbers.Integral
的虚拟子类。
如果一个值可能是浮点数类型,可以使用isinstance(x, numbers.Real)
检查。这样代码就能接受bool
、int
、float
、fractions.Fraction
,或者外部库(如
NumPy,它做了相应的注册)提供的非复数类型。
decimal.Decimal
没有注册为numbers.Real
的虚拟子类,原因是,如果你的程序需要 Decimal
的精度,要防止与其他低精度数字类型混淆,尤其是浮点数。(换言之区分开来,才好计较小数位数)
自己动手实现一个基类
场景:
你要在网站或移动应用中显示随机广告,但是在整个广告清单轮转一遍之前,不重复显示广告。假设我们在构建一个广告管理框架,名为ADAM
。它的职责之一是,支持用户提供随机挑选的无重复类。为了让ADAM
的用户明确理解“随机挑选的无重复”组件是什么意思,我们将定义一个抽象基类Tombola
。
xxxxxxxxxxxxxxxx图片
Tombola
抽象基类有四个方法,其中两个是抽象方法:
.load(...)
:把元素放入容器.pick()
:从容器中随机拿出一个元素,返回选中的元素。
另外两个是具体方法:
.loaded()
:如果容器中至少有一个元素,返回 True.inspect()
:返回一个有序元组,由容器中的现有元素构成,不会修改容器的内容(内部的顺序不保留)
|
|
abc.ABC
是python 3.4
新增的类,之前的版本为:
|
|
在python 2
中:
|
|
声明抽象类方法的推荐方式
注意叠放顺序
|
|
抽象方法可以有实现代码。即便实现了,子类也必须覆盖抽象方法,但是在子类中可以使用super()
函数调用抽象方法,为它添加功能,而不是从头开始实现(老生常谈)
使用LookupError
异常的原因是,在Python的异常层次关系中,它与IndexError
和KeyError
有关,这两个是具体实现Tombola
所用的数据结构最有可能抛出的异常。据此,实现代码可能会抛出 LookupError
、IndexError
或KeyError
异常
|
|
IndexError
和KeyError
是LookupError
的子类
虚拟子类
即便不继承,也有办法把一个类注册为抽象基类的虚拟子类,这样做时,需要保证注册的类忠实的实现了抽象基类定义的接口。
注册虚拟子类的方式是在抽象基类上调用register()
方法, 注册为虚拟子类后,issubclass()
和isinstance()
都能识别。
虚拟子类不会继承注册的抽象基类, 不会从抽象基类中继承任何方法或属性
, 而且任何时候都不会检查它是否符合抽象基类的接口, 即便在实例化时也不会检查。为了避免运行时错误,虚拟子类要实现所需的全部方法。
__subclasses__()
返回类的直接子类列表,不含虚拟子类。
_abc_registry
只有抽象基类有这个数据属性,其值是一个 WeakSet 对象,即抽象类注册的虚拟子类的弱引用。