Большинство Pythonic способ объявить абстрактное свойство класса

Предположим, что вы пишете абстрактный класс, и один или несколько его не-абстрактных методов класса требуют, чтобы конкретный класс имел определенный атрибут класса; например, если экземпляры каждого конкретного класса могут быть построены путем сопоставления с другим регулярным выражением, вы можете указать своей ABC следующее:

@classmethod
def parse(cls, s):
    m = re.fullmatch(cls.PATTERN, s)
    if not m:
        raise ValueError(s)
    return cls(**m.groupdict())

(Возможно, это может быть лучше реализовано с помощью пользовательского метакласса, но попробуйте проигнорировать это для примера.)

Теперь, поскольку переопределение абстрактных методов и свойств проверяется на время создания экземпляра, а не на время создания подкласса, пытаясь использовать abc.abstractmethod, чтобы гарантировать, что конкретные классы имеют атрибуты PATTERN, не будут работать - но, конечно, должно быть что-то чтобы рассказать кому-то, кто смотрит на ваш код "Я не забыл определить PATTERN на ABC, конкретные классы должны определять их собственные". Возникает вопрос: что-то самое питоническое?

  • Куча декораторов

    @property
    @abc.abstractmethod
    def PATTERN(self):
        pass
    

    (Предположим, например, Python 3.4 или новее). Это может очень ввести в заблуждение читателей, так как это означает, что PATTERN должно быть свойством экземпляра, а не атрибутом класса.

  • Башня декораторов

    @property
    @classmethod
    @abc.abstractmethod
    def PATTERN(cls):
        pass
    

    Это может быть очень запутанным для читателей, поскольку @property и @classmethod обычно не могут комбинироваться; они работают только здесь (для заданного значения "работа" ), потому что метод игнорируется после переопределения.

  • Значение фиктивного

    PATTERN = ''
    

    Если конкретный класс не может определить свой собственный PATTERN, parse будет принимать только пустой ввод. Этот параметр не является широко применимым, поскольку не все варианты использования будут иметь соответствующее фиктивное значение.

  • Ошибка, вызывающая фиктивное значение

    PATTERN = None
    

    Если конкретный класс не может определить свой собственный PATTERN, parse вызовет ошибку, и программист получит то, что заслуживает.

  • Ничего не делать. В принципе, более хардкорный вариант # 4. В ABC docstring может быть записка где-то, но сама ABC не должна иметь ничего похожего на атрибут PATTERN.

  • Другое???

Ответ 1

Python >= 3.6 Версия

(Прокрутите вниз для версии, которая работает для Python <= 3.5).

Если вам посчастливилось использовать Python 3.6 и не нужно беспокоиться о обратной совместимости, вы можете использовать новый __init_subclass__, который был введен в Python 3.6 на упростить настройку класса, не прибегая к метаклассам. При определении нового класса он вызывается как последний шаг перед созданием объекта класса.

На мой взгляд, самый питонический способ использования этого заключается в том, чтобы сделать декоратор класса, который принимает атрибуты для создания абстрактных, тем самым делая его явным для пользователя, что им нужно определить.

from custom_decorators import abstract_class_attributes

@abstract_class_attributes('PATTERN')
class PatternDefiningBase:
    pass

class LegalPatternChild(PatternDefiningBase):
    PATTERN = r'foo\s+bar'

class IllegalPatternChild(PatternDefiningBase):
    pass

Трассировка может быть следующей и происходит в момент создания подкласса, а не время создания.

NotImplementedError                       Traceback (most recent call last)
...
     18     PATTERN = r'foo\s+bar'
     19 
---> 20 class IllegalPatternChild(PatternDefiningBase):
     21     pass

...

<ipython-input-11-44089d753ec1> in __init_subclass__(cls, **kwargs)
      9         if cls.PATTERN is NotImplemented:
     10             # Choose your favorite exception.
---> 11             raise NotImplementedError('You forgot to define PATTERN!!!')
     12 
     13     @classmethod

NotImplementedError: You forgot to define PATTERN!!!

Перед тем, как продемонстрировать, как работает декоратор, поучительно показать, как вы могли бы реализовать это без декоратора. Приятно, что при необходимости вы можете сделать свой базовый класс абстрактным базовым классом без необходимости выполнять какую-либо работу (просто наследовать от abc.ABC или сделать метакласс abc.ABCMeta).

class PatternDefiningBase:
    # Dear programmer: implement this in a subclass OR YOU'LL BE SORRY!
    PATTERN = NotImplemented

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)

        # If the new class did not redefine PATTERN, fail *hard*.
        if cls.PATTERN is NotImplemented:
            # Choose your favorite exception.
            raise NotImplementedError('You forgot to define PATTERN!!!')

    @classmethod
    def sample(cls):
        print(cls.PATTERN)

