19 September 2014

ディスクリプタとは?

Pythonでは、ある特定の性質を持つオブジェクトが実装すべき
一連のメソッドの事をプロトコルと呼ぶ

# ある特定の性質を持つオブジェクトとは、
# 例えばイテレータもプロトコルと呼ばれるものである

# イテレータの例
ham = "ham"
for c in ham:
    print c

spam = ["spam1", "spam2", "spam3"]
for c in spam:
    print c
# このようにリストやタプル、文字列オブジェクトは
# 反復処理によりアイテムを順次的に取得するイテレータ型がサポートされている

これと同じように、 以下のメソッドのいずれかを定義したオブジェクトをディスクリプタと呼ぶ

class descriptor():
    def __get__(self): pass
    def __set__(self, val): pass
    def __delete__(self): pass

このディスクリプタの仕組みは、
プロパティやメソッド、super()等の実装で取り入れられており、
ユーザの定義できるプロトコルとしても提供されている


ディスクリプタの分類

ディスクリプタは、定義されているディスクリプタメソッドにより、 データディスクリプタ、非データディスクリプタ、読み取り専用ディスクリプタの分類に分けられる

データディスクリプタ(data-descriptor)
__get__、__set__の両方が定義されている

# これはデータディスクリプタと呼ばれる
class descriptor():
    def __get__(self): pass
    def __set__(self, val): pass

非データディスクリプタ(non-data-descriptor)
__get__だけが定義されている

# これは非データディスクリプタと呼ばれる
class descriptor():
    def __get__(self): pass

読み取り専用ディスクリプタ(read-only-data-descriptor)
データディスクリプタの中で、__set__が読み出されたときにAttributeErrorが送出されるもの

# これは読み取り専用ディスクリプタと呼ばれる
class descriptor():
    def __get__(self): pass
    def __set__(self):
        raise AttributeError


ディスクリプタの定義と実行

以下のようなコードを定義する
これは、データディスクリプタである

class descriptor():
    def __init__(self, val):
        #クラス属性valを初期化する
        self.val = val
    def __get__(self):
        print "called method: __get__"
        return self.val #クラス属性valを返す
    def __set__(self, val):
        print "called method: __set__"
        self.val = val #クラス属性valを書き換える

class MyDescriptor():
    x = descriptor("spam")
    y = "none descriptor"

obj = MyDescriptor()
print(obj.x) #ディスクリプタが呼び出され__get__が実行される
print(obj.y) #ディスクリプタではないのでクラス属性がそのまま返ってくる

obj.x = "ham" #ディスクリプタが呼び出され__set__が実行される
obj.y = "ham" #ディスクリプタではないのでクラス属性に値が書き込まれる

呼び出し側であるコードでは、 単純にobj.xと属性へアクセスするコードになっている
しかし、xがディスクリプタであるかそうでないかによって 実際の挙動が変わるし、変える事が可能になる

また、obj.x.__get__() のように直接呼び出す事も可能である

プロパティとディスクリプタの違い

プロパティ(Property)は、 属性アクセスをカスタマイズしたい対象のクラス定義の中で、
そのクラスのインスタンスの属性アクセスをカスタマイズする為に使う
プロパティは以下のようなコードである

class Property():
    def __init__(self, val):
        self._val = val
    def get_val(self):
        return self._val
    def set_val(self, val):
        self._val = val
    def del_val(self):
        del self._val
    val = property(get_val, set_val, del_val)

obj = Property("ham")
print(obj.val) #プロパティとしてget_valが呼び出される
obj.val = "spam" #プロパティとしてset_valが呼び出される
del obj.val #プロパティとしてset_delが呼び出される

プロパティは対象クラスの属性に対して定義されるものである
プロパティと違ってディスクリプタは、 特定のクラスとは独立に定義されて、
他のクラスの属性アクセスをカスタマイズする為に使う

属性アクセスのカスタマイズされた処理だけを独立して定義できる為、応用範囲が広く汎用性も高い

ディスクリプタの意味

ディスクリプタはそれ自身が属性になった時、 参照(__get__)、代入(__set__)、削除(__delete__)の基本的なアクセスに対して、
どう動作するかをコントロールする為にディスクリプタが作られた


ディスクリプタ実行の挙動

属性アクセス(obj.val)した時の実際の挙動について
obj.valにより属性アクセスが発生すると、
Pythonはobj.__dict__["val"] -> type(obj).__dict__["val"] -> type(a)の基底クラスへと探索を進める

ディスクリプタの実行のされ方は、 ディスクリプタが実装されている対象(上記で言えばobj)が、
インスタンス属性であるかクラス属性であるかによって異なる


ディスクリプタの例

例えば、ある対象のクラス属性を呼び出す時、
呼び出される度にインクリメントされて返ってくる属性を定義したいとする

プロパティを使って定義するとこうなる

class prop():
    def __init__(self):
        self._val = 0
    def get_val(self):
        self._val += 1
        return self._val
    def set_val(self, val):
        self._val = val
    def del_val(self):
        del self._val

    val = property(get_val, set_val, del_val)

obj = prop()
print(obj.val) #1
print(obj.val) #2
print(obj.val) #3

ディスクリプタを使って定義するとこうなる

class descriptor():
    def __init__(self):
        self.val = 0
    def __get__(self, obj, type=None):
        self.val += 1
        return self.val

class incr():
    val = descriptor()

obj = incr()
print(obj.val) #1
print(obj.val) #2
print(obj.val) #3

ディスクリプタであれば、インクリメントする処理がdescriptorクラスで独立しているので、
別のクラスの属性アクセスの動作としても汎用的に利用する事ができる

class descriptor():
    def __init__(self):
        self.val = 0
    def __get__(self, obj, type=None):
        self.val += 1
        return self.val

class incr():
    #インクリメントされる属性valのみ必要なクラス
    val = descriptor()

class spam():
    #インクリメントされる属性valが必要なクラス
    val = descriptor()
    #このクラスでは別の属性も必要である
    y = 10


TODO

  • __getattribute__との関連
  • ディスクリプタの応用
  • 実装読む