web-dev-qa-db-ja.com

抽象クラスプロパティを宣言する最もPython的な方法

抽象クラスを作成していて、その非抽象クラスメソッドの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ですか?

  1. デコレータの山

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

    (ちなみに、Python 3.4以上と仮定してください。)これは、PATTERNがクラス属性ではなくインスタンスプロパティである必要があることを意味するため、読者を誤解させる可能性があります。

  2. デコレータの塔

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

    @property@classmethodは通常組み合わせることはできないため、これは読者を混乱させる可能性があります。メソッドがオーバーライドされると無視されるため、ここでは(特定の「work」の値に対して)一緒にのみ機能します。

  3. ダミー値

    PATTERN = ''
    

    具象クラスが独自のPATTERNを定義できない場合、parseは空の入力のみを受け入れます。すべてのユースケースに適切なダミー値があるわけではないため、このオプションは広く適用できません。

  4. エラーを引き起こすダミー値

    PATTERN = None
    

    具象クラスが独自のPATTERNの定義に失敗した場合、parseはエラーを発生させ、プログラマーは必要なものを取得します。

  5. 何もしない。基本的に#4のよりハードコアなバリアント。 ABCのdocstringのどこかにメモがある可能性がありますが、ABC自体にはPATTERN属性のように何も含めるべきではありません。

  6. その他???

21
jwodder

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.
14
SethMMorton