python: абстрактный базовый класс ' __init__(): инициализация или проверка?
class ABC
это "абстрактный базовый класс". class X
это его подкласс.
Есть некоторая работа, которая должна быть сделана в любом подклассе ABC
, который легко забыть или сделать неправильно. мне бы хотелось ABC.__init__()
чтобы помочь поймать такие ошибки с помощью:
(1) начать эту работу или (2) подтвердить ее
Это влияет ли super().__init__()
вызывается в начале или в конце X.__init__()
,
Вот упрощенный пример для иллюстрации:
Предположим, что каждый подкласс ABC
должен иметь атрибут registry
и это должен быть список. ABC.__init__()
можно либо (1) инициализировать registry
или (2) проверьте, что он был правильно создан. Ниже приведен пример кода для каждого подхода.
Подход 1: инициализировать в ABC
class ABC:
def __init__(self):
self.registry = []
class X:
def __init__(self):
super().__init__()
# populate self.registry here
...
Подход 2: подтвердить в 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 подходу 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, используя только три (3) строки кода san-commment следующим образом:
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 не нужно использовать слоты, он может так же легко "неявно" определять атрибуты, устанавливая их в своем инициализаторе new(). Например:
instance.otherBaseProperty = "Thingee2"
будет работать нормально. Также обратите внимание, что инициализатор базового класса поддерживает тривиальные (без аргументов) инициализаторы в своих подтипах, а также инициализаторы аргументов переменной длины и аргументов с ключевыми словами. Я рекомендую всегда использовать эту форму, так как она не навязывает синтаксис в простейшем (тривиальном конструкторе) случае, но допускает более сложную функциональность без наложения обслуживания.
В приведенном вами примере я бы сделал это так же, как в вашем подходе 1. Однако я бы видел класс ABC главным образом как помощник для реализации X и других классов, которые реализуют определенный интерфейс. Указанный интерфейс состоит из атрибута "реестр".
Вы должны, по крайней мере логически, различать интерфейс, совместно используемый X и другими классами, и базовый класс, который помогает вам реализовать его. То есть определите отдельно, что существует интерфейс (например, "ABC"), который выставляет список "Registry". Затем вы можете решить реализовать реализацию интерфейса в качестве общего базового класса (концептуально в качестве дополнения) для разработчиков интерфейса ABC, поскольку это позволяет очень легко вводить новые реализующие классы (в дополнение к X).
Изменить: Что касается защиты от ошибок в реализации классов, я бы нацелился на это с помощью модульных тестов. Я думаю, что это более всеобъемлющий, чем пытаться учесть все в вашей реализации:)
Первый - лучший дизайн, потому что подклассам не нужно знать, что вы внедрили реестр со списком. Например, вы могли бы предложить _is_in_registry
функция, которая принимает один аргумент и возвращает наличие элемента в реестре. Затем вы можете позже изменить суперкласс и заменить список на набор, потому что элементы могут появиться только один раз в реестре, и вам не нужно будет менять подклассы.
Кроме того, это меньше кода: представьте, что у вас есть 100 полей, как это в ABC
а также ABC
имеет 100 подклассов, как X
...