抽象クラスを作成していて、その非抽象クラスメソッドの1つ以上で、具象クラスに特定のクラス属性が必要であるとします。たとえば、各具象クラスのインスタンスを異なる正規表現と照合することで作成できる場合、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
属性があることを確認しようとしても機能しませんが、 besomethingコードを見ている人に「ABCでPATTERN
を定義するのを忘れていませんでした。具体的なクラスは自分で定義することになっています。」問題は次のとおりです:最も何かは最もPythonicですか?
デコレータの山
@property
@abc.abstractmethod
def PATTERN(self):
pass
(ちなみに、Python 3.4以上と仮定してください。)これは、PATTERN
がクラス属性ではなくインスタンスプロパティである必要があることを意味するため、読者を誤解させる可能性があります。
デコレータの塔
@property
@classmethod
@abc.abstractmethod
def PATTERN(cls):
pass
@property
と@classmethod
は通常組み合わせることはできないため、これは読者を混乱させる可能性があります。メソッドがオーバーライドされると無視されるため、ここでは(特定の「work」の値に対して)一緒にのみ機能します。
ダミー値
PATTERN = ''
具象クラスが独自のPATTERN
を定義できない場合、parse
は空の入力のみを受け入れます。すべてのユースケースに適切なダミー値があるわけではないため、このオプションは広く適用できません。
エラーを引き起こすダミー値
PATTERN = None
具象クラスが独自のPATTERN
の定義に失敗した場合、parse
はエラーを発生させ、プログラマーは必要なものを取得します。
何もしない。基本的に#4のよりハードコアなバリアント。 ABCのdocstringのどこかにメモがある可能性がありますが、ABC自体にはPATTERN
属性のように何も含めるべきではありません。
その他???
Python> = 3.6バージョン
(Python <= 3.5)で機能するバージョンを下にスクロールします)。
幸運にもPython 3.6を使用するだけで十分であり、後方互換性を気にする必要がない場合は、導入された新しい __init_subclass__
メソッドを使用できます。 in Python 3.6 to メタクラスに頼らずにカスタマイズクラスの作成を容易にする 。新しいクラスを定義するとき、それはクラスオブジェクトが作成される前の最後のステップとして呼び出されます。
私の意見では、これを使用する最もPython的な方法は、属性を受け入れて抽象化するクラスデコレーターを作成し、ユーザーに定義する必要があることをユーザーに明示することです。
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解決策は、メタクラスが頭を回るのが難しいためですが、それは The 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.