Избегайте наследования сгенерированных атрибутов класса с использованием метакласса
Я думал об автоматическом добавлении дочерних классов к родительским для "цепочки" с использованием метакласса. Однако наследование этих атрибутов от родительских классов портит дело. Есть хороший способ избежать этого?
class MetaError(type):
def __init__(cls, name, bases, attrs):
for base in bases:
setattr(base, name, cls)
super(MetaError, cls).__init__(name, bases, attrs)
class BaseError(Exception, object):
def __init__(self, message):
super(BaseError, self).__init__(message)
class HttpError(BaseError):
__metaclass__ = MetaError
class HttpBadRequest(HttpError):
pass
class HttpNotFound(HttpError):
pass
class FileNotFound(HttpNotFound):
pass
class InvalidJson(HttpBadRequest):
pass
http = HttpError
# now I can do
raise http.HttpNotFound('Not found')
raise http.HttpNotFound.FileNotFound('File not found')
raise http.HttpBadRequest.InvalidJson('Invalid json')
# unfortunately this also works
raise http.HttpBadRequest.HttpBadRequest('Bad request')
raise http.HttpBadRequest.HttpNotFound('Not found')
2 ответа
Что ж, это оказывается сложнее, чем кажется на первый взгляд - потому что в основном вы хотите иметь отношения наследования классов, но не используете обычные пути поиска атрибутов для наследования классов - в противном случае HTTPError, например, является подклассом BaseError, всегда будет иметь все атрибуты, присутствующие в самом BaseError - Таким образом, цепочка BaseError.HTTPError.HTTPError.HTTPError.HTTPError...
всегда будет действительным.
К счастью, Python действительно предлагает механизм для регистрации классов как подклассов других, без "физического" наследования, то есть он сообщается как подкласс, но не имеет родительского класса в своих базах или __mro__
- и, следовательно, поиск атрибутов в производном классе (принятый?) не выполняет поиск атрибутов в "приемном" родителе.
Этот механизм предоставляется через " абстрактные базовые классы" или "abc", через его метакласс ABCMeta и метод "register".
И теперь, в связи с тем, что вы также, вероятно, хотите объявить иерархию классов с обычным синтаксисом наследования - то есть, возможность писать class HTTPError(BaseError):
чтобы указать, что новый класс происходит от BaseError - вы получаете фактическое "физическое" наследование.
Таким образом, мы можем наследовать от класса ABCMeta (вместо type
) и напиши __new__
метод, чтобы исключить физическое наследование - и мы используем setattr
для локализации, которую вы намеревались использовать с помощью своего кода, а также мы вызываем необходимый вызов parentclass.register
прямо на метаклассе.
(Обратите внимание, что, поскольку мы сейчас меняем базовые классы, нам нужно __new__
метод метакласса, а не на __init__
:
from abc import ABCMeta
class MetaError(ABCMeta):
def __new__(metacls, name, bases, attrs):
new_bases = []
base_iter = list(reversed(bases))
seen = []
register_this = None
while base_iter:
base = base_iter.pop(0)
if base in seen:
continue
seen.append(base)
if isinstance(base, MetaError):
register_this = base
base_iter = list(reversed(base.__mro__)) + base_iter
else:
new_bases.insert(0, base)
cls = super(MetaError, metacls).__new__(metacls, name, tuple(new_bases), attrs)
if register_this:
setattr(register_this, name, cls)
register_this.register(cls)
return cls
И для быстрого теста:
class BaseError(Exception):
__metaclass__ = MetaError
class HTTPError(BaseError):
pass
class HTTPBadRequest(HTTPError):
pass
В интерактивном режиме проверьте, работает ли он так, как вы собираетесь:
In [38]: BaseError.HTTPError
Out[38]: __main__.HTTPError
In [39]: BaseError.HTTPError.HTTPError
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-39-5d5d03751646> in <module>()
----> 1 BaseError.HTTPError.HTTPError
AttributeError: type object 'HTTPError' has no attribute 'HTTPError'
In [40]: HTTPError.__mro__
Out[40]: (__main__.HTTPError, Exception, BaseException, object)
In [41]: issubclass(HTTPError, BaseError)
Out[41]: True
In [42]: issubclass(HTTPBadRequest, BaseError)
Out[42]: True
In [43]: BaseError.HTTPError.HTTPBadRequest
Out[43]: __main__.HTTPBadRequest
In [44]: BaseError.HTTPBadRequest
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-44-b40d65ca66c6> in <module>()
----> 1 BaseError.HTTPBadRequest
AttributeError: type object 'BaseError' has no attribute 'HTTPBadRequest'
И затем, самое главное, проверка, действительно ли иерархия исключений работает следующим образом:
In [45]: try:
....: raise HTTPError
....: except BaseError:
....: print("it works")
....: except HTTPError:
....: print("not so much")
....:
it works
Несколько замечаний: не нужно наследовать от обоих Exception
а также object
явно - Exception
само по себе уже наследует object
, И, самое главное: независимо от того, над каким проектом вы работаете, сделайте все возможное, чтобы переместить его в Python 3.x вместо Python 2. В Python 2 подсчитываются дни, и в Python 3 появилось много-много новых возможностей, которыми вы являетесь. исключая себя от использования. (Код в этом ответе совместим с Python 2/3, но для __metaclass__
декларация об использовании конечно).
Довольно наивное глобальное картографическое решение, которое также, кажется, работает:
m = {}
class MetaError(type):
def __init__(cls, name, bases, attrs):
for base in bases:
m[(base, name)] = cls
super(MetaError, cls).__init__(name, bases, attrs)
def __getattribute__(self, value):
if (self, value) in m:
return m[self, value]
return type.__getattribute__(self, value)
class BaseError(Exception):
__metaclass__ = MetaError
class HttpError(BaseError):
pass
class HttpBadRequest(HttpError):
pass
class HttpNotFound(HttpError):
pass
class FileNotFound(HttpNotFound):
pass
class InvalidJson(HttpBadRequest):
pass