web-dev-qa-db-ja.com

Pythonのインターフェース、多重継承と自家製ソリューション

Pythonフレームワークを書いています。クラスにいくつかのプロパティがあることを確認するために、次のような基本的な「インターフェース」クラスを作成します。

_class BananaContainer:
    def __init__(self):
        self._bananas = []
    @property
    def bananas(self):
        return self._bananas
_

次に、オブジェクトがバナナのコンテナーであると想定されている場合は、BananaContainerから派生させます。

オブジェクトが複数のコンテナである場合、潜在的な問題が発生します。私のチームでは、多重継承が適切な方法であるかどうか、または代替ソリューションが存在するかどうかを自問しています。

  • BananaContainerAppleContainerの両方からオブジェクトを継承することは正しいですか?
    • メソッド解決の順序や名前の衝突などの問題で後で私たちを襲うことができますか? (たとえば、_.name_などの同様のプロパティを持たないように注意することができます。これらのインターフェイスを必要最小限に制限したいので、それ以上は行いません)

代替の実装は、多重継承ではなく構成に基づく「機能」を介して提案されます。提案されている例については、以下を参照してください。

_class BaseCapabilities:
    EXISTING_CAPABILITIES = {"banana": BananaContainer, "Apple": AppleContainer, ...}

    def get_capability(self, name):
        if name in self.capabilities():
            return EXISTING_CAPABILITIES[name](self)
    def capabilities(self):
        # should return a list of capabilities
        raise NotImplementedError
_

各オブジェクトは1つのクラスからのみ派生する必要がありますが、機能機構を実装する必要があります。

_ class MyContainer(BaseCapabilities):
     def capabilities(self):
         return ["banana", "Apple"]
_

次に、フレームワーク内で、オブジェクトに必要な機能があるかどうかを確認し、コンテナークラスのインスタンスを取得できます。これは、有害と見なされる多重継承を回避するためです。

私が好む選択は、多重継承を行い、isinstance(obj, BananaContainer)を使用して、たとえばobjがバナナコンテナーかどうかを知ることです。しかし、同僚の議論も理解しています。

私はこの質問について決定を下すためにいくつかの助けを借りてとても感謝しています。

1
mguijarr

インターフェイス、抽象基本クラス、ミックスイン、または同様の手法にPythonの多重継承(MI)を使用することは、まったく問題ありません。ほとんどの場合、MROは直感的な結果を生成します。

ただし、多重継承のもとでのオブジェクトの初期化は非常に注意が必要です。 Pythonでは、参加するすべてのクラスがMI用に設計されていない限り、MIごとに複数のクラスを組み合わせることができません。問題は、__init__()メソッドがどのクラスから呼び出されるかを認識できないことですsuper().__init__()メソッドのシグネチャはどうなるかということです。事実上、これはMIコンストラクタが次のことを意味することを意味します。

  • super().__init__()を呼び出す必要があります
  • 引数は、位置ではなく名前でのみ受け取る必要があります
  • _**kwargs_をsuper().__init__()に転送する必要があります
  • 予期しない引数について警告してはなりません

可能であれば、より良い代替策は、インターフェイスのようなクラスの__init__()メソッドを回避し、代わりに、抽象メソッドを介して要件を表現することです。たとえば、BananaContainerクラスの代わりに、次のインターフェイス/ ABCを記述できます。

_import abc  # abstract base class

class BananaContainer(abc.ABC):
  @property
  @abc.abstractmethod
  def bananas(self) -> list:
    raise NotImplementedError
_

クラスがBananaContainerになりたい場合は、そのプロパティを実装する必要があります。

一般に、複数のインターフェースまたはミックスインから継承するクラスがある場合は、まったく問題ありません。名前の衝突、上記の__init__()の問題、およびクラスの一般的なAPIの膨張を除いて、注目すべき問題は発生しません。


質問の2番目の部分では、継承を使用する代わりに、機能ベースのアプローチを提案しています。多くの場合、継承の代わりに構成を使用することは非常に良いアイデアです。たとえば、設計により初期化の問題を排除します。また、ナビゲートが容易になり、名前の衝突を回避できる、より明示的なAPIにつながる傾向があります。機能を表すオブジェクトを返すか、機能がサポートされていない場合はNoneを返すメソッドが必要です。

