ゆくゆくは有へと

おかゆ/彼ノ∅有生 の雑記

__getattribute__とsuperのメモ

ディスクリプタについて勉強してから10か月くらい前か~

iuk.hateblo.jp

qiita.com

このQiita記事が公式のHowtoディスクリプタよりも詳しい&分かりやすくてよきよき。特に__getattribute__の中身の擬似コードあるのがいいよね。

Effective Python にはディスクリプタ使おうとして __getattribute__ のぬかるみにハマるなよ!って書いてあるけど、知らないに越したことないでしょ。良質なドキュメントさえあれば理解に難くもないし。

とりあえず、インスタンスの属性アクセスのときに使われる__getattribute__(objectクラスのやつ)

  1. 当該属性名をクラス属性(スーパークラス含め)から探し、それがデータディスクリプタなら__get__(obj, type(obj))呼び出し
  2. インスタンスの属性辞書(__dict__)から当該属性名を探し、あればそれをそのまま返す
  3. 1.の非データディスクリプタ
  4. エラー

次に、クラスの属性アクセスのときに使われる__getattribute__(typeクラスのやつ)*1

  1. 当該属性名をメタクラス属性から探して、それがデータディスクリプタなら__get__(cls, type(cls))呼び出し
  2. クラス(スーパークラス含め)の属性辞書から当該属性名を探し、それがディスクリプタなら__get__(None, cls)呼び出し、違うならそのまま返す。
  3. 1.の非データディスクリプタ
  4. エラー

多分、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に従って行われるわけですが、もしmrostarttype.__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) になります。

*1:オブジェクトの属性アクセスはそのオブジェクトのクラスの__getattribute__に支配されるわけで、つまりオブジェクトがクラスの場合はメタクラスの__getattribute__。