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


213

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

PEPで理論的根拠を読み込もうとしたが、それは私の頭を超えた。変更可能なシーケンスコンテナーを探していた場合は、を確認する__setitem__か、おそらくそれを使用しようとします(EAFP)。ABCを使用する数値モジュールの実際の使用例はありませんでしたが、これは理解する必要がある最も近いものです。

誰かがその理由を説明してくれませんか?

回答:


162

短縮版

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

ロングバージョン

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

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

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

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

しかし、契約にはより高いレベルの意味論的約束もあります。

たとえば、__str__()メソッドがある場合、オブジェクトの文字列表現を返すことが期待されています。それは可能性があり、オブジェクトのすべての内容を削除し、トランザクションをコミットし、プリンタから空白のページを吐く...しかし、Pythonのマニュアルに記載されたそれは何をすべきかの共通の理解があります。

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

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

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


12
あなたはそこにポイントを持っていると思いますが、私はあなたについていけません。では、コントラクトに関して、実装__contains__するクラスと継承するクラスの違いは何collections.Containerですか?あなたの例では、Pythonでは常にの共通の理解がありました__str__。実装__str__は、ABCから継承して実装するのと同じ約束をします__str__。どちらの場合も、契約を解除できます。静的型付けにあるような証明可能なセマンティクスはありません。
Muhammad Alkarouri、2010

15
collections.Containerは退化したケースで、だけが含まれ\_\_contains\_\_、事前定義された規則を意味するだけです。ABCを使用しても、それだけではそれほど価値はありません。Set継承できるようにするために追加されたのではないかと思います。あなたがにたどり着くまでにSet、突然ABCに属することはかなりの意味論を持っています。アイテムをコレクションに2回含めることはできません。メソッドの存在によっては検出できません。
2010

3
はい、私Setはより良い例だと思いますprint()。意味があいまいで、名前だけではわからないメソッド名を見つけようとしていたので、その名前とPythonのマニュアルだけでは正しいことを実行できるかどうか確信が持てませんでした。
2010

3
Set代わりに例として答えを書き直す可能性はありprintますか?Set@Oddthinking、とても理にかなっています。
Ehtesh Choudhury、2014

1
私はこの記事がそれを非常によく説明していると思います:dbader.org/blog/abstract-base-classes-in-python
szabgab

228

Oddthinkingの答え@間違っていないですが、私はそれはミスだと思う本物の実用的な Pythonはダックタイピングの世界ではいろはを持っている理由を。

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

Pythonのソースコードは例です。これ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__属性を持つクラスは、直接サブクラス化していなくても、コンテナのサブクラスと見なされることを示しています。だから私はこれを書くことができます:

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

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

つまり、適切なインターフェースを実装すれば、あなたはサブクラスです!ABCは、ダックタイピングの精神に忠実でありながら、Pythonでインターフェースを定義する正式な方法を提供します。さらに、これは開閉原理を尊重する方法で機能します。

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

単一のアイテムまたはアイテムのコレクションに作用する多態性関数を作成しているときがあり、同等のブロックisinstance(x, collections.Iterable)よりもはるかに読みやすくなっています。(Pythonを知らなかった場合、この3つのどれがコードの意図を最も明確にするでしょうか?)hasattr(x, '__iter__')try...except

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

* Javaが「伝統的な」オブジェクト指向システムであるかどうかについての議論に入る必要はありません...


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

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には比較的少ない)があります。A __subclasshook__と非抽象メソッドの両方でABCを定義することは避けてください。さらに、__subclasshook__ABCが定義する一連の抽象メソッドと一貫性のある定義を作成する必要があります。


21
「適切なインターフェースを実装すれば、あなたはサブクラスになります」それを本当にありがとう。Oddthinkingがそれを逃したかどうかはわかりませんが、確かに逃しました。FWIWの方isinstance(x, collections.Iterable)がわかりやすく、Pythonを知っています。
Muhammad Alkarouri 2013年

素晴らしいポスト。ありがとうございました。私は補遺は、トラップ「まさにそれをしない」と思い、持っその後、通常のサブクラスの継承を行うことが、少し似ているCサブクラスが削除(または修理を超えて台無し)abc_method()から継承されたがMyABC。主な違いは、サブクラスではなく継承コントラクトを台無しにしているのがスーパークラスであることです。
Michael Scott Cuthbert

Container.register(ContainAllTheThings)与えられた例が機能するために行う必要はありませんか?
BoZenKhaa

1
@BoZenKhaa回答のコードは機能します。それを試してみてください!の意味__subclasshook__は、「この述部を満たすクラスは、ABCに登録されているかどうかに関係なく、また直接のサブクラスかどうかに関係なく、目的isinstanceissubclassチェックのためにサブクラスと見なされます」です。答えで述べたように、適切なインターフェースを実装すれば、あなたはサブクラスです!
ベンジャミンホジソン

1
書式を斜体から太字に変更する必要があります。「適切なインターフェースを実装すれば、あなたはサブクラスです!」非常に簡潔な説明。ありがとうございました!
marti 2018年

108

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に感謝


5
例とリンクはどちらも非常に役に立ちました。ありがとう!
nalyd88

Baseが別のファイルで定義されている場合は、Base.Baseとして継承するか、インポート行を「from base import Base」に変更する必要があります
Ryan Tennill

Python3では構文が少し異なることに注意してください。この回答を参照してください。stackoverflow.com/questions/28688784/...を
PandasRocks

7
C#のバックグラウンドから来ているため、抽象クラスを使用するのはこのためです。あなたは機能を提供していますが、その機能にはさらに実装が必要であると述べています。他の答えはこの点を見逃しているようです。
ジョシュノエ

18

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


6

抽象メソッドは、親クラスで呼び出すメソッドが子クラスに表示される必要があることを確認します。以下は、抽象を呼び出して使用する通常の方法です。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()が呼び出されました」


1
ありがとう。それは上記のケルベロスの答えと同じ点ではありませんか?
ムハンマドアルカローリ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.