web-dev-qa-db-ja.com

Pythonで抽象基本クラスを使用する理由

Pythonでのダックタイピングの古い方法に慣れているため、ABC(抽象基本クラス)の必要性を理解できません。 help は、それらの使用方法に適しています。

PEP の理論的根拠を読み取ろうとしましたが、それは私の頭の上に行きました。可変シーケンスコンテナーを探している場合は、__setitem__を確認するか、それを使用する可能性が高くなります( EAFP )。 numbers モジュールの実際の使用に出会ったことはありませんが、ABCを使用しますが、これが最も理解しやすいものです。

誰も私に理論的根拠を説明できますか?

187

短縮版

ABCは、クライアントと実装されたクラスの間のより高いレベルのセマンティックコントラクトを提供します。

ロングバージョン

クラスとその呼び出し元の間には契約があります。このクラスは、特定のことを行い、特定のプロパティを持つことを約束します。

契約にはさまざまなレベルがあります。

非常に低いレベルでは、コントラクトにはメソッドの名前またはパラメーターの数が含まれる場合があります。

静的に型付けされた言語では、そのコントラクトは実際にはコンパイラーによって強制されます。 Pythonでは、EAFPまたはイントロスペクションを使用して、未知のオブジェクトがこの予想される契約を満たしていることを確認できます。

しかし、コントラクトにはより高いレベルのセマンティックな約束もあります。

たとえば、__str__()メソッドがある場合、オブジェクトの文字列表現を返すことが期待されます。それcouldオブジェクトのすべてのコンテンツを削除し、トランザクションをコミットし、プリンターから空白ページを吐き出します...しかし、Pythonマニュアル。

これは、セマンティックコントラクトがマニュアルに記載されている特殊なケースです。 print()メソッドは何をすべきですか?オブジェクトをプリンターに書き込むか、行を画面に書き込むか、または他の何かを書き込む必要がありますか?それは依存します-あなたはここで完全な契約を理解するためにコメントを読む必要があります。 print()メソッドが存在することを単純に確認するクライアントコードは、契約の一部を確認しました。メソッド呼び出しを行うことができますが、呼び出しのより高いレベルのセマンティクスについては同意しません。

抽象基本クラス(ABC)の定義は、クラスの実装者と呼び出し元の間でコントラクトを作成する方法です。これは単にメソッド名のリストではなく、それらのメソッドが何をすべきかについての共通の理解です。このABCを継承する場合、print()メソッドのセマンティクスを含む、コメントで説明されているすべてのルールに従うことを約束します。

Pythonのダックタイピングには、静的タイピングよりも柔軟性に多くの利点がありますが、すべての問題を解決できるわけではありません。 ABCは、自由形式のPythonと静的に型付けされた言語の束縛と規律の間の中間的なソリューションを提供します。

141
Oddthinking

@Oddthinkingの答えは間違っていませんが、realpractical理由PythonがABCを持っている世界を見逃していると思いますアヒルのタイピング。

抽象メソッドはきちんとしていますが、私の意見では、アヒルのタイピングでまだカバーされていないユースケースを実際には満たしていません。抽象基本クラスの真の力は、 isinstanceおよびissubclass の動作をカスタマイズできる方法にあります。 (__subclasshook__は基本的にPythonの __instancecheck__および__subclasscheck__ フックの上にあるより使いやすいAPIです。)組み込みコンストラクトをカスタム型で動作するように適合させることは、Pythonの哲学の大部分です。

Pythonのソースコードは模範的です。 Here は、標準ライブラリでcollections.Containerがどのように定義されているか(執筆時点):

class Container(metaclass=ABCMeta):
    __slots__ = ()

    @abstractmethod
    def __contains__(self, x):
        return False

    @classmethod
    def __subclasshook__(cls, C):
        if cls is Container:
            if any("__contains__" in B.__dict__ for B in C.__mro__):
                return True
        return NotImplemented

__subclasshook__のこの定義は、__contains__属性を持つクラスは、直接サブクラス化されていなくても、Containerのサブクラスと見なされることを示しています。だから私はこれを書くことができます:

class ContainAllTheThings(object):
    def __contains__(self, item):
        return True

>>> issubclass(ContainAllTheThings, collections.Container)
True
>>> isinstance(ContainAllTheThings(), collections.Container)
True

言い換えれば、適切なインターフェースを実装すれば、あなたはサブクラスになります!ABCは、Pythonでインターフェースを定義する正式な方法を提供する一方で、ダックタイピングの精神に忠実です。また、これは Open-Closed Principle を尊重する方法で機能します。

