ディスクリプタについて勉強してから10か月くらい前か~
このQiita記事が公式のHowtoディスクリプタよりも詳しい&分かりやすくてよきよき。特に__getattribute__
の中身の擬似コードあるのがいいよね。
Effective Python にはディスクリプタ使おうとして __getattribute__
のぬかるみにハマるなよ!って書いてあるけど、知らないに越したことないでしょ。良質なドキュメントさえあれば理解に難くもないし。
とりあえず、インスタンスの属性アクセスのときに使われる__getattribute__
(objectクラスのやつ)
- 当該属性名をクラス属性(スーパークラス含め)から探し、それがデータディスクリプタなら
__get__(obj, type(obj))
呼び出し - インスタンスの属性辞書(
__dict__
)から当該属性名を探し、あればそれをそのまま返す - 1.の非データディスクリプタ版
- エラー
次に、クラスの属性アクセスのときに使われる__getattribute__
(typeクラスのやつ)*1
- 当該属性名をメタクラス属性から探して、それがデータディスクリプタなら
__get__(cls, type(cls))
呼び出し - クラス(スーパークラス含め)の属性辞書から当該属性名を探し、それがディスクリプタなら
__get__(None, cls)
呼び出し、違うならそのまま返す。 - 1.の非データディスクリプタ版
- エラー
多分、setの方もこれと同じだと思う。
で、あとは各々のディスクリプタの__get__
と__set__
が色々してくれる。おわり!
すーぱー
super
クラスはまた別の__getattribute__
を持っているらしい。
2. 組み込み関数 — Python 3.5.2 ドキュメント
これも上のリンクから擬似コードをもってきますと、
def super_getattribute(su, key): "Emulate super_getattro() in Objects/typeobject.c" starttype = su.__self_class__ mro = iter(starttype.__mro__) for cls in mro: if cls is su.__self_class__: break # Note: mro is an iterator, so the second loop # picks up where the first one left off! for cls in mro: if key in cls.__dict__: attr = cls.__dict__[key] if hasattr(attr, '__get__'): return attr.__get__(su.__self__, starttype) return attr raise AttributeError
メソッド内で呼び出した場合は、super
を呼び出したクラスの__mro__
の自分自身以降の属性辞書を探索して、それがディスクリプタ(大抵は__init__
とか上位クラスのメソッドを呼ぶわけで、つまり大体の場合でディスクリプタ)なら、呼び出したところのインスタンスとクラスで__get__
を呼び出す(__init__
のself
に呼び出した時点のインスタンスを束縛したバウンドメソッドを返すわけですな)。
もちろん、親メソッドを連鎖させるためには、それぞれのクラスがsuper
で親クラスを呼び出していくことで実現する。
なんか Python2 だとあんまいい感じに動かない仕様だったらしく、Python3 使っててよかった~って感じだ。
ところで、さっきの擬似コードと少し挙動の異なるところを見つけたのでそれを調べる。確かめたいのは次の2つ:
__get__
の引数はこれで本当にあってるのかsuper
が第1引数に取ったクラスはどこで使われてるのか(上の擬似コードだとこれは不使用)
class Descriptor(): def __init__(self, value): self.value = value def __get__(self, obj, obj_cls=None): return (obj, obj_cls, self.value) class C(): def __init__(self): super().__init__() x = Descriptor("C") class D(C): def __init__(self): super().__init__() x = Descriptor("D") class D2(C): def __init__(self): super().__init__() x = Descriptor("D2") class E(D, D2): def __init__(self): su = super() su.__init__() x = Descriptor("E") class F(E): def __init__(self): super().__init__() x = Descriptor("F") if __name__ == '__main__': print("F mro :", F.__mro__) print("E mro :", E.__mro__) print("D mro :", D.__mro__) print("D2 mro :", D2.__mro__) print("C mro :", C.__mro__) print() print("F() :", F().x) print("super(F, F()) :", super(F, F()).x) print("super(E, F()) :", super(E, F()).x) print("super(D, F()) :", super(D, F()).x) print("super(D2, F()) :", super(D2, F()).x)
> python func_annotations.py F mro : (<class '__main__.F'>, <class '__main__.E'>, <class '__main__.D'>, <class '__main__.D2'>, <class '__main__.C'>, <class 'object'>) E mro : (<class '__main__.E'>, <class '__main__.D'>, <class '__main__.D2'>, <class '__main__.C'>, <class 'object'>) D mro : (<class '__main__.D'>, <class '__main__.C'>, <class 'object'>) D2 mro : (<class '__main__.D2'>, <class '__main__.C'>, <class 'object'>) C mro : (<class '__main__.C'>, <class 'object'>) F() : (<__main__.F object at 0x000001C4756C4470>, <class '__main__.F'>, 'F') super(F, F()) : (<__main__.F object at 0x000001C4756C4470>, <class '__main__.F'>, 'E') super(E, F()) : (<__main__.F object at 0x000001C4756C4470>, <class '__main__.F'>, 'D') super(D, F()) : (<__main__.F object at 0x000001C4756C4470>, <class '__main__.F'>, 'D2') super(D2, F()) : (<__main__.F object at 0x000001C4756C4470>, <class '__main__.F'>, 'C')
すべてのクラスが非データディスクリプタであるx
をもっています。super
第二引数は最下層のF()
に固定して、第一引数を色々動かして、x
にアクセスしてみました。もちろん、isinstance(F(), cls)
の範囲内で。
Descriptor
は見てもらえばその通りですが、__get__
の引数と、ディスクリプタ初期化の際に入れた文字列(それぞれのクラス名にしてある)のタプルを返してくれます。
まず、擬似コードにおける__get__
の引数は間違ってないようです。それぞれ obj
, obj_cls
に渡されるのは、super
第二引数のオブジェクトsu.__self__
と、そのクラスsu.__self_class__
です。
一方で、呼び出されているディスクリプタはsuper
第一引数によって変化しています!第一引数とディスクリプタの所属するクラスを比べてみると、どうやらsuper
第一引数は擬似コードにおけるstarttype
に相当しそうです(もちろん、__get__
の第二引数にはもはやstarttype
は使えず、su.__self_class__
を代入するように書き換える必要がありますが)。つまり、属性辞書の探索は、第二引数オブジェクトのクラス以降ではなく、super
第一引数以降ということになりそうです。
というわけで、変更すべき箇所はこのあたりです:
def super_getattribute(su, key): "Emulate super_getattro() in Objects/typeobject.c" starttype = su.__self_class__ mro = iter(starttype.__mro__) for cls in mro: if cls is su.__self_class__: break
superオブジェクトがどんな属性を持っているのか、日本語のドキュメントが見つからなくてムムムしていましたが、どうやらsuper
第一引数は__thisclass__
に収納されてるようです。よって、まず、starttype = su.__thisclass__
とすべきです。
さらに、__thisclass__
を starttype
とした場合、mro
についても変更すべきです。属性辞書の探索はmro
に従って行われるわけですが、もしmro
がstarttype.__mro__
を元につくられるとしたら、super(D, F()).x
(下から2行目)がD2のディスクリプタに辿り着くことは不可能だからです(D.__mro__
参考)。というわけで、mro = iter(su.__self_class__.__mro__)
とすべきです。
さらに、イテレータの巻き上げは、su.__self_class__
ではなく、 starttype
で止めるべきですね。
というわけで、恐らくより正しい擬似コードは以下の通りです:
def super_getattribute(su, key): "Emulate super_getattro() in Objects/typeobject.c .. and revised by iuk." starttype = su.__thisclass__ mro = iter(su.__self_class__.__mro__) for cls in mro: if cls is starttype: break # Note: mro is an iterator, so the second loop # picks up where the first one left off! for cls in mro: if key in cls.__dict__: attr = cls.__dict__[key] if hasattr(attr, '__get__'): return attr.__get__(su.__self__, su.__self_class__) return attr raise AttributeError
super
第一引数は mro
の巻き上げに使われてたってことですね。
というわけで、super
のもう少し詳しい挙動が分かりました。
super(type, obj)
の属性アクセスは、obj
のクラスの__mro__
に従って、type
以降(type
は含まない)のクラスの属性辞書を順に探索し、それがディスクリプタなら__get__(obj, obj.__class__)
を呼び出し、ディスクリプタでないならそのまま返す。
まあディスクリプタのあたりの挙動は他と特に変わらないですね。ちなみにsuper(fromtype, type)
の属性アクセスの場合は __get__(None, type)
になります。