这一章主要讲 ABC 的机制。但是处处提醒读者 不要 随意自定义新的 ABC,而是使用已有的 ABC:
ABCs, like descriptors and metaclasses, are tools for building frameworks. Therefore, only a very small minority of Python developers can create ABCs without imposing unreasonable limitations and needless work on fellow programmers.
Interfaces and Protocols in Python Culture
对于 Python 来说(或者对于大部分动态语言来说),接口(interface)是用类的一些公共 attibutes (method or data attributes) 来表达的。
Protocol 是不正式的 interfaces,它是靠文档和约定(convention)来定义的。比如 "a file-like object" 是一个实现了 .read()
函数的类。而且它不保证一整套函数都实现了,比如一个支持 slicing 的 sequence 类,它有可能没有长度这个概念,于是它可以不实现 __len__
函数。
Python Digs Sequences
Python 总是尽力去实现基础的 protocol(这个表达可能不好)。比如下面这个类:
class Foo:
def __getitem__(self, pos):
return range(0, 30, 10)[pos]
>>> f = Foo()
>>> for i in f:
... print(i)
0
10
20
>>> 10 in f
True
这个类并没有实现 __iter__
, __contains__
函数,但是也可以被使用在相应的语法中,因为聪明的 Python 会把 __getitem__
作为一个 fallback 去遍历。
Monkey-Patching to Implement a Protocol at Runtime
这节在我看来不是 best practise。它通过 hack 进一个 __setitem__
函数到 FrenchDeck
类中来使这个类支持被 random.shuffle()
操作。这意味着 hack 进去的函数必需知道 FrenchDeck
类的内部实现是怎样的,而且它还使用了类里面的私有变量。但是这也展示了一种可能性,后面写代码时可以权衡下。
Alex Martelli’s Waterfowl
Alex Martelli 的故事很长且难看懂,主要是从特种的分类学出发,描述其分类从按语音(phonetics)到按其 DNS 序列的吻合程度(cladistics)的演变。里面有一段提到 ABC 的好处,即可以 register
而不需要继承,使得减少了很多继承带来的耦合性:
Among the many conceptual advantages of ABCs over concrete classes (e.g., Scott Meyer’s “all non-leaf classes should be abstract”—see Item 33 in his book, More Effective C++), Python’s ABCs add one major practical advantage: theregister
class method, which lets end-user code “declare” that a certain class becomes a “virtual” subclass of an ABC (for this purpose the registered class must meet the ABC’s method name and signature requirements, and more importantly the underlying semantic contract—but it need not have been developed with any awareness of the ABC, and in particular need not inherit from it!). This goes a long way toward breaking the rigidity and strong coupling that make inheritance something to use with much more caution than typically practiced by most OOP programmers…
Subclassing an ABC
Python does not check for the implementation of the abstract methods at import time
(when the frenchdeck2.py module is loaded and compiled), but only at runtime when we actually try to instantiate FrenchDeck2.
Python 只会在运行时,实例化一个类时才去检查它是否实现了它的 ABC 父类要求的函数。
图中的斜体部分,表示虚函数,是你需要实现的函数。Python 的 ABC 也会向提供一些函数供你使用,比如 MutableSequence
中的 append
是不需要你实现的,你只需要实现 insert
函数,append
函数就会调用它来实现自身逻辑。
同时,对于 ABC 默认实现的函数,你也可以写自己的版本覆盖它,实现更好的性能。比如 Sequence
类的 __contains__
函数,默认实现是遍历 sequence,你可以写一个性能更好的版本替代它。
ABCs in the Standard Library
Python 中最重要的 ABC 分布在两个地方:
书中的 UML 图比较直观地表示了各 ABC 间的关系。
Defining and Using an ABC
上代码:
import abc
class Tombola(abc.ABC):
@abc.abstractmethod
def load(self, iterable):
"""Add items from an iterable."""
@abc.abstractmethod
def pick(self):
"""Remove item at random, returning it.
This method should raise `LookupError` when the instance is empty."""
def loaded(self):
"""Return `True` if there's at least 1 item, `False` otherwise."""
return bool(self.inspect())
def inspect(self):
"""Return a sorted tuple with the items currently inside."""
items = []
while True:
try:
items.append(self.pick())
except LookupError:
break
self.load(items)
return tuple(sorted(items))
划重点:
- ABC 中可以提供具体的函数实现。但是函数实现必须依赖于公开接口,而不能有私有数据
- 用了
@abstractmethod
的函数,也可以有具体实现,只是子类仍然需要写一个自己的版本;不过子类可以调用父类的同名函数,不用重头写起 pick
函数抛出一个LookupError
是有意为之,因为LookupError
有两个子类IndexError
和KeyError
,方便子类抛出更具体的异常。虽然没有具体机制去约束之类可能抛出的异常,但是子类还是应该与父类保持一致
@abc.abstractmethod
可以叠 @classmethod
, @staticmethod
, @property
,只需要 @abc.abstractmethod
在最内层。
A Virtual Subclass of Tombola
register
让一个类声明自己实现了 ABC 所要求的接口,而不需要继承它。特点:
- 能过
issubclass
isinstanceof
检查 - Python 解释器不会在任何时刻检查它的接口是否实现(甚至是在子类实例初始化时)
How the Tombola Subclasses Were Tested
演示了一把怎样用 doc test 来测试一个 reStructuredText 文档中的测试样例。
Geese Can Behave as Ducks
Sized
用了 __subclasshook__
,会用来做 ABCMeta.__subclasscheck__
:
class Sized(metaclass=ABCMeta):
__slots__ = ()
@abstractmethod
def __len__(self):
return 0
@classmethod
def __subclasshook__(cls, C):
if cls is Sized:
if any("__len__" in B.__dict__ for B in C.__mro__):
return True
return NotImplemented
所以:
>>> class Struggle:
... def __len__(self): return 23
...
>>> from collections import abc
>>> isinstance(Struggle(), abc.Sized)
True
>>> issubclass(Struggle, abc.Sized)
True
但是这个 __subclasshook__
适用的范围很小。自己写的代码不要用它。比如 dict
虽然实现了 Sequence 所要求的 __len__
__getitem__
__iter__
,但是它不是 Sequence,因为它的内部元素不是有序的。所以 dict
的实现上不会用 __subclasshook__
做动态的 dunder function 判断。
Soapbox
Strong versus weak typing
If the language rarely performs implicit conversion of types, it’s considered strongly typed; if it often does it, it’s weakly typed. Java, C++, and Python are strongly typed. PHP, JavaScript, and Perl are weakly typed.
Static versus dynamic typing
If type-checking is performed at compile time, the language is statically typed; if it happens at runtime, it’s dynamically typed. Static typing requires type declarations (some modern languages use type inference to avoid some of that). Fortran and Lisp are the two oldest programming languages still alive and they use, respectively, static and dynamic typing.
这节把强 / 弱类型、静态 / 动态类型做了描述。所以 Python 是强类型 + 动态类型。