Pythonのオブジェクトモデルは、表面上はより「伝統的な」OOシステム(つまりJava *)に似ています-yerクラス、yerオブジェクト、yerメソッドがありますが、表面をスクラッチするとはるかにリッチで柔軟なものを見つけるでしょう。同様に、Pythonの抽象基底クラスの概念は、Java開発者に認識される可能性がありますが、実際には非常に異なる目的を意図しています。

私は時々、単一のアイテムまたはアイテムのコレクションに作用するポリモーフィック関数を書いていることに気付きます。また、isinstance(x, collections.Iterable)は、hasattr(x, '__iter__')または同等のtry...exceptブロックよりもはるかに読みやすくなっています。 (Pythonを知らなかった場合、これらの3つのうちどれがコードの意図を明確にしますか?)

そうは言っても、自分でABCを作成する必要はほとんどなく、通常はリファクタリングを通じてABCの必要性を発見します。ポリモーフィック関数が多くの属性チェックを行っている、または多くの関数が同じ属性チェックを行っているのを見ると、その匂いは抽出されるのを待っているABCの存在を示唆しています。

* Javaが「従来の」OOシステムであるかどうかについて議論することなく...


補遺:抽象基本クラスはisinstanceおよびissubclassの動作をオーバーライドできますが、まだ MROを入力しません仮想サブクラスの 。これは、クライアントにとって潜在的な落とし穴です。isinstance(x, MyABC) == TrueのすべてのオブジェクトがMyABCで定義されたメソッドを持つわけではありません。

class MyABC(metaclass=abc.ABCMeta):
    def abc_method(self):
        pass
    @classmethod
    def __subclasshook__(cls, C):
        return True

class C(object):
    pass

# typical client code
c = C()
if isinstance(c, MyABC):  # will be true
    c.abc_method()  # raises AttributeError

残念ながら、これらの「ただやってはいけない」トラップ(Pythonは比較的少ない!):__subclasshook__メソッドと非抽象メソッドの両方でABCを定義することは避けてください。さらに、__subclasshook__の定義を、ABCが定義する一連の抽象メソッドと一致させる必要があります。

205

ABCの便利な機能は、すべての必要なメソッド(およびプロパティ)を実装していない場合、実際に使用しようとすると AttributeError ではなく、インスタンス化時にエラーが発生する可能性があることです。メソッドがありません。

from abc import ABCMeta, abstractmethod

# python2
class Base(object):
    __metaclass__ = ABCMeta

    @abstractmethod
    def foo(self):
        pass

    @abstractmethod
    def bar(self):
        pass

# python3
class Base(object, metaclass=ABCMeta):
    @abstractmethod
    def foo(self):
        pass

    @abstractmethod
    def bar(self):
        pass

class Concrete(Base):
    def foo(self):
        pass

    # We forget to declare `bar`


c = Concrete()
# TypeError: "Can't instantiate abstract class Concrete with abstract methods bar"

https://dbader.org/blog/abstract-base-classes-in-python の例

編集:python3構文を含めるには、@ PandasRocksに感謝します

95
cerberos

プロトコル内のすべてのメソッドの存在を確認することなく、またはサポートされていないために「敵」領域の深い例外をトリガーすることなく、オブジェクトが特定のプロトコルをサポートするかどうかを決定します。

抽象メソッドは、親クラスで呼び出しているメソッドを子クラスに表示する必要があることを確認します。以下は、抽象を呼び出して使用する通常の方法です。 python3で書かれたプログラム

通常の呼び出し方法

class Parent:
def methodone(self):
    raise NotImplemented()

def methodtwo(self):
    raise NotImplementedError()

class Son(Parent):
   def methodone(self):
       return 'methodone() is called'

c = Son()
c.methodone()

「methodone()が呼び出されます」

c.methodtwo()

NotImplementedError

抽象メソッドを使用

from abc import ABCMeta, abstractmethod

class Parent(metaclass=ABCMeta):
    @abstractmethod
    def methodone(self):
        raise NotImplementedError()
    @abstractmethod
    def methodtwo(self):
        raise NotImplementedError()

class Son(Parent):
    def methodone(self):
        return 'methodone() is called'

c = Son()

TypeError:抽象メソッドmethodtwoで抽象クラスSonをインスタンス化できません。

Methodtwoは子クラスで呼び出されないため、エラーが発生しました。適切な実装は次のとおりです

from abc import ABCMeta, abstractmethod

class Parent(metaclass=ABCMeta):
    @abstractmethod
    def methodone(self):
        raise NotImplementedError()
    @abstractmethod
    def methodtwo(self):
        raise NotImplementedError()

class Son(Parent):
    def methodone(self):
        return 'methodone() is called'
    def methodtwo(self):
        return 'methodtwo() is called'

c = Son()
c.methodone()

「methodone()が呼び出されます」

5
sim