4.1 抽象基类

    抽象基类的核心定义在一个名为abc的模块中。模块中包括了创建抽象基类需要的修饰符和元类型。其他的类也依赖于这些定义。

    在Python 3.2版本中,集合的抽象基类定义在collections中。但是,在Python 3.3版本中,抽象基类被分离到了一个独立的模块collections.abc中。

    我们还会介绍numbers模块,因为它包含了数值类型的抽象基类的定义。io模块包含了I/O的抽象基类。

    我们主要基于Python 3.3版本讨论。Python 3.3版本中的定义对Python 3.2版本也同样适用,只是由于库的结构有一些变化,需要稍微修改一下import语句。

    一个抽象基类具有以下特性。

    • 抽象意味着这些类中不包括我们需要的所有方法的定义。为了让它成为一个真正有用的子类,我们需要提供一些方法定义。
    • 基类意味着其他类会把它当作基类来使用。
    • 抽象类本身提供了一些方法的定义。更重要的是,抽象基类为缺失的方法函数提供了方法签名。子类必须提供正确的方法来创建符合抽象类定义的接口的具体类。

    这些抽象类的特性是基于下面的初衷设计的。

    • 我们可以用它们为 Python 的内部类和我们的程序中的自定义类定义一组一致的基类。
    • 我们可以用它们创建一些通用的、可重用的抽象。
    • 我们可以用它们来支持适当的类检查,以确定一个类的功能。这让我们的自定义类可以与库中的类更好地协作。为了正确地实现类检查,对于像“容器”和“数值”这种概念,我们最好可以有正式的定义。

    如果没有抽象基类,一个容器不一定能够为Sequence类提供一致的特性。这经常会导致我们得到一个很像Sequence的类。反过来,这也会带来一些奇怪的不一致的行为,而对于一个没有为Sequence提供完整实现的类,这会给出一些笨拙的解决办法。

    使用抽象基类,你就可以保证一个给定的类会有所有我们预期的行为。如果这个类没有实现其中的一个特性,那么这个未定义的抽象方法会造成无法使用这个类构建对象实例。

    我们在以下这些情况下会考虑使用抽象基类。

    • 当我们自定义类时,使用抽象基类作为基类。
    • 我们在一个方法中使用抽象基类来确保一种操作是可行的。
    • 我们在诊断信息或者异常中使用抽象基类来指出一种操作为什么不能生效。

    对于第1种情况,我们可能会用下面的方式创建模块。

    import collections.abc
    class SomeApplicationClass( collections.abc.Callable ):
      pass

    SomeApplicationClasss定义为一个Callable类。接着,它必须实现可回调对象需要的方法,否则我们无法使用它来创建实例。

    一个函数是一个Callable类的具体例子。抽象基类是定义了call()方法的类。在后面的部分和第5章“可调用对象和上下文的使用”中,我们会介绍Callable类的细节。

    对于第2种情况,我们可能会用下面的方式定义方法。

    defsome_method( self, other ):
      assertisinstance(other, collections.abc.Iterator)

    some_method()方法要求other参数是Iterator的一个子类。如果other参数无法通过这个测试,我们会得到一个异常。另外一种更好的方法是在if语句中抛出一个TypeError。在下面的部分中,我们会看到这种用法。

    对于第3种情况,可以使用以下代码进行判断。

    try:
      someobj.somemethod( another )
    exceptAttributeError:
      warnings.warn( "{0!r} not an Iterator, found {0.class.__bases
    !r}". format(another) )
      raise

    在这个例子中,我们输出了包含给定对象基类的警告信息,这些信息有助于我们调试。