ディスクリプタの使い方を色々考えてて、挙動が面白かったので書いておこう。

てか、ディスクリプタって何で使えばいいんだろうか。

何かに使えそうな感じはするんだけど、具体的に何使うかは何とも・・・。

オブジェクトに値入れる時のバリデーションを共通化とかかな?

とりあえず、公式はこの辺

で、公式に

クラスへ束縛すると、A.x は呼び出し A.__dict__['x'].__get__(None, A) に変換されます。

って書いてあったので、試しに、、、

class DescriptorA:
    def __set_name__(self,owner,name):
        self._name = name

    def __get__(self, instance, owner):
        return owner.a

class MyClassA:
    a = DescriptorA()

ってやってみたら、案の定、無限ループして落ちた。つか、Pythonのエンジン側で再帰の制限かかってたのね。初めて知った。

  [Previous line repeated 496 more times]
RecursionError: maximum recursion depth exceeded

ふむふむ。アクセスメソッド的に呼ばれるインターフェースが決まってて、それを定義したクラスがディスクリプタって感じでしょか。

で、公式に乗ってるサンプルは下記なんだけど、

class RevealAccess(object):
    """A data descriptor that sets and returns values
       normally and prints a message logging their access.
    """

    def __init__(self, initval=None, name='var'):
        self.val = initval
        self.name = name

    def __get__(self, obj, objtype):
        print('Retrieving', self.name)
        return self.val

    def __set__(self, obj, val):
        print('Updating', self.name)
        self.val = val

class MyClass(object):
...     x = RevealAccess(10, 'var "x"')
...     y = 5

これって、値の本体はMyClassじゃなくて、RevealAccess側のインスタンスだよね?

なんかMyClassのクラス変数でもインスタンス変数でも無いように思うのですが、こういう使い方が良いの?

てか、むしろこれやると、


class DescriptorB:
    def __init__(self,value):
        self.value = value

    def __set_name__(self,owner,name):
        self._name = name

    def __get__(self, instance, owner):
        return self.value

    def __set__(self, instance, value):
        self.value = value

class MyClassA:
    a = DescriptorA(10)
    b = 100

if __name__ == "__main__":
    x = MyClassB()
    y = MyClassB()

    print(f"before x.a : {x.a}")
    print(f"before y.a : {y.a}")
    y.a = 20
    print("set : y.a = 20")
    print(f"after x.a : {x.a}!!!!")
    print(f"after y.a : {y.a}")

    print(f"before x.b : {x.b}")
    print(f"before y.b : {y.b}")
    y.b = 200
    print("set : y.b = 200")
    print(f"after x.b : {x.b}")
    print(f"after y.b : {y.b}")

↓↓↓↓↓↓↓↓

before x.a : 10
before y.a : 10
set : y.a = 20
after x.a : 20!!!!
after y.a : 20

before x.b : 100
before y.b : 100
set : y.b = 200
after x.b : 100
after y.b : 200

って感じで、なんちゃってシングルトンのような超グローバル変数的な何かになる。

意図せずやっちゃったら相当ヤバイけど、意図的に何かに使えそうな気もする。

で、あと、色々見てると↓の感じでインスタンス変数扱いにしてるのを見る。

class DescriptorC:

    def __set_name__(self,owner,name):
        self._name = name

    def __get__(self, instance, owner):
        return instance.__dict__[self._name]

    def __set__(self, instance, value):
        instance.__dict__[self._name] = value

class MyClassC:
    a = DescriptorC()

if __name__ == "__main__":
    x = MyClassC()
    x.a = 10
    print(x.a)

↓↓↓↓↓

10

で、インスタンス変数へのアクセスっぽいんだけど、これって、値の代入するまでインスタンスの__dict__に対象の名前いないから、代入する前にアクセスすると落ちるよね?

if __name__ == "__main__":
    x = MyClassC()
    print(x.a)#これは落る
    x.a = 10
    print(x.a)#こっちは落ちない
↓↓↓↓↓
return instance.__dict__[self._name]でkeyerror

あと、公式だと↓で書いてあるので、

オブジェクトインスタンスへ束縛すると、a.x は呼び出し type(a).__dict__['x'].__get__(a, type(a))

クラスへ束縛すると、A.x は呼び出しA.__dict__['x'].__get__(None, A)

クラス変数として呼ぶと

if __name__ == "__main__":
    x = MyClassC()
    x.a = 10
    print(x.a)
    print(MyClassC.a)#これでも__get__呼び出されてキーエラー

んー、扱いがムズイ。

基本的にはインスタンス変数として扱いたいんだと思うので、↓の感じがいいのかしら。

class DescriptorD:

    def __set_name__(self,owner,name):
        self._name = name

    def __get__(self, instance, owner):
        if instance is None:
            return "申し訳ないがClassはNG"

        return instance.__dict__[self._name]

    def __set__(self, instance, value):
        instance.__dict__[self._name] = value

class MyClassD:
    a = DescriptorD()
    def __init__(self):
        self.a = ""

if __name__ == "__main__":
    x = MyClassD()
    print(x.a)
    x.a = "init"
    print(x.a)
    print(MyClassD.a)

どうなんでしょ。個人的にはこれがいいと思うんだけど。それか、__get__の中でkeyが無かったら何かしらの初期値入れて、初期化するように組むか。

あと、公式の引数がそうなってるからだと思うのですが、色々見るサンプルって、下の感じ。

object.__get__(self, instance, owner)

このownerってクラスなんだけど、分りにくくない?

実際、Djangoのmodelsのソースでもディスクリプタ使われてるんですけど、

下の名前で使ってる。

def __get__(self, instance, cls=None):

clsの方が分かりやすいと思うんだけどなー。