概要
Python の descriptor は,属性 (attribute) へのアクセスをフックに関数呼び出しを行う機構である. descriptor の機構は,メンバ変数の getter や setter を簡潔に定義できるようにした property などに使われている.
ここでは,
について説明を行う.
descriptor の基本
あるクラスに対して,特別なメソッド
__get__()
__set__()
__delete__()
のいずれかが定義されているとき, そのクラスのオブジェクトを descriptor という.
descriptor が特別な振る舞いをするのは,descriptor のインスタンスがクラス変数として使われたときである. このとき,descriptor へのアクセスが次のように変換される.
- 値の読み出し ->
__get__()
- 値の書き込み ->
__set__()
- 値の削除 ->
__delete__()
ここでは,__get__()
と__set__()
を中心に説明する.
基本的な例
class D: def __get__(self,obj,objtype=None): print("Called D.__get__()") class A: x = D() a = A() a.x
出力
Called D.__get__()
クラスD
は__get__()
メソッドを実装しているので,値の読み出しに関して descriptor として働く.
クラスA
はクラス変数としてクラスD
のオブジェクトx
を持っている.
したがって,クラスA
のオブジェクトa
を通して descriptor x
にアクセスしようとすると,D.__get__()
が呼ばれる.
__get__()
に自動で渡される引数
__get__()
が呼ばれる際,引数obj
には呼び出し元のオブジェクト(上の例ではa
),objtype
にはa
の型(上の例ではclass A
)がそれぞれ渡される.
A.dsc
のようにクラスから直接 descriptor へアクセスした場合は,引数obj
は None
が渡される.
__set__()
メソッド
x
が descriptor であり,__set__()
メソッドを実装しているとき,a.x = ...
のような x
への書き込みは,__set__()
メソッドの呼び出しに置き換えられる.
def __set__(self, obj, value): ...
のように__set__()
メソッドが定義されているとき,呼び出し時に引数obj
には呼び出し元のオブジェクト,引数value
には書き込もうとした値が自動的に渡される.
descriptor でインスタンス変数を操作する
class D: def __get__(self,obj,objtype=None): print(f"Accessing 'val' giving {obj._val}") return obj._val def __set__(self,obj,v): print(f"Update 'val' to {v}") obj._val = v class A: val = D() def __init__(self,v): self.val = v a = A(123) a.val b = A(456) b.val a.val
出力
>>> a = A(123) Update 'val' to 123 >>> a.val Accessing 'val' giving 123 123 >>> b = A(456) Update 'val' to 456 >>> b.val Accessing 'val' giving 456 456 >>> a.val Accessing 'val' giving 123 123
クラスA
はクラス変数として descriptor val
をもっている.
A
のオブジェクトの初期化時に descriptor val
へのアクセスが起こり,オブジェクトのインスタンス変数 _val
がセットされる.
val
自体はクラス変数であるが,__set__()
の呼び出し時に呼び出し元のオブジェクトがobj
として渡されているため,個別のオブジェクトに関するインスタンス変数を操作することができているという点に注意しておく.
複数のインスタンス変数を操作する
上の例でdescriptor を通してインスタンス変数を操作できることを確認したが,この例ではインスタンス変数が_val
という名前で決め打ちになっていた.
そのため,1つのクラスに複数の descriptor を持ちたい場合,上の方法では各 descriptor ごとに別々のクラスを用意しなければならないという問題が生ずる.
これを解決する方法として,__set_name__()
という特殊なメソッドを用いることができる.
__set_name__()
はクラスの構築時にクラス変数としてもっている descriptor ごとに呼ばれる.
def __set_name__(self, owner, name): ...
という定義があったとき,owner
にはその descriptor を保持するクラスが,name
にはその descriptor がクラス内でもつクラス変数の名前が文字列として渡される.
ソースコード中に記述したクラス変数名の文字列が直接文字列として渡されるという点が特徴的である.
例
class D: def __set_name__(self,owner,name): self.public_name = name self.private_name = "_" + name def __get__(self,obj,objtype=None): val = getattr(obj,self.private_name) print(f"Accesing {self.public_name!r} giving {val}") return val def __set__(self,obj,v): print(f"Update {self.public_name!r} to {v}") setattr(obj,self.private_name,v) class A: x = D() y = D() def __init__(self,v1,v2): self.x = v1 self.y = v2 a = A(123,456) a.x a.y
出力
>>> a = A(123,456) Update 'x' to 123 Update 'y' to 456 >>> a.x Accesing 'x' giving 123 123 >>> a.y Accesing 'y' giving 456 456
__set_name__()
メソッド内で public_name
と private_name
の2つの変数を定義し,public_name
をログ表示用に,private_name
を実際のインスタンス変数操作用に使っている.
x
とy
のそれぞれのクラス変数が__set_name__()
メソッドを呼び出すので,変数名が動的に解決され,衝突することなく使うことができている.
参考
Descriptor HowTo Guide — Python 3.10.8 documentation
Descriptor についてまとまった公式のドキュメント
3. Data model — Python 3.10.8 documentation
リファレンス内の Data model の章に,descriptor の呼び出しなどに関する詳細な記述がある.