オブジェクトの属性を操る(4) - ディスクリプタ

【 目次 】

ディスクリプタ

ディスクリプタはディスクリプタプロトコルを実装したクラスを指すようで、 以下のメソッドのいずれか1つ以上を実装していればディスクリプタ(クラス)という事になる。

あるクラス(オーナクラスという)の属性にディスクリプタクラスのインスタンスを定義する事で、
ディスクリプタがあたかもそのクラスのインスタンスの属性のようにアクセス
できるようになる。

文章では分かりづらいので、公式ドキュメントに少し手を加えたサンプルを。
まず、ディスクリプタクラスを定義。

class MyDscCls(object):
    def __init__(self, initval_value=None):
        self.value = initval_value

    def __get__(self, oner_inst, owner_cls):
        print u"__get__を実行"
        return self.value

    def __set__(self, oner_inst, value):
        print u"__set__を実行", value
        self.value = value

オーナクラスMyOnerClsを定義して、
MyOnerClsクラスのメンバーxにディスクリプタクラスMyDscClsのインスタンスを設定、
オーナクラスのインスタンスmを生成。

class MyOwnerCls(object):
    x = MyDscCls(10)
    y = 5

m = MyOnerCls()

これにより、MyOnerClsの属性xがディスクリプタの役割を果たす事になる。

print m.x

を実行すると
ディスクリプタクラスMyDscClsの__get__メソッドが実行されて

print MyOwnerCls.__dict__["x"].__get__(m, MyOwnerCls)

実行結果

__get__を実行
10

m.xに値を代入

m.x = 20

を実行すると

MyOwnerCls.__dict__["x"].__set__(m,20)

が実行されて

実行結果

__set__を実行 20

m.xの値にあたかも20が代入されたように見える。

ちなみに

MyOwnerCls.x

MyOwnerCls.__dict__["x"]

は、通常のクラス変数の場合は等価であるが

MyOwnerCls.xがディスクリプタに展開された結果になるのに対して、MyOwnerCls.__dict__["x"]はディスクリプタそのものを示す。
これは

print type(MyOwnerCls.x)

実行結果

<type 'int'>

と、

print type(MyOwnerCls.__dict__["x"])

実行結果

<class '__main__.MyDscCls'>

の結果により確認できる。

MyOwnerCls.xの型がディスクリプタによって得られた値の型intになるのに対して、MyOwnerCls.__dict__["x"]の型はディスクリプタクラスMyDscClsを示している。

ディスクリプタの値をオーナクラスインスタンスに格納する。

ディスクリプタxはクラスMyOnerClsのクラス属性として定義される。
そして、この例ではディスクリプタxの値はMyDscClsのインスタンス変数self.valueに保存されている。
わかりずらいのだが、これによりMyDscClsのインスタンス変数self.valueはMyOnerClsクラスに従属する値となってしまう。

すなわちディスクリプタの値はクラス変数と同様にクラス固有の値となってしまう。

試しに、m.xに100を代入した後、新しいMyOwnerClsのインスタンスm2を生成してm2.xの値を確認してみる。

m.x=100
m2=MyDscCls()
print m2.x

実行結果

__set__を実行 100
__get__を実行
100

m.xの値を変更するとm2.xの値も変更されてしまう事になる。

では、ディスクリプタにインスタンス固有の値を持たせるにはどうしてら良いだろう。
これはディスクリプタの値をオーナクラスのインスタンス変数に格納すれば良い。

ディスクリプタクラスの特殊メソッド__get__,__set__のselfの後の2つ目の引数(ここではoner_instという引数名を付けている)にはオーナクラスのインスタンスが渡されている。
このoner_instの属性としてディスクリプタの値を格納する。

ちなみに__get__メソッド3つ目の引数(owner_cls)にはオーナクラスであるMyOwnerClsクラスオブジェクトが渡される。

# ディスクリプタクラスを定義
class MyDscCls(object):
    def __get__(self, oner_inst, owner_cls):
        return oner_inst._x

    def __set__(self, oner_inst, value):
        oner_inst._x = value

# オーナクラスを定義
class MyOwnerCls(object):
    x = MyDscCls()

    def __init__(self, x):
        self._x = x

オーナクラスのインスタンス生成,ディスクリプタの値を確認