ただし、これらの機能はさまざまな方法で実装できます。通常の構成を使用するか、独自のデータ構造に機能を格納します。

  • オブジェクトモデルに特別なニーズがない限り、言語にこだわってください。通常のオブジェクトフィールドにメソッドを格納し、それらにアクセスするための通常のメソッドを提供します。これはより快適なAPIにつながり、オートコンプリーターとタイプチェッカーをサポートする可能性が高くなります。

  • オブジェクトの使用可能な機能を変更する必要がある場合実行時で、実行時に新しい種類の機能を導入する必要がある場合は、辞書を使用するのが適切な場合があります。しかし、この時点であなたはあなた自身のオブジェクトシステムを発明しています。これは良いアイデアかもしれません。新しい機能が構成ファイルで定義される複雑な機能システムを持つゲーム。

    ほとんどのソフトウェアにはこれらの要件はなく、そのような柔軟性のメリットはありません。

    さらに、Pythonの組み込みオブジェクトシステムは柔軟性が高いため、新しいオブジェクトシステムを作成しなくても、新しい型やメソッドを作成できます。 getattr()setattr()hasattr()などの組み込み関数、およびtype()コンストラクタがここで役立ちます。

AppleContainerとBananaContainerの両方の機能を持つことができるオブジェクトを次のように表現します。

_class BananaContainer:
  ...

class AppleContainer:
  ...

class HasCapabilities:
  def __init__(self, x, y, z):
    # somehow determine the appropriate capabilities and initialize them
    self._banana_container = BananaContainer(y) if x else None
    self._Apple_container = AppleContainer(y)

  @property
  def as_banana_container(self) -> Optional[BananaContainer]:
    return self._banana_container

  @property
  def as_Apple_container(self) -> Optional[AppleContainer]:
    return self._Apple_container

o = HasCapabilities(...)
bc = o.as_banana_container
if bc is not None:
  bc.do_banana_things()
_

またはPython 3.8代入式:

_if (bc := o.as_banana_container) is not None:
  bc.do_banana_things()
_

機能を介したリフレクションのためのカスタムメカニズムが必要な場合は、このソリューションの上に、ボイラープレートをいくらか追加して実装できます。 MIセーフにしたい場合は、機能を持つすべてのクラスが継承する必要がある次の基本クラスを宣言します。

_class CapabilityReflection:
  # a base implementations so that actual implementations
  # can safely call super()._get_capabilities()
  def _list_capabilities(self):
    return ()

  def all_capabilities(self):
    """deduplicated set of capabilities that this object supports."""
    set(self._list_capabilities())

  def get_capability(self, captype):
    """find a capability by its type. Returns None if not supported."""
    return None
_

上記の場合、次のように実装されます。

_class HasCapabilities(CapabilityReflection):
  ...
  def _list_capabilities(self):
    caps = [  # go through properties in case they have been overridden
      self.as_banana_container,
      self.as_Apple_container,
    ]
    yield from (cap for cap in caps if cap is not None)
    yield from super()._list_capabilities()

  def get_capability(self, captype):
    if captype == BananaContainer:
      return self.as_banana_container
    if captype == AppleContainer:
      return self.as_Apple_container
    return super().get_capability(captype)
_
3
amon

クラスにいくつかのプロパティがあることを確認するために、基本的な「インターフェース」クラスを作成します

これは静的に型付けされた言語で一般的な設計パターンですが、Pythonプログラマーはクラスに duck型付け を使用する方がより慣用的であると見なします。言語は動的に型付けされるため、 FooクラスとBarクラスの両方にバナナを含めることができる場合は、どちらかを指定できる変数でunknown.bananaを自由に呼び出すことができます。バナナを実装していないオブジェクトがunknownである可能性がある場合は、getattrまたはtry/except AttributeErrorブロック。明示的なインターフェイスは、言語がすでにサポートしている機能を膨らませただけです。

何らかの理由でこれらのインターフェースを削除したくない場合は、少なくとも多重継承を使用できます。それは用途があり、多くの場合に使用することが正しいために存在します。

メソッド解決の順序や名前の衝突などの問題で後で私たちを襲うことができますか?

多重継承宣言では、シンボルの衝突に関しては、最初のオブジェクトが優先されます。場合によっては、これは機能ですが、子クラスでメソッドとプロパティを定義するときのように、これが意図しないオーバーライドを引き起こさないように注意する必要があります。

機能の提案は、継承メカニズムよりも過度に防御的です。誤って上書きしないようにするのはあなたの責任ですが、大きな負担にはなりません。それが1つである場合は、他の問題がある可能性があります。また、クラスに含まれているシンボルが不明で、ブラックボックスとして使用したい場合は、 composition を使用するのが適切です。

2
Arthur Havlicek