python: 抽象基类的 __init__():初始化还是验证?

17 投票
3 回答
18988 浏览
提问于 2025-04-16 12:37

class ABC 是一个“抽象基类”。class X 是它的子类。

在任何 ABC 的子类中,都有一些工作需要完成,这些工作很容易被忘记或者做错。我希望 ABC.__init__() 能帮助发现这些错误,方法有两种:

(1) 开始进行这些工作,或者

(2) 验证这些工作是否正确。

这会影响到 super().__init__() 是在 X.__init__() 的开始部分调用,还是在结束部分调用。

下面是一个简化的例子来说明这个问题:

假设每个 ABC 的子类都必须有一个属性 registry,而且它必须是一个列表。ABC.__init__() 可以选择 (1) 初始化 registry,或者 (2) 检查它是否被正确创建。以下是每种方法的示例代码。

方法一:在 ABC 中初始化

class ABC:
    def __init__(self):
        self.registry = []

class X:
    def __init__(self):
        super().__init__()
        # populate self.registry here
        ...

方法二:在 ABC 中验证

class ABC:
    class InitializationFailure(Exception):
        pass
    def __init__(self):
        try:
            if not isinstance(self.registry, list):
                raise ABC.InitializationError()
        except AttributeError:
            raise ABC.InitializationError()

class X:
    def __init__(self):
        self.registry = []
        # populate self.registry here
        ...
        super().__init__()

哪种设计更好呢?

3 个回答

1

首先,这种设计更好,因为子类不需要知道你是用列表来实现注册的。例如,你可以提供一个叫做 _is_in_registry 的函数,它接受一个参数,然后返回这个元素是否在注册中。这样的话,后来你可以修改父类,把列表换成集合,因为在注册中每个元素只能出现一次,而你不需要去改动子类。

而且,这样的代码更少:想象一下,如果在 ABC 中有100个这样的字段,而 ABC 又有100个像 X 这样的子类……

2

在你提供的例子中,我会按照你提到的第一种方法来做。不过,我会把类 ABC 主要看作是 X 以及其他实现某个接口的类的一个辅助工具。这个接口包含了一个叫做 'registry' 的属性。

从逻辑上讲,你应该区分一下 X 和其他类共享的接口,以及帮助你实现这个接口的基类。也就是说,要单独定义一个接口(比如叫 "ABC"),它提供一个列表 "registry"。然后,你可以考虑把这个接口的实现提取出来,作为一个公共的基类(概念上可以理解为一个混合类),这样可以方便地为接口 ABC 添加新的实现类(除了 X 之外)。

补充一下:关于防止实现类出错,我建议通过单元测试来解决。我觉得这样比试图在你的实现中考虑所有情况要全面得多 :)

16

当然,大家更喜欢方法1而不是方法2,因为方法2把基础类变成了一个标签接口,而没有实现抽象功能。不过,方法1本身并不能完全达到你的目标,那就是防止子类开发者忘记正确调用super(),从而确保初始化。

你可能想了解一下“工厂”模式,这样可以减少子类实现者忘记初始化的可能性。考虑一下:

class AbstractClass(object):
    '''Abstract base class template, implementing factory pattern through 
       use of the __new__() initializer. Factory method supports trivial, 
       argumented, & keyword argument constructors of arbitrary length.'''

   __slots__ = ["baseProperty"]
   '''Slots define [template] abstract class attributes. No instance
       __dict__ will be present unless subclasses create it through 
       implicit attribute definition in __init__() '''

   def __new__(cls, *args, **kwargs):
       '''Factory method for base/subtype creation. Simply creates an
       (new-style class) object instance and sets a base property. '''
       instance = object.__new__(cls)

       instance.baseProperty = "Thingee"
       return instance

这个基础类可以比方法1更简单地扩展,只需要三行代码,像这样:

class Sub(AbstractClass):
   '''Subtype template implements AbstractClass base type and adds
      its own 'foo' attribute. Note (though poor style, that __slots__
      and __dict__ style attributes may be mixed.'''

   def __init__(self):
       '''Subtype initializer. Sets 'foo' attribute. '''
       self.foo = "bar"

注意,虽然我们没有调用父类的构造函数,但baseProperty会被初始化:

Python 2.6.1 (r261:67515, Jun 24 2010, 21:47:49) 
[GCC 4.2.1 (Apple Inc. build 5646)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from TestFactory import *
>>> s = Sub()
>>> s.foo
'bar'
>>> s.baseProperty
'Thingee'
>>> 

正如它的注释所说,基础类AbstractClass不一定要使用slots,它也可以在new()初始化器中“隐式”定义属性。例如:

instance.otherBaseProperty = "Thingee2"

这样也能正常工作。还要注意,基础类的初始化器支持子类中的简单(无参数)初始化器,以及可变参数和关键字参数的初始化器。我建议总是使用这种形式,因为它在最简单的(简单构造函数)情况下不会强加语法,但又允许在不增加维护负担的情况下实现更复杂的功能。

撰写回答