m = MyOwnerCls(10)
m2 = MyOwnerCls(20)

print m.x
print m2.x

実行結果

10
20

インスタンスmとm2で別々の値を保持できる事になる。

データディスクリプタと非データディスクリプタ

プロパティ,関数,メソッドの実装にはディスクリプタが深くかかわっている。

__get__メソッドと__set__メソッドの両方を定義ているディスクリプタをデータディスクリプタ,__get__メソッドのみを定義しているディスクリプタを非データディスクリプタと言う。
プロパティはデータディスクリプタで、関数は非データディスクリプタになるらしい。

読み込み専用のデータディスクリプタを作るには、 __get__() と __set__() の両方を定義し、 __set__() が呼び出されたときに AttributeErrorを送出する。

プロパティとディスクリプタ

ディスクリプタはプロパティをもっと高度にしたもの,って言うかプロパティはディスクリプタを使って実装されているもようで
propertyはオブジェクト」での例の

my_inst.x="xxx"
print my_inst.x

は、実はディスクリプタの特殊メソッドを使った

MyCls.x.__set__(my_inst, "xxx")
print MyCls.x.__get__(my_inst)

に置き換えられる。
そして、__set__メソッド,__get__メソッドの内部から前述のfset,fgetが呼ばれる。

プロパティのディスクリプタによる実装の擬似コードについては

関数,メソッドとディスクリプタ

関数もオブジェクトであり、

def func(arg):
    print u"funcを実行", "arg=",arg

に対して、関数funcの型は

print type(func)

実行結果

<type 'function'>

functionクラスのインスタンスという事になる。

ちなみにfunctionクラスはpythonのコードから直接呼び出せないようで、
functionクラスから何がアクセスできるか確認しようとして

print dir(function)

などと記述するとエラーになってしまう。

実行結果

NameError: name 'function' is not defined

functionクラスにアクセスするためには
typesモジュールに定義されているFunctionTypeを使うか
またはfunc.__class__を使って間接的にアクセスする事になる。

関数の呼び出しは

func("arg")

または

func.__call__("arg")

であるが、
関数もディスクリプタとして定義されているようで、
functionクラスの属性辞書である__dict__のメンバーに__get__メソッド,__set__メソッドが格納されているか、以下のコードで確認してみると

if "__get__" in func.__class__.__dict__:
    print u"__get__メソッドが定義されています。"
else:    
    print u"__get__メソッドが定義されていません。"

if "__set__" in func.__class__.__dict__:
    print u"__set__メソッドが定義されています。"
else:    
    print u"__set__メソッドが定義されていません。"

実行結果

__get__メソッドが定義されています。
__set__メソッドが定義されていません。

つまり、関数は__get__メソッドのみが定義されている非データディスクリプタという事になる。

以下の説明はあまり役に立つものでもないのと,こみっいた話しになって内容にいささか自信がないので軽く読み飛ばしていただきたいが、自分なりに関数とメソッドのディスクリプタとしての動作を確認してみた。

関数funcをディスクリプタとして呼び出す

ディスクリプタであるfunctionクラスに定義されている__get__メソッドを呼び出してみる。

__get__メソッドには3つの引数が渡されるが、functionクラスのインスタンスより__get__メソッドを呼び出す時には第1引数selfについてはpythonの内部動作により自動的にfunctionクラスのインスタンスfuncが渡されるので、第2引数と第3引数のみを渡してやる事になる。
ここでは、仮に第2引数としてオーナクラスのインスタンスの代わりにunicode文字列u"オーナクラスのインスタンス"を,第3引数として第2引数であるunicode文字列の__class__属性を渡してやる事にする。

oner_inst=u"オーナクラスのインスタンス"
func.__get__(oner_inst, oner_inst.__class__)()

実行結果

funcを実行 arg= オーナクラスのインスタンス

__get__メソッドによる戻り値を関数として実行してやると、関数funcの引数として__get__メソッドの第2引数であるunicode文字列u"オーナクラスのインスタンス"が渡されているのが確認できる。
これは、インスタンスメソッドとしての動作そのものではないか - つまりインスタンスメソッドの第1引数としてオーナクラスのインスタンスであるselfが__get__メソッドの処理により渡されていると解釈する事ができる。

