クラスに記述子を設定すると記述子が上書きされるのはなぜですか?


10

簡単な再現:

class VocalDescriptor(object):
    def __get__(self, obj, objtype):
        print('__get__, obj={}, objtype={}'.format(obj, objtype))
    def __set__(self, obj, val):
        print('__set__')

class B(object):
    v = VocalDescriptor()

B.v # prints "__get__, obj=None, objtype=<class '__main__.B'>"
B.v = 3 # does not print "__set__", evidently does not trigger descriptor
B.v # does not print anything, we overwrote the descriptor

この質問には効果的な重複がありますが、重複は回答されなかったため、学習課題としてCPythonソースをもう少し掘り下げました。警告:雑草に入りました。私はそれらの水を知っている船長から助けが得られることを本当に望んでいます。私自身の将来の利益と将来の読者の利益のために、私が見ている電話を追跡する際にできるだけ明確にしようとしました。

__getattribute__ルックアップの優先順位など、記述子に適用された動作に大量のインクがこぼれたことを確認しました。でPythonはスニペット「の起動記述子」は、単に以下のFor classes, the machinery is in type.__getattribute__()...大体私は相当であると考えているものと私の心に同意CPythonのソースtype_getattro、私が見てによって見つけ出し、「tp_slots」そしてtp_getattroが移入されます。そして、B.v最初に印刷__get__, obj=None, objtype=<class '__main__.B'>するという事実は私には理にかなっています。

私が理解していないのは、割り当てB.v = 3がトリガーするのではなく、なぜ記述子を盲目的に上書きするのv.__set__ですか?「tp_slots」からもう一度CPython呼び出しをトレースし、次にtp_setattroが入力されている場所を調べ、次にtype_setattroを調べましたtype_setattro 以下のように見えるの周りの薄いラッパー_PyObject_GenericSetAttrWithDict。そして、私の混乱の核心があります: _PyObject_GenericSetAttrWithDict持っているように見えます記述子のに優先権を与えるロジック__set__方法を!これを念頭に置いて、トリガーB.v = 3するのvではなく、なぜ盲目的に上書きするのか理解できませんv.__set__

免責事項1:私はソースからprintfsを使用してPythonを再構築しなかったため、のtype_setattro間に何が呼び出されているのか完全にはわかりませんB.v = 3

免責事項2: VocalDescriptor「典型的」または「推奨」記述子の定義を例示することを意図したものではありません。メソッドがいつ呼び出されているかを通知するのは、冗長な何もしないことです。


1
私にとって、これは最後の行に3を出力します...コードは正常に機能します
Jab

3
記述子は、クラス自体ではなく、インスタンスから属性にアクセスするときに適用されます。私にとって、謎はなぜ__get__機能し__set__なかったのではなく、なぜ機能したのかです。
jasonharper

1
@Jab OPは引き続き__get__メソッドを呼び出すことを期待しています。 B.v = 3効果的に属性をint
r.ook

2
属性アクセス@jasonharperか否かを判断する__get__と呼ばれ、デフォルトの実装object.__getattribute__type.__getattribute__呼び出し__get__インスタンスまたはクラスを使用する場合。viaの割り当て__set__はインスタンスのみです。
chepner '16年

@jasonharper __get__クラス自体から呼び出されたときに記述子のメソッドがトリガーされるはずだと思います。使い方ガイドによると、これが@classmethodsと@staticmethodsの実装方法です。@JabなぜB.v = 3クラス記述子を上書きできるのか疑問に思っています。CPythonの実装に基づいて、私B.v = 3はもトリガーすることを期待していました__set__
マイケルカリリ

回答:


6

あなたは正しいB.v = 3単に整数(それが必要のような)との記述が上書きされます。

B.v = 3記述子を呼び出すために、記述子はすなわち上、メタクラスに定義されている必要がありますtype(B)

>>> class BMeta(type): 
...     v = VocalDescriptor() 
... 
>>> class B(metaclass=BMeta): 
...     pass 
... 
>>> B.v = 3 
__set__

で記述子を呼び出すにはB、インスタンスを使用しますB().v = 3

B.vゲッターを呼び出す理由は、記述子インスタンス自体を返すことができるようにするためです。通常は、クラスオブジェクトを介して記述子へのアクセスを許可するために、次のようにします。

class VocalDescriptor(object):
    def __get__(self, obj, objtype):
        if obj is None:
            return self
        print('__get__, obj={}, objtype={}'.format(obj, objtype))
    def __set__(self, obj, val):
        print('__set__')

これで、対話できるB.vようなインスタンスが返さ<mymodule.VocalDescriptor object at 0xdeadbeef>れます。これは文字どおり記述子オブジェクトであり、クラス属性として定義され、その状態B.v.__dict__はのすべてのインスタンス間で共有されますB

もちろん、ユーザーが何をしたいかを正確に定義するのはユーザーのコード次第でB.vあり、リターンselfは単なる一般的なパターンです。


1
この答えを完全にする__get__ために、インスタンス属性またはクラス属性__set__として呼び出されるように設計されていますが、インスタンス属性としてのみ呼び出されるように設計されていることを追加します。関連ドキュメント:docs.python.org/3/reference/datamodel.html#object.__get__
sanyash

@wim Magnificent !! 並行して、type_setattro呼び出しチェーンをもう一度見ていました。_PyObject_GenericSetAttrWithDictへの呼び出しがタイプを提供していることがわかります(この場合、Bの場合)。
Michael Carilli

_PyObject_GenericSetAttrWithDictは、BのPy_TYPEをとしてプルしますtp。これは、Bのメタクラス(type私の場合)であり、記述子短絡論理によって処理されるのはメタクラス tpです。記述子は直接に定義されていません(したがって、私の元のコードにその短絡ロジックによって見呼び出されていない)が、メタクラスに定義されたディスクリプタがされ短絡ロジックによって見。B __set__
Michael Carilli

したがって、メタクラスに記述子がある__set__場合、その記述子のメソッド呼び出されます。
マイケルカリリ

@sanyashは直接自由に編集してください。
WIM

3

オーバーライドB.vがなければ、と同等ですがtype.__getattribute__(B, "v")b = B(); b.vと同等object.__getattribute__(b, "v")です。どちらの定義も、定義__get__されている場合、結果のメソッドを呼び出します。

への呼び出し__get__はそれぞれの場合で異なることに注意してください。B.v通過Noneしながら、最初の引数としてB().vインスタンス自体を通過します。どちらの場合もB、2番目の引数として渡されます。

B.v = 3一方、はtype.__setattr__(B, "v", 3)、を呼び出さないと同等__set__です。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.