https://docs.python.org/ja/3/howto/descriptor.html
こちらを読んだ際のメモです。
読んでいるときの脳内をダンプしたものなので、単純に内容を知りたい方は直接ドキュメントを読むことをおすすめします。
python3.8.2
なぜドキュメントを読んでいるか
- ここらでドキュメントを読んで、知らないだけで実は使うと便利な機能を知っておきたい
- pythonを使い始めてかれこれ8年ほどになるが、雰囲気でやっているので未だに知らない機能がある
- pythonではinner classからouter classが見えない
- https://blog.hirokiky.org/entry/2013/12/25/000000
- しかし、ディスクリプタを変えると見えるように出来る
- 上記のブログで言及されていたstackoverflow: https://stackoverflow.com/questions/2278426/inner-classes-how-can-i-get-the-outer-class-object-at-construction-time
- この当たりがなぜなのかわからなかったのでディスクリプタとは何だろうと読んでみた
概要
- pythonのデスクリプタについて
定義と導入
一般に、デスクリプタは "束縛動作 (binding behavior)" をもつオブジェクト属性で、その属性アクセスが、デスクリプタプロトコルのメソッドによってオーバーライドされたものです
- 何が何やら。おそらく理解したあとに見ると簡潔にまとまった良い表現だが、理解してない時に見ても呪文にしか見えないやつ
属性アクセスのデフォルトの振る舞いはa.x
はa.__dict__['x']
のようになる。
探す範囲はメタクラスを除く基底クラス。
見つかったオブジェクトがデスクリプタメソッドを持っていれば、デフォルトの振る舞いをオーバーライドして代わりにデスクリプタメソッドを呼ぶ。
イメージ的にはこんな感じ?
イメージのgetattr.py
def getattr(a, name):
_x = a.__dict__[name]
if hasattr(_x, '__get__'):
return _x.__get__()
return _x
デスクリプタプロトコル
- 以下の3つで全て
descr.__get__(self, obj, type=None) -> value
descr.__set__(self, obj, value) -> None
descr.__delete__(self, obj) -> None
- データデスクリプタと非データデスクリプタが存在していて、オーバーライド時にデスクリプタと同名の項目が辞書にあった場合にそれぞれ、デスクリプタを優先する/辞書を優先する
- データデスクリプタってなんだろう
- 探しているオブジェクトそのものであるようにも、__get__などのメソッド自体にも読めるような気がする
デスクリプタの呼び出し
- 呼び出される時
- 直接呼ばれた時:
d.__get__(obj)
- 属性アクセス時:
obj.d
- 直接呼ばれた時:
- 上と似たような例が出てきた
- ちょっと違うけど大筋理解は間違って無さそう
上と似たような例.py
def __getattribute__(self, key):
"Emulate type_getattro() in Objects/typeobject.c"
v = object.__getattribute__(self, key)
if hasattr(v, '__get__'):
return v.__get__(None, self)
return v
- 実際の実装はこちら
- 憶えておくべき重要な点は:
- デスクリプタは __getattribute__() メソッドによって呼び出される
- __getattribute__() をオーバーライドすると、自動的なデスクリプタの呼び出しが行われなくなる
- object.__getattribute__() と type.__getattribute__() では、 __get__() の呼び出しが異なる。
- データデスクリプタは、必ずインスタンス辞書をオーバーライドする。
- 非データデスクリプタは、インスタンス辞書にオーバーライドされることがある。
- 要は普通は所属するオブジェクトの__dict__に自身をキーとして値を持っているが、やりたければ別の方法で呼び出すように__get__を定義することで挙動をオーバーライドすることもできるということか
例
流し見
プロパティ
- オブジェクトからのデータディスクリプタ呼び出しの挙動がわかる
- ちょっとした利用例もある
関数とメソッド
- こちらは非データディスクリプタの挙動
- 関数・メソッドが非データディスクリプタ?
- クラス内で定義したメソッドは、1つ目の引数がオブジェクトに限定された特殊な関数である、非データディスクリプタ
-
self
とかく慣習になっているが、ただの一個目の引数 - オブジェクトから呼び出した際はselfにオブジェクト自身を入れた、部分適用した関数を呼び出す
- クラスから呼び出した際は普通に関数を返すため一個目のいち引数が必要
-
- 例でいくつか関数オブジェクトがデフォルトで持つ変数が紹介されていた
- __dict__: 何度も出ている、そのオブジェクトが持っているアトリビュートの名前と値の辞書
- __qualname__: 呼び出した際のフルネーム(e.g.
D.f
で呼び出した際は、"D.f"
) - __func__: もとの素の関数.オブジェクトから呼び出しても第一引数selfが必要になる
- __self__: selfに代入されるオブジェクト
- __class__: この変数を持っているオブジェクトのクラス
静的メソッドとクラスメソッド
- だいぶ理解が進んできたのでなんとなくわかるようになってきた
obj.f(*args) の呼び出しを f(obj, *args) に変換します。 klass.f(*args) を呼び出すと f(*args) になります
- アノテーションを何も付けずにクラス内で普通に定義したメソッド
f(self, *args)
-
obj.f(*args)
→f(obj, *args)
-
klass.f(*args)
→f(*args)
-
- 静的メソッド(@staticmethod): そのまま返す
- どちらも
f(*args)
- どちらも
- クラスメソッド(@classmethod)
-
obj.f(*args)
→f(type(obj), *args)
-
klass.f(*args)
→f(klass, *args)
-
- つまり、普通のメソッド・静的メソッド・クラスメソッドの違いはバウンドするしないとその対象
- やろうと思えば、objから参照したときは素の関数を返して、klassからのときはklassにバウンドした関数を返すということも出来そう
- メリットが思いつかないけども
- 静的メソッドは、何もバウンドせずに使いたい関数を定義する
- クラスメソッドは常にクラスの方をバウンドしたい時に使う
- 例としてカスタムコンストラクタが出ていた(dict.fromkeys)
感想
- pythonを触り始めて間もない頃にクラス内のメソッドの1つ目の引数を
self
と書くと普通のメソッドでcls
と書くとクラスメソッドになると勘違いしていたのを思い出した- 今は流石にそんなことないと知ってはいるが、具体的な挙動を初めて理解した
- オブジェクトというのはいろんな辞書をいい感じで管理すること何だなと思いました
- 当初の疑問の、inner classからouter classが見えないのは、inner classの__dict__にouter classが入っていないから
- クラスデコレータでディスクリプタをオーバーライドして入れてあげればOKということだったと理解しました
- また冒頭で引用したブログで言及されていたしっかりしたbounded inner classレシピはこちら