第 3 章 类与继承(下)
第 25 条: 用 super 初始化父类
初始化父类的传统方式,是在子类里用子类实例直接调用父类的 __init__
方法。
class MyBaseClass(object):
def __init__(self, value):
self.value = value
class MyChildClass(MyBaseClass):
def __init__(self):
MyBaseClass.__init__(self, 5)
这种办法对于简单的继承体系是可行的,但是在许多情况下会出问题。
如果子类受到了多重继承的影响,那么直接调用超类的 __init__
方法,可能会产生无法预知的行为。
在子类里调用 __init__
的问题之一,是它的调用顺序并不固定。例如,下面定义两个超类,它们都操作名为 value 的实例字段:
class TimesTwo(object):
def __init__(self):
self.value *= 2
class PlusFive(object):
def __init__(self):
self.value += 5
下面这个类用其中一种顺序来定义它所继承的各个超类。
class Oneway(MyBaseClass, TimesTwo, PlusFive):
def __init__(self, value):
MyBaseClass.__init__(self, value)
TimesTwo.__init__(self)
PlusFive.__init_(self)
构建该类实例之后,我们发现,它所产生的结果与继承时的超类顺序相符。
foo = Oneway(5)
print('First ordering is (5 * 2) + 5 =', foo.value)
>>>
First ordering is (5 * 2) + 5 = 15
下面这个类,用另外—种顺序来定义它所继承的各个超类:
class Anotherway(MyBaseClass, PlusFive, TimesTwo):
def __init__(self, value):
MyBaseClass.__init__(self, value)
TimesTwo.__init__(self)
PlusFive.__init__(self)
但是,上面这段代码并没有修改超类构造器的调用顺序,它还是和以前一样,先调用 TimesTwo.__init__
,然后才调用 PlusFive.__init__
,这就导致该类所产生的结果与其超类的定义顺序不相符。
bar = Anotherway(5)
print('Second ordering still is', bar.value)
>>>
Second ordering still is 15
还有一个问题发生在钻石形继承之中。如果子类继承自两个单独的超类,而那两个超类又继承自同一个公共基类,那么就构成了钻石形继承体系8。这种继承会使钻石顶部的那个公共基类多次执行其 __init__
方法,从而产生意想不到的行为。例如,下面定义的这两个子类,都继承自 MyBaseClass。
class TimesFive(MyBaseClass):
def __init__(self, value):
MyBaseClass.__init__(self, value)
self.value *= 5
class PlusTwo(MyBaseClass):
def __init__(self, value):
MyBaseClass.__init__(self, value)
self.value += 2
然后定义一个子类,同时继承上面这两个类,这样 MyBaseClass 就成了钻石顶部的那个公共基类。
class Thisway(TimesFive, PlusTwo):
def __init__(self, value):
TimesFive.__init__(self, value)
PlusTwo.__init__(self, value)
foo = Thisway(5)
print('Should be (5 * 5) + 2 = 27 but is', foo.value)
>>>
Should be (5 * 5) + 2 = 27 but is 7
可能认为输出的结果会是 27,因为(5 * 5)+ 2 = 27,但实际上却是 7,因为在调用第二个超类的构造器,也就是 PlusTwo.__init__
的时候,它会再度调用 MyBaseClass.__init__
,从而导致 self.value 重新变成 5。
Python 2.2 增加了内置的 super 函数,并且定义了方法解析顺序(method resolution order, MRO),以解决这一问题。MRO 以标准的流程来安排超类之间的初始化顺序(例如,深度优先、从左至右),它也保证钻石顶部那个公共基类的 __init__
方法只会运行一次。
下面重新创建钻石形的继承体系,但是这一次,我们用 super 初始化超类(范例代码以 Python 2 的风格来使用 super):
# Python 2
class TimesFiveCorrect(MyBaseClass):
def __init__(self, value):
super(TimesFiveCorrect, se1f).__init__(value)
self.value *= 5
class PlusTwoCorrect(MyBaseClass):
def __init__(self, value):
super(PlusTwoCorrect, self).__init__(value)
self.value += 2
现在,对于处在钻石顶部的那个 MyBaseClass 类来说,它的 __init__
方法只会运行一次。而其他超类的初始化顺序,则与这些超类在 class 语句中出现的顺序相同。
# Python 2
class Goodway(TimesFiveCorrect, PlusTwoCorrect):
def __init__(self, value):
super(Goodway, self).__init__(value)
foo = Goodway(5)
print 'Should be 5 * (5 + 2) = 35 and is', foo.value
>>>
Should be 5 * (5 + 2) = 35 and is 35
看到上面的运行结果之后,可能觉得程序的计算顺序和自己所想的刚好相反。应该先运行 TimesFiveCorrect.__init__
,然后运行 PlusTwoCorrect.__init__
,并得出(5 * 5)+ 2 = 27 才对啊。但实际上却不是这样的。程序的运行顺序会与 GoodWay 类的 MRO 保持一致,这个 MRO 顺序可以通过名为 mro 的类方法来查询。
from pprint import pprint
pprint(Goodway.mro())
>>>
[<class '__main__.Goodway '>,
<class '__main__.TimesFiveCorrect'>,
<class '__main__.PlusTwoCorrect'>,
<class '__main__.MyBaseClass'>,
<class 'object'>]
调用 GoodWay(5) 的时候,它会调用 TimesFiveCorrect.__init__
, 而 TimesFiveCorrect.__init__
又会调用 PlusTwoCorrect.__init__
,PlusTwoCorrect.__init__
会调用 MyBaseClass.__init__
。到达了钻石体系的顶部之后,所有的初始化方法会按照与刚才那些 __init__
相反的顺序来运作。于是,MyBaseClass.__init__
会先把 value 设为 5,然后 PlusTwoCorrect.__init__
会为它加 2,使 value 变成 7,最后,TimesFiveCorrect.__init__
会将 value 乘以 5,使其变为 35。
内置的 super 函数确实可以正常运作,但在 Python 2 中有两个问题值得注意:
- super 语句写起来有点麻烦。我们必须指定当前所在的类和 self 对象,而且还要指定相关的方法名称(通常是
__init__
)以及那个方法的参数。对于 Python 编程新手来说,这种构造方式有些费解。 - 调用 super 时,必须写出当前类的名称。由于我们以后很可能会修改类体系,所以类的名称也可能会变化,那时,必须修改每一条 super 调用语句才行。
Python 3 则没有这些问题,因为它提供了一种不带参数的 super 调用方式,该方式的效果与用 __class__
和 self 来调用 super 相同。Python 3 总是可以通过 super 写出清晰、精练而又准确的代码。
class Explicit(MyBaseClass):
def __init__(self, value):
super(__class__, self).__init__(value * 2)
class Implicit(MyBaseClass):
def __init__ (self, value):
super().__init__(value * 2)
assert Explicit(10).value == Implicit(10).value
由于 Python 3 程序可以在方法中通过 __class__
变量准确地引用当前类,所以上面这种写法能够正常运作,而 Python 2 则没有定义 __class__
,故而不能采用这种写法。可能想试着用 self.__class__
做参数来调用 super,但实际上这么做不行,因为 Python2 是用特殊方式来实现 super 的。
总结
- Python 采用标准的方法解析顺序来解决超类初始化次序及钻石继承问题。
- 总是应该使用内置的 super 函数来初始化父类。
第 26 条: 只在使用 Mix-in 组件制作工具类时进行多重继承
Python 是面向对象的编程语言,它提供了一些内置的编程机制,使得开发者可以适当地实现多重继承。但是,仍然应该尽量避开多重继承。
若一定要利用多重继承所带来的便利及封装性,那就考虑编写 mix-in 类。mix-in 是一种小型的类,它只定义了其他类可能需要提供的一套附加方法,而不定义自己的实例属性,此外,它也不要求使用者调用自己的 __init__
构造器。
由于 Python 程序可以方便地查看各类对象的当前状态,所以编写 mix-in 比较容易。可以在 mix-in 里面通过动态检测机制先编写一套通用的功能代码,稍后再将其应用到其他很多类上面。分层地组合 mix-in 类可以减少重复代码并提升复用度。
例如,要把内存中的 Python 对象转换为字典形式,以便将其序列化(serialization),那就不妨把这个功能写成通用的代码,以供其他类使用。
下列代码定义了实现该功能所用的 mix-in 类,并在其中添加了一个新的 public 方法,使其他类可以通过继承这个 mix-in 类来具备此功能:
class ToDictMixin(object):
def to_dict(self):
return self._traverse_dict(self.__dict__)
具体的实现代码写起来也很直观:只需要用 hasattr 函数动态地访问属性、用 isinstance 函数动态地检测对象类型,并用 __dict__
来访问实例内部的字典即可。
def _traverse_dict(self, instance_dict):
output = {}
for key, value in instance_dict.items():
output[key] = self._traverse(key, value)
return output
def _traverse(self, key, value):
if isinstance(value, ToDictMixin):
return value.to_dict()
elif isinstance(value, dict):
return self._traverse_dict(value)
elif isinstance(value, list):
return [self._traverse(key, i) for i in value]
elif hasattr(value, '__dict__'):
return self._traverse_dict(value.__dict__)
else:
return value
下面定义的这个类演示了如何用 mix-in 把二叉树表示为字典:
class BinaryTree(ToDictMixin):
def __init__(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right
现在,可以把一大批互相关联的 Python 对象都轻松地转换成字典。
tree = BinaryTree(10,
left=BinaryTree(7, right=BinaryTree(9)),
right=BinaryTree(13, left=BinaryTree(11)))
print(tree.to_dict())
>>>
{'left': {'left': None,
'right': {'left': None, 'right': None, 'value': 9},
'value': 7},
'right': {'1eft': {'left': None, 'right': None, 'value': 11},
'right': None,
'value': 13},
'value': 10}
mix-in 的最大优势在于,使用者可以随时安插这些通用的功能,并且能在必要的时候覆写它们。例如,下面定义的这个 BinaryTree 子类,会持有指向父节点的引用。假如采用默认的 ToDictMixin.to_dict 来处理它,那么程序就会因为循环引用而陷入死循环。
class BinaryTreeWithParent(BinaryTree):
def __init__(self, value, left=None,
right=None, parent=None):
super().__init__(value, left=left, right=right)
self.parent = parent
解决办法是在 BinaryTreeWithParent 类里面覆写 ToDictMixin._traverse 方法,令该方法只处理与序列化有关的值,从而使 mix-in 的实现代码不会陷入死循环。下面覆写的这个 _traverse 方法,不再遍历父节点,而是只把父节点所对应的数值插入最终生成的字典里面。
def _traverse(self, key, value):
if (isinstance(value, BinaryTreewithParent) and key == 'parent'):
return value.value # Prevent cycles
else:
return super()._traverse(key, value)
现在调用 BinaryTreeWithParent.to_dict 是不会有问题的,因为程序已经不再追踪导致循环引用的那个 parent 属性了。
root = BinaryTreeWithParent(10)
root.left = BinaryTreeWithParent(7, parent=root)
root.left.right = BinaryTreeWithParent(9, parent=root.left)
print(root.to_dict())
>>>
{'left': {'left': None,
'parent': 10,
'right': {'left': None,
'parent': 7,
'right': None,
'value': 9},
'value': 7},
'parent': None,
'right': None,
'value': 10}
定义了 BinaryTreeWithParent._traverse 方法之后,如果其他类的某个属性也是 BinaryTreeWithParent 类型,那么 ToDictMixin 会自动地处理好这些属性。
class NamedSubTree(ToDictMixin):
def __init__(self, name, tree_with_parent):
self.name = name
self.tree_with_parent = tree_with_parent
my_tree = NamedSubTree('foobar', root.left.right)
print(my _tree.to_dict()) # No infinite loop
>>>
{'name': 'foobar',
'tree _with_parent': {'left': None,
'parent': 7,
'right': None,
'va1ue': 9}}
多个 mix-in 之间也可以相互组合。例如,可以编写这样一个 mix-in,它能够为任意类提供通用的 JSON 序列化功能。可以假定:继承了 mix-in 的那个类,会提供名为 to_dict 的方法(此方法有可能是那个类通过多重继承 ToDictMixin 而具备的,也有可能不是)。
class JsonMixin(object):
@classmethod
def from_json(cls, data):
kwargs = json.loads(data)
return cls(**kwargs)
def to_json(self):
return json.dumps(self.to_dict())
请注意,JsonMixin 类既定义了实例方法,又定义了类方法。这两种行为都可以通过 mix-in 来提供。在本例中,凡是想继承 JsonMixin 的类,只需要符合两个条件即可,第一,包含名为 to_dict 的方法;第二,__init__
方法接受关键字参数。
有了这样的 mix-in 之后,只需编写极少量的例行代码,就可以通过继承体系,轻松地创建出相关的工具类,以便实现序列化数据以及从 JSON 中读取数据的功能。例如,用下面这个继承了 mix-in 组件的数据类来表示数据中心的拓扑结构:
class DatacenterRack(ToDictMixin, JsonMixin):
def __init__(self, switch=None, machines=None):
self.switch = Switch(**switch)
self.machines = [
Machine(**kwargs) for kwargs in machines]
class Switch(ToDictMixin, JsonMixin):
# ...
class Machine(ToDictMixin, ]sonMixin):
# ...
对这样的类进行序列化,以及从 JSON 中加载它,都是比较简单的。下面的这段代码,会重复执行序列化及反序列化操作,以验证这两个功能有没有正确地实现出来。
serialized = """{
"switch": {"ports": 5, "speed": 1e9},
"machines": [
{"cores": 8, "ram": 32e9, "disk": 5e12},
{"cores": 4, "ram": 16e9, "disk": 1e12},
{"cores": 2, "ram": 4e9, "disk": 500e9}
]
}"""
deseria1ized = DatacenterRack.from_json(serialized)
roundtrip = deserialized.to_json()
assert json.loads(serialized) == json.loads(roundtrip)
使用这种 mix-in 的时候,既可以像本例这样,直接继承多个 mix-in 组件,也可以先令继承体系中的其他类继承相关的 mix-in 组件,然后再令本类继承那些类,以达到同样的效果。
总结
- 能用 mix-in 组件实现的效果,就不要用多重继承来做。
- 将各功能实现为可插拔的 mix-in 组件,然后令相关的类继承自己需要的那些组件,即可定制该类实例所应具备的行为。
- 把简单的行为封装到 mix-in 组件里,然后就可以用多个 mix-in 组合出复杂的行为了。
第 27 条:多用 public 属性,少用 private 属性
对 Python 类来说,其属性的可见度只有两种,也就是 public(公开的、公共的)和 private(私密的、私有的)。
class MyObject(object):
def __init__(self):
self.public_field = 5
self.__private_field = 10
def get_private_field(self):
return self.__private_field
任何人都可以在对象上通过 dot 操作符(即 . 操作符,点操作符)来访问 public 属性。
foo = My0bject()
assert foo.pub1ic_field == 5
以两个下划线开头的属性,是 private 字段。本类的方法可以直接访问它们。
assert foo.get_private_fie1d() == 10
在类的外面直接访问 private 字段会引发异常。
foo.__private_field
>>>
AttributeError: 'Myobject' object has no attribute'__private_field'
由于类级别的方法仍然声明在本类的 class 代码块之内,所以,这些方法也是能够访问 private 属性的。
class MyOtherObject(object):
def __init__(self):
self.__private_field = 71
@classmethod
def get_private_field_of_instance(cls, instance):
return instance.__private_field
bar = MyOtherObject()
assert MyOther0bject.get_private_field_of_instance(bar) == 71
正如大家所预料的那样,子类无法访问父类的 private 字段。
class MyParentObject(object):
def __init__(self):
self.__private_field = 71
class MyChildObject(MyParentObject):
def get_private_field(self):
return self.__private_field
baz = MyChild0bject()
baz.get_private_field()
>>>
AttributeError: 'MyChild0bject' object has no attribute '_MyChildObject__private_field'
Python 会对私有属性的名称做一些简单的变换,以保证 private 字段的私密性。当编译器看到 MyChildObject.get_private_field 方法要访问私有属性时,它会先把 __private_field 变换为 _MyChildObject__private_field,然后再进行访问。本例中,__private_field 字段只在 MyParentObject.__init__
里面做了定义,因此,这个私有属性的真实名称,实际上是 _MyParentObject__privatc_field。子类之所以无法访问父类的私有属性,只不过是因为变换后的属性名与待访问的属性名不相符而已。
了解这套机制之后,就可以从任意类中访问相关类的私有属性了。无论是从该类的子类访问,还是从外部访问,都不受制于 private 属性的访问权限。
assert baz._MyParentObject__private_field == 71
查询该对象的属性字典,就能看到,私有属性实际上是按变换后的名称来保存的。
print(baz.__dict__)
>>>
{'_MyParentObject__private_fie1d': 71}
Python 为什么不从语法上严格保证 private 字段的私密性呢?用最简单的话来说,就是:We are all consenting adults here(我们都是成年人)。这句广为流传的格言,表达了很多 Python 程序员的观点,大家都认为开放要比封闭好。
另外一个原因在于:Python 语言本身就已经提供了一些属性挂钩,使得开发者能够按照自己的需要来操作对象内部的数据。既然如此,那为什么还要阻止访问 private 属性呢?
为了尽量减少无意间访问内部属性所带来的意外,Python 程序员会遵照 《Python 风格指南》 的建议,用一种习惯性的命名方式来表示这种字段。也就是说:以单个下划线开头的字段,应该视为 protected(受保护的)字段,本类之外的那些代码在使用这种字段的时候要多加小心。
虽说有了这种约定,但仍然有很多 Python 编程新手会使用私有字段表示那种不应该由子类或外部来访问的 API。
class MyClass(object):
def __init__(self, value):
self._value = value
def get_value(self):
return str(self.__value)
foo = MyClass(5)
assert foo.get_value() == '5'
这种写法不好。因为包括你自己在内的许多开发者,以后可能都需要从这个类中继承子类,并在子类里面添加新的行为,或是改进现有方法中效率不高的那些部分(例如,上面那个类的 MyClass.get_value 方法总是返回字符串,这可能就需要改进)。假如超类使用了 private 属性,那么子类在覆写或扩展的时候,就会遇到麻烦和错误。继承该类的那些子类,在万不得已的时候,仍然要去访问 private 字段。
class MyIntegerSubclass(MyClass):
def get_value(self):
return int(self._MyClass__value)
foo = MyIntegerSubclass(5)
assert foo.get_value() == 5
可是,一旦子类后面的那套继承体系发生变化,这些对 private 字段的引用代码就会失效,从而导致子类出现错误。就本例来说,MyIntegerSubclass 类的直接超类 MyClass 本来是直接继承自 object 的,但是现在,改令其继承自名为 MyBaseClass 的类:
class MyBaseClass(object):
def __init__(self, value):
self._value = value
# ...
class MyClass(MyBaseClass):
# ...
class MyIntegerSubclass(MyClass):
def get_value(self):
return int(self._MyClass__value)
原来的 __value 属性是在 MyIntegerSubclass 的直接超类 MyClass 里面赋值的,但是现在,把它上移,放在 MyClass 的超类 MyBaseClass 里面来赋值。于是,MyIntegerSubclass 类里面指向私有变量 self._MyClass__value 的引用就失效了。
foo = MyIntegerSubclass(5)
foo.get_value()
>>>
AttributeError: 'MyIntegerSubc1ass' object has no attribute '_MyClass__value'
一般来说,恰当的做法应该是:宁可叫子类更多地去访问超类的 protected 属性,也别把这些属性设成 private。应该在文档中说明每个 protected 字段的含义,解释哪些字段是可供子类使用的内部 API、哪些字段是完全不应触碰的数据。这种建议信息,不仅可以给其他程序员看,而且也能在将来扩展代码的时候提醒自己,应该如何保证数据安全。
class MyClass(object):
def __init__(se1f, value):
# This stores the user-supplied value for the object.
# It should be coercible to a string. Once assigned for
# the object it should be treated as immutable.
self._value = value
只有一种情况是可以合理使用 private 属性的,那就是用它来避免子类的属性名与超类相冲突。如果子类无意中定义了与超类同名的属性,那么程序就可能出问题。
class ApiClass(object):
def __init__(self):
self._value = 5
def get(self):
return self._value
class Child(ApiClass):
def __init__(self):
super.__init__()
seif._value = 'hello' # Conflicts
a = Child()
print(a.get(), 'and', a._value, 'should be different')
>>>
hello and hello should be different
当超类是公共 API 的一部分时,可能就需要考虑上面这个问题了。由于子类不在开发者的控制范围之内,所以不能通过重构来解决这种冲突。如果属性名称是个很常用的词语(如本例中的 value),那么更容易引发重名现象。为了降低风险,可以在超类中使用 private 属性,以确保子类的属性名不会与之重复。
class ApiClass(object):
def __init__(self):
self.__value = 5
def get(self):
return self.__value
class Child(ApiClass):
def __init__(self):
super().__init__()
self._value = 'hello' # OK!
a = Child()
print(a.get(), 'and', a._value, 'are different'
>>>
5 and hello are different
总结
- Python 编译器无法严格保证 private 字段的私密性。
- 不要盲目地将属性设为 private,而是应该从一开始就做好规划,并允许子类更多地访问超类的内部 API。
- 应该多用 protected 属性,并在文档中把这些字段的合理用法告诉子类的开发者,而不要试图用 private 属性来限制子类访问这些字段。
- 只有当子类不受自己控制时,才可以考虑用 private 属性来避免名称冲突。
第 28 条:继承 collections.abc 以实现自定义的容器类型
大部分的 Python 编程工作,其实都是在定义类。类可以包含数据,并且能够描述出这些数据对象之间的交互方式。Python 中的每一个类,从某种程度上来说都是容器,它们都封装了属性与功能。Python 也直接提供了一些管理数据所用的内置容器类型,例如,list(列表)、tuple(元组)、set(集)、dictionary(字典)等。
如果要设计用法比较简单的序列,那自然就会想到直接继承 Python 内置的 list 类型。例如,要创建一种自定义的列表类型,并提供统计各元素出现频率的方法。
class FrequencyList(list):
def __init__(self, members):
super().__init__(members)
def frequency(self):
counts = {}
for item in self:
counts.setdefault(item, 0)
counts[item] += 1
return counts
上面这个 FrequencyList 类继承了 list,并获得了由 list 所提供的全部标准功能,使得所有 Python 程序员都可以用他们所熟悉的写法来使用这个类。此外,还根据自己的需求,在子类里添加了其他的方法,以定制其行为。
foo = FrequencyList(L'a', 'b', 'a', 'c', 'b', 'a', 'd'])
print('Length is', len(foo))
foo.pop()
print('After pop:', repr(foo))
print('Frequency:', foo.frequency())
>>>
Length is 7
After pop: ['a', 'b', 'a', 'c', 'b', 'a']
Frequency: {'a': 3, 'c': 1, 'b': 2}
现在,假设要编写这么一种对象:它本身虽然不属于 list 子类,但是用起来却和 list 一样,也可以通过下标访问其中的元素。例如,要令下面这个表示二叉树节点的类,也能够像 list 或 tuple 等序列那样来访问。
class BinaryNode(object):
def __init__(seif, value, left=None, right=None):
self.value = value
self.left = left
self.right = right
上面这个类,如何才能够表现得和序列类型一样呢?可以通过特殊方法完成此功能。Python 会用一些名称比较特殊的实例方法,来实现与容器有关的行为。用下标访问序列中的元素时:
bar = [1, 2, 3]
bar[0]
Python 会把访问代码转译为:
bar.__getitem__(0)
于是,提供自己定制的 __getitem__
方法,令 BinaryNode 类可以表现得和序列一样。下面这个方法按深度优先的次序来访问二叉树中的对象:
class IndexableNode(BinaryNode):
def _search(self, count, index):
# ...
# Returns (found, count)
def __getitem__(self, index):
found, _ = self._search(0, index)
if not found:
raise IndexError('Index out of range')
return found.value
构建二叉树的代码,依然与平常一样。
tree = IndexableNode(
10,
left=IndexableNode(
5,
left=IndexableNode(2),
right=IndexabTeNode(
6, right=IndexableNode(7),
right=IndexableNode(
15, left=IndexableNode(11)))
但是访问它的时候,除了可以像普通的二叉树那样进行遍历之外,还可以使用与 list 相同的写法来访问树中的元素。
print('LRR =', tree.left.right.right.value)
print('Index 0 =', tree[0])
print('Index 1 =', tree[1])
print('11 in the tree?', 11 in tree)
print('17 in the tree?', 17 in tree)
print('Tree is', list(tree))
>>>
LRR =7
Index 0 = 2
Index 1 = 5
11 in the tree? True
17 in the tree? False
Tree is [2, 5, 6, 7, 10, 11, 15]
然而只实现 __getitem__
方法是不够的,它并不能使该类型支持想要的每一种序列操作。
len(tree)
>>>
TypeError: object of type 'IndexableNode' has no len()
想要使内置的 len 函数正常运作,就必须在自己定制的序列类型中实现另外一个名叫 __len__
的特殊方法。
class SequenceNode(IndexableNode):
def __len__(self):
_, count = self._search(0, None)
return count
tree = SequenceNode(
# ...
)
print('Tree has %d nodes' % len(tree))
>>>
Tree has 7 nodes
实现了 __len__
方法之后,这个类的功能依然不完整。其他 Python 程序员还希望这个序列能够像 list 或 tuple 那样,提供 count 和 index 方法。这样看来,定义自己的容器类型,似乎要比想象中困难得多。
为了在编写 Python 程序时避免这些麻烦,可以使用内置的 collections.abc 模块。该模块定义了一系列抽象基类,它们提供了每一种容器类型所应具备的常用方法。从这样的基类中继承了子类之后,如果忘记实现某个方法,那么 collections.abc 模块就会指出这个错误。
from collections.abc import Sequence
class BadType(Sequence):
pass
foo = BadType()
>>>
TypeError: Can't instantiate abstract class BadType with abstract methods __getitem__, __len__
如果子类已经实现了抽象基类所要求的每个方法,那么基类就会自动提供剩下的那些方法。例如,刚才的 SequenceNode 类就满足这一点,于是,它会自动具备 index 和 count 等方法。
class BetterNode(SequenceNode, Sequence):
pass
tree = BetterNode(
# ...
)
print('Index of 7 is', tree.index(7))
print('Count of 10 is', tree.count(10))
>>>
Index of 7 is 3
Count of 10 is 1
对于 Set 和 MutableMapping 等更为复杂的容器类型来说,若不继承抽象基类,则必须实现非常多的特殊方法,才能令自己所定制的子类符合 Python 编程习惯。在这种情况下,继承抽象基类所带来的好处会更加明显。
总结
- 如果要定制的子类比较简单,那就可以直接从 Python 的容器类型(如 list 或 dict)中继承。
- 想正确实现自定义的容器类型,可能需要编写大量的特殊方法。
- 编写自制的容器类型时,可以从 collections.abc 模块的抽象基类中继承,那些基类能够确保我们的子类具备适当的接口及行为。