python: 抽象基类的 __init__():初始化还是验证?
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 个回答
首先,这种设计更好,因为子类不需要知道你是用列表来实现注册的。例如,你可以提供一个叫做 _is_in_registry
的函数,它接受一个参数,然后返回这个元素是否在注册中。这样的话,后来你可以修改父类,把列表换成集合,因为在注册中每个元素只能出现一次,而你不需要去改动子类。
而且,这样的代码更少:想象一下,如果在 ABC
中有100个这样的字段,而 ABC
又有100个像 X
这样的子类……
在你提供的例子中,我会按照你提到的第一种方法来做。不过,我会把类 ABC 主要看作是 X 以及其他实现某个接口的类的一个辅助工具。这个接口包含了一个叫做 'registry' 的属性。
从逻辑上讲,你应该区分一下 X 和其他类共享的接口,以及帮助你实现这个接口的基类。也就是说,要单独定义一个接口(比如叫 "ABC"),它提供一个列表 "registry"。然后,你可以考虑把这个接口的实现提取出来,作为一个公共的基类(概念上可以理解为一个混合类),这样可以方便地为接口 ABC 添加新的实现类(除了 X 之外)。
补充一下:关于防止实现类出错,我建议通过单元测试来解决。我觉得这样比试图在你的实现中考虑所有情况要全面得多 :)
当然,大家更喜欢方法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"
这样也能正常工作。还要注意,基础类的初始化器支持子类中的简单(无参数)初始化器,以及可变参数和关键字参数的初始化器。我建议总是使用这种形式,因为它在最简单的(简单构造函数)情况下不会强加语法,但又允许在不增加维护负担的情况下实现更复杂的功能。