Ответ 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.