class LegalPatternChild(PatternDefiningBase):
    PATTERN = r'foo\s+bar'

Вот как может быть реализован декоратор.

# custom_decorators.py

def abstract_class_attributes(*names):
    """Class decorator to add one or more abstract attribute."""

    def _func(cls, *names):
        """ Function that extends the __init_subclass__ method of a class."""

        # Add each attribute to the class with the value of NotImplemented
        for name in names:
            setattr(cls, name, NotImplemented)

        # Save the original __init_subclass__ implementation, then wrap
        # it with our new implementation.
        orig_init_subclass = cls.__init_subclass__

        def new_init_subclass(cls, **kwargs):
            """
            New definition of __init_subclass__ that checks that
            attributes are implemented.
            """

            # The default implementation of __init_subclass__ takes no
            # positional arguments, but a custom implementation does.
            # If the user has not reimplemented __init_subclass__ then
            # the first signature will fail and we try the second.
            try:
                orig_init_subclass(cls, **kwargs)
            except TypeError:
                orig_init_subclass(**kwargs)

            # Check that each attribute is defined.
            for name in names:
                if getattr(cls, name, NotImplemented) is NotImplemented:
                    raise NotImplementedError(f'You forgot to define {name}!!!')

        # Bind this new function to the __init_subclass__.
        # For reasons beyond the scope here, it we must manually
        # declare it as a classmethod because it is not done automatically
        # as it would be if declared in the standard way.
        cls.__init_subclass__ = classmethod(new_init_subclass)

        return cls

    return lambda cls: _func(cls, *names)

Python <= 3.5 Версия

Если вам не повезло использовать Python 3.6 и не нужно беспокоиться о обратной совместимости, вам придется использовать метакласс. Несмотря на то, что это вполне допустимый Python, можно было бы обсудить, как pythonic это решение, потому что метаклассы трудно обернуть вокруг вашего мозга, но я думаю, что он попадает в большинство точек Zen of Python, поэтому я думаю, что это не так уж плохо.

class RequirePatternMeta(type):
    """Metaclass that enforces child classes define PATTERN."""

    def __init__(cls, name, bases, attrs):
        # Skip the check if there are no parent classes,
        # which allows base classes to not define PATTERN.
        if not bases:
            return
        if attrs.get('PATTERN', NotImplemented) is NotImplemented:
            # Choose your favorite exception.
            raise NotImplementedError('You forgot to define PATTERN!!!')

class PatternDefiningBase(metaclass=RequirePatternMeta):
    # Dear programmer: implement this in a subclass OR YOU'LL BE SORRY!
    PATTERN = NotImplemented

    @classmethod
    def sample(cls):
        print(cls.PATTERN)

class LegalPatternChild(PatternDefiningBase):
    PATTERN = r'foo\s+bar'

class IllegalPatternChild(PatternDefiningBase):
    pass

Это ведет себя точно так же, как приведенный выше метод Python >= 3.6 __init_subclass__ (за исключением того, что трассировка будет выглядеть немного по-другому, поскольку перед ее прохождением маршрутизируется через другой набор методов).

В отличие от метода __init_subclass__, если вы хотите сделать подкласс абстрактным базовым классом, вам нужно будет выполнить лишь немного дополнительной работы (вам нужно будет составить метакласс с ABCMeta).

from abs import ABCMeta, abstractmethod

ABCRequirePatternMeta = type('ABCRequirePatternMeta', (ABCMeta, RequirePatternMeta), {})

class PatternDefiningBase(metaclass=ABCRequirePatternMeta):
    # Dear programmer: implement this in a subclass OR YOU'LL BE SORRY!
    PATTERN = NotImplemented

    @classmethod
    def sample(cls):
        print(cls.PATTERN)

    @abstractmethod
    def abstract(self):
        return 6

class LegalPatternChild(PatternDefiningBase):
    PATTERN = r'foo\s+bar'

    def abstract(self):
        return 5

class IllegalPatternChild1(PatternDefiningBase):
    PATTERN = r'foo\s+bar'

print(LegalPatternChild().abstract())
print(IllegalPatternChild1().abstract())

class IllegalPatternChild2(PatternDefiningBase):
    pass

Выводится так, как вы ожидали.

5
TypeError: Can't instantiate abstract class IllegalPatternChild1 with abstract methods abstract
# Then the NotImplementedError if it kept on going.