Effective Python -- 第 3 章 类与继承(下)

第 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 模块的抽象基类中继承,那些基类能够确保我们的子类具备适当的接口及行为。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值