Python のディスクリプタプロトコルと関数の関係
ディスクリプタは __get__, __set__, __delete__ を定義したオブジェクトで、属性アクセスをカスタマイズできます。実は Python の関数もディスクリプタであり、これがメソッドの仕組みの根幹です。
ディスクリプタプロトコル
class Descriptor:
def __get__(self, obj, objtype=None):
print(f"__get__ called: obj={obj}, objtype={objtype}")
return "value"
def __set__(self, obj, value):
print(f"__set__ called: obj={obj}, value={value}")
class MyClass:
attr = Descriptor()
obj = MyClass()
print(obj.attr) # __get__ が呼ばれる
obj.attr = 10 # __set__ が呼ばれる
関数はディスクリプタ
関数オブジェクトは __get__ を持っています。これがメソッドバインディングの仕組みです。
def greet(self):
return f"Hello, {self.name}!"
print(hasattr(greet, "__get__")) # True
クラス内で関数を定義すると、インスタンスからアクセスしたときに __get__ が呼ばれ、バウンドメソッドが返されます。
class Person:
def __init__(self, name):
self.name = name
def greet(self):
return f"Hello, {self.name}!"
p = Person("Alice")
# クラス経由:関数そのもの
print(Person.greet) # <function Person.greet>
# インスタンス経由:バウンドメソッド
print(p.greet) # <bound method Person.greet of <Person>>
get の動作を確認
func = Person.__dict__["greet"]
print(func) # <function Person.greet>
print(func.__get__(p, Person)) # <bound method Person.greet of <Person>>
インスタンス経由でアクセスすると、func.__get__(instance, type) が呼ばれ、第一引数が束縛されたメソッドオブジェクトが返されます。
自作ディスクリプタで関数をラップ
class LoggedMethod:
def __init__(self, func):
self.func = func
def __get__(self, obj, objtype=None):
if obj is None:
return self.func
def wrapper(*args, **kwargs):
print(f"Calling {self.func.__name__}")
return self.func(obj, *args, **kwargs)
return wrapper
ディスクリプタを理解すると、プロパティ、クラスメソッド、スタティックメソッドの仕組みも明確になります。