コンポジションエイリアスを実行するPythonの方法


8

コンポジションエイリアスを実行する最もPython的で正しい方法は何ですか?

架空のシナリオを次に示します。

class House:
    def cleanup(self, arg1, arg2, kwarg1=False):
        # do something

class Person:
    def __init__(self, house): 
        self.house = house
        # aliases house.cleanup
        # 1.
        self.cleanup_house = self.house.cleanup

    # 2.
    def cleanup_house(self, arg1, arg2, kwarg1=False):
        return self.house.cleanup(arg1=arg1, arg2=arg2, kwarg1=kwarg1)

私のテストしたエディターは#1のAFAIKでこれらを#2と同じように理解しています-自動補完、ドキュメント文字列など。

#1アプローチの欠点はありますか?Pythonの観点からどちらがより正しい方法ですか?

メソッド#1を拡張できず、型ヒントのバリアントを追加しても、コメントで指摘されているすべての問題の影響を受けません。

class House:

    def cleanup(self, arg1, arg2, kwarg1=False):
        """clean house is nice to live in!"""
        pass


class Person:
    def __init__(self, house: House):
        self._house = house
        # aliases
        self.cleanup_house = self.house.cleanup

    @property
    def house(self):
        return self._house

5
人が新しい家を手に入れたらどうなりますか?
user2357112は

この経済ではありません!冗談はさておき、家の属性を設定不可にして、セッターを追加するとうまくいくと思います。ここでの問題は、どちらが保守が容易かということです。これまたは多くのメソッドのシグネチャの書き換えですか。
グラニトサウルス

2
#2は、直感的に期待Person.cleanup_houseすることと一致しています。その人に関連付けられている家を取得し、それを片付けます。#1はいくつかの家を掃除し、ある時点(初期化)で、その家が正しい家でした。それは人が家を変えるか、または人が家を持っていない場合にうまく対処しません
ディスコのための賛美歌

回答:


8

最初の方法にはいくつかの問題があります。

  1. 余分なフープを飛び越えない限り、エイリアスが参照する属性が変更されても、エイリアスは更新されません。たとえば、セッターを使用しhousepropertyを作成することもできますが、これは、それを必要としないものに対しては簡単な作業です。実装例については、この回答の最後をご覧ください。
  2. cleanup_house継承できません。クラスで定義された関数オブジェクトは、継承およびオーバーライドが可能で、インスタンスにバインドできる非データ記述子です。最初のアプローチのようなインスタンス属性は、クラスにはまったく存在しません。バインドされたメソッドであるという事実は付随的です。super().cleanup_house具体的な例として、子クラスはにアクセスできません。
  3. person.cleanup_house.__name__ != 'cleanup_house'。これは頻繁にチェックするものではありませんが、チェックすると、関数名はになるはずcleanupです。

良い知らせは、アプローチ#2を使用するために署名を複数回繰り返す必要がないことです。Pythonは、すべての引数チェックをラップされるメソッドに委任するための非常に便利な*splat ()/ splatty-splat(**)表記を提供します。

def cleanup_house(self, *args, **kwargs):
    return self.house.cleanup(*args, **kwargs)

以上です。通常の引数とデフォルトの引数はすべてそのまま渡されます。

これが、#2がはるかにPython的なアプローチである理由です。メソッドシグネチャをコピーしない限り、タイプヒントをサポートするエディタとどのように相互作用するのか私にはわかりません。

問題となる可能性があることの1つcleanup_house.__doc__は、と同じではないことですhouse.cleanup.__doc__。これは、潜在的に変換値する可能性housepropertyそのセッター割り当てを、cleanup_house.__doc__


問題1.(ただし2.または3.ではない)に対処houseするには、セッターを使用してプロパティとして実装します。アイデアは、house属性が変更されるたびにエイリアスを更新することです。これは一般的には良い考えではありませんが、質問であなたが持っているものに代わる実装があり、少しはうまくいくでしょう:

class House:
    def cleanup(self, arg1, arg2, kwarg1=False):
        """clean house is nice to live in!"""
        pass


class Person:
    def __init__(self, house: House):
        self.house = house  # use the property here

    @property
    def house(self):
        return self._house

    @house.setter
    def house(self, value):
        self._house = house
        self.cleanup_house = self._house.cleanup

ただし、*args, **kwargs自動補完機能を解除すると、一般的に扱いにくくなります。また、二回ドキュメンテーション文字列を定義する必要がありますhouse.cleanupし、person.cleanup_houseあるいは何らかの形でそれらをハック。したがって、このアプローチでは、シグネチャとドキュメント文字列を常に手動で維持する必要があります。これは、大きなプログラムでは多くの作業になる可能性があります。
グラニトサウルス

2
@グラニトサウルス。私はどのようにわからない*args**kwargs厄介です。それらは文字通りそこにあるので、署名を維持する必要はありません。__doc__直接割当により更新することができます。
マッド物理学者

argsとkwargsは署名を難読化し、編集者は期待される値が何であるかわかりません。この問題はデコレータを追加することである程度解決できるようですが、それでもfunctool.wraps(<parent_func>)多くの矛盾が生じます。
グラニトサウルス

私はあなたの提案するセッターのアプローチを本当に好みます!私はそれをテストしたところ、期待されるエディター機能(オートコンプリート、署名、ドキュメント、少なくともPyCharmでは)で完全に機能します!
Granitosaurus

@グラニトサウルス。機能的な問題を認識している限り、選択するアプローチは完全に決定するものです。
マッド物理学者

4

ここにもう1つアプローチを追加したいhouseと思います。パブリックで設定可能にしたい場合(通常、このようなものを不変として扱います)、次のようcleanup_houseにプロパティを作成できます。

class House:
    def cleanup(self, arg1, arg2, kwarg1=False):
        """clean house is nice to live in!"""
        print('cleaning house')


class Person:
    def __init__(self, house: House):
        self.house = house

    @property
    def cleanup_house(self):
        return self.house.cleanup

少なくともIpython REPLでは、コード補完とdocstringは期待どおりに機能しているようです。型注釈とどのように相互作用するかを確認してください...

編集:それで、mypy 0.740は少なくともの型シグネチャを推測できませんperson.cleanup_house、それは素晴らしいことではありませんが、驚くことではありません:

(py38) Juans-MBP:workspace juan$ cat test_mypy.py
class House:
    def cleanup(self, arg1:int, arg2:bool):
        """clean house is nice to live in!"""
        print('cleaning house')


class Person:
    house: House
    def __init__(self, house: House):
        self.house = house  # use the property here

    @property
    def cleanup_house(self):
        return self.house.cleanup

person = Person(House())
person.cleanup_house(1, True)
person.cleanup_house('Foo', 'Bar')
reveal_type(person.cleanup_house)
reveal_type(person.house.cleanup)
(py38) Juans-MBP:workspace juan$ mypy test_mypy.py
test_mypy.py:19: note: Revealed type is 'Any'
test_mypy.py:20: note: Revealed type is 'def (arg1: builtins.int, arg2: builtins.bool) -> Any'

私はまだ#2で行きます。


1
これは、ラッパー関数のように機能するだけでなく、ターゲットの注釈とシグネチャも転送する非データ記述子を書くという私の考えに勝るものです。
マッド物理学者
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.