Избегайте наследования сгенерированных атрибутов класса с использованием метакласса

Я думал об автоматическом добавлении дочерних классов к родительским для "цепочки" с использованием метакласса. Однако наследование этих атрибутов от родительских классов портит дело. Есть хороший способ избежать этого?

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
Другие вопросы по тегам