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

ディスクリプタを理解すると、プロパティ、クラスメソッド、スタティックメソッドの仕組みも明確になります。