インスタンスメソッドの動作

クラス内で関数を定義して、

class MyCls(object):
    def func(arg):
        print u"funcを実行", "arg=",arg

my_inst=MyCls()

関数を呼び出すと、

my_inst.func()

これは、ディスクリプタとして展開されて

MyCls.__dict__["func"].__get__(my_inst, MyCls)()

となり、あたかも

func(my_inst)

が呼び出されたかのようにふるまう。

実行結果

funcを実行 arg= <__main__.MyCls object at 0x02669F50>

これが、インスタンスメソッドが実行のからくりのようだ。

クラスメソッドとして呼び出す。

クラスメソッドはどうかというと
関数をクラスメソッドに変換して

class_method=classmethod(func)

クラスメソッドに対して__get__メソッドで呼び出すと

class_method.__get__("self", "cls")()

実行結果

funcを実行 arg= cls

これは__get__メソッドの第2引数を使って

func("cls")

として呼び出される事になる。 これは、クラスメソッドの引数にはオーナクラスが渡されることを意味する。

スタティックメソッドとして呼び出す。

そしてスタティックメソッドの場合は

static_method=staticmethod(func)

スタティックメソッドに対して__get__メソッドで呼び出すと

static_method.__get__("self", "cls")("arg_value")

実行結果

funcを実行 arg= arg_value

引数は直接、渡され以下のような

func("arg_value")

通常の関数のようにふるまうことになる。

メソッドディスクリプタの擬似コード

いささか、こじつけ的であるが強引にメソッドのディスクリプタについて擬似コードを書くと。

# インスタンスメソッドディスクリプタ
class MyInstMethod(object):
    def __init__(self, f):
        self.f = f

    def __get__(self, oner_inst, owner_cls):
          def newfunc(*args):
               return self.f(oner_inst, *args)
          return newfunc

# クラスメソッドディスクリプタ
class MyClsMethod(object):
    def __init__(self, f):
          self.f = f

    def __get__(self, oner_inst, owner_cls=None):
          if owner_cls is None:
               owner_cls = type(oner_inst)
          def newfunc(*args):
               return self.f(owner_cls, *args)
          return newfunc

# 静的メソッドディスクリプタ
class MyStaticMethod(object):
    def __init__(self, f):
        self.f = f

    def __get__(self, oner_inst, owner_cls):
        return self.f
def func(arg):
    print u"funcを実行", "arg=",arg

# オーナクラス
class MyCls(object):
    i_method=MyInstMethod(func)
    c_method=MyClsMethod(func)
    s_method=MyStaticMethod(func)

my_inst=MyCls()
my_inst.i_method()
my_inst.c_method()
my_inst.s_method("arg_value")

上記のインスタンスメソッドディスクリプタとクラスメソッドディスクリプタの擬似コードはtypeモジュールのMethodType を使って以下のように記述する事もできる。

import types

class MyInstMethod(object):
    def __init__(self, f):
        self.f = f

    def __get__(self, oner_inst, owner_cls):
        if oner_inst is None:
            return self
            oner_inst = type(oner_inst)
        return types.MethodType(self.f, oner_inst)

class MyClsMethod(object):
    def __init__(self, f):
          self.f = f

    def __get__(self, oner_inst, owner_cls=None):
        if owner_cls is None:
            owner_cls = type(oner_inst)
        return types.MethodType(self.f, owner_cls)

メソッドについてはまだまだ奥深いところがいろいろと、
それにつてはメソッドオブジェクト(1)~(3)で述べる事にする。

その他のディスクリプタについての参考記事

ディスクリプタについて他のサイトの記事も参考に。

オブジェクトが属性をたどる順序

これまで数回に渡ってオブジェクトの属性について述べてきた。
最後に、オブジェクトが属性をたどる順序について

によると
インスタンスが属性をたどる順序は

  1. データディスクリプタ
  2. インスタンスの属性辞書
  3. 非データディスクリプタ

そしてクラスがクラスの属性をたどる順序は

  1. データディスクリプタ
  2. 親クラスを辿って属性辞書を参照して、さらにそれがディスクリプタだったらディスクリプタの __get__ を呼び出す
  3. 非データディスクリプタ

という事になる。

ページのトップへ戻る