ディスクリプタは __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
ディスクリプタを理解すると、プロパティ、クラスメソッド、スタティックメソッドの仕組みも明確になります。