はじめに
この記事は、 2014年9月12-14日に開催された PyCon JP 2014 で発表した内容をまとめたものです。
ディスクリプタとは
ディスクリプタとは、以下のようなメソッドを定義したオブジェクトのことです。
class Descriptor(object):
def __get__(self, obj, type=None): pass
def __set__(self, obj, value): pass
def __delete__(self, obj): pass
Python では、特定の性質を持つオブジェクトが実装すべき一連のメソッドのことをプロトコルと呼びます (代表的なプロトコルとして イテレータプロトコル などがあります)。ディスクリプタはそのようなプロトコルの一種です。
プロパティや、メソッド(静的メソッド、クラスメソッド、インスタンスメソッド)、 super
など、 Python の基本的な機能の背後にはこのディスクリプタが使われています。また、ディスクリプタは汎用的なプロトコルなので、ユーザが定義することも可能です。
ディスクリプタには大きく分けて2つの種類があります。
- データディスクリプタ
-
__get__
と__set__
の両方を定義しているディスクリプタ
-
- 非データディスクリプタ
-
__get__
だけを定義しているディスクリプタ
-
データディスクリプタは通常の属性アクセスと同じような振る舞いをするもので、プロパティがその典型です。非データディスクリプタは典型的にはメソッド呼び出しで使われます。
データディスクリプタの中で __set__
が呼び出されたときに AttributeError
が送出されるものは「読み取り専用のデータディスクリプタ」と呼ばれます。 fset
が定義されない読み取り専用のプロパティは、非データディスクリプタではなく読み取り専用のデータディスクリプタという分類になります。
この分類は属性アクセスの際の優先順位に影響します。具体的に、優先順位は以下の順になっています。
- データディスクリプタ
- インスタンスの属性辞書
- 非データディスクリプタ
これについてはなぜそうなっているかあとで詳しく見ていきます。
プロパティとの違い
ここで、ディスクリプタとプロパティはどう違うのかという疑問を持つかもしれません。
まず用途の違いを挙げると、プロパティは通常クラス定義の中でデコレータとして使い、そのクラスのインスタンスの属性アクセスをカスタマイズするために使います。一方、ディスクリプタは特定のクラスとは独立に定義されるもので、他のクラスの属性アクセスをカスタマイズするために使います。
より本質的な関係として、プロパティはディスクリプタの一種です。つまり、ディスクリプタの方が応用範囲が広いということで、逆に言うとプロパティはディスクリプタのよくある使い方に特化されたものと言うことができます。
X.Y
の裏で何が行われているか
ソースコードに X.Y
と書いた場合、その単純な見た目に反して裏側で起きていることは複雑です。実際、 X
がクラスかインスタンスか、 Y
がプロパティかメソッドか通常の属性かによって、それぞれ何が起きるかが異なります。
インスタンス属性の場合
インスタンス属性の場合、インスタンスの属性辞書 __dict__
から指定されたキーに対応する値を参照するという意味になります。
class C(object):
def __init__(self):
self.x = 1
obj = C()
assert obj.x == obj.__dict__['x']
クラス属性の場合
クラス属性の場合、クラス経由でもインスタンス経由でも、クラスの属性辞書から値を参照するという意味になります。
class C(object):
x = 1
assert C.x == C.__dict__['x']
obj = C()
assert obj.x == C.__dict__['x']
ここまでは話は簡単です。
プロパティの場合
プロパティの場合、クラスから参照した場合はプロパティ自身になり、インスタンスから参照した場合は関数の戻り値になります。
class C(object):
@property
def x(self):
return 1
assert C.x == C.__dict__['x'].__get__(None, C)
# クラスから参照した場合はプロパティ自身
assert isinstance(C.x, property)
obj = C()
assert obj.x == C.__dict__['x'].__get__(obj, C)
# インスタンスから参照した場合は関数の戻り値
assert obj.x == 1
その裏では、クラスの属性辞書から値を参照して、そのオブジェクトの __get__
メソッドが呼ばれます。この部分でディスクリプタが使われています。このとき、クラス経由の場合は __get__
の第1引数が None
、インスタンス経由の場合はそのインスタンスになり、この違いによって得られる値が違ってきます。
メソッドの場合
メソッドの場合も、基本的にプロパティと同じです。裏でディスクリプタが呼び出されるため、クラス経由で参照した場合とインスタンス経由で参照した場合で異なる値が得られます。
class C(object):
def x(self):
return 1
assert C.x == C.__dict__['x'].__get__(None, C)
obj = C()
assert obj.x == C.__dict__['x'].__get__(obj, C)
assert C.x != obj.x
ディスクリプタと __getattribute__
との関係
__getattribute__ をオーバーライドすると、クラスの全部の属性アクセスをカスタマイズできます。一方、ディスクリプタを使うと特定の属性アクセスをカスタマイズできるという点が違います。
さらに、組み込み型の __getattribute__
の実装の中でディスクリプタを考慮した処理が行われていて、その結果としてディスクリプタが意図した通りに動くことになります。こちらの方が本質的な関係です。
__getattribute__
を実装する代表的なクラスは object
と type
それに super
です。ここでは object
と type
を比較していきます。
Python の ソースコード の中で object
型に対応した PyBaseObject_Type
構造体が定義されていて、この中で tp_getattro
というスロットに PyObject_GenericGetAttr
という関数が指定されているので、 object.__getattribute__
はこの関数を呼び出します。
この関数の定義は Objects/object.c にありますが、これを Python の疑似コードで表すと以下のようになります:
def object_getattribute(self, key):
"Emulate PyObject_GenericGetAttr() in Objects/object.c"
tp = type(self)
attr = PyType_Lookup(tp, key)
if attr:
if hasattr(attr, '__get__') and hasattr(attr, '__set__'):
# data descriptor
return attr.__get__(self, tp)
if key in self.__dict__:
return self.__dict__[key]
if attr:
if hasattr(attr, '__get__'):
return attr.__get__(self, tp)
return attr
raise AttributeError
大きく3つのブロックがあり、それぞれ 1) データディスクリプタ呼び出し、 2) インスタンス自身の属性辞書参照、 3) 非データディスクリプタ呼び出しまたはクラスの属性辞書参照 を行っています。
まず最初に、オブジェクトのクラスを取得して、そのクラスの属性を検索します。ここで PyType_Lookup
は、あるクラスとその親クラスを順に辿って属性辞書から指定されたキーに対応する値を返す関数だと思ってください。ここで属性が見つかって、それがデータディスクリプタだったらその __get__
が呼ばれます。データディスクリプタが見つからなかった場合、インスタンスの属性辞書が参照されて値があればそれが返ります。最後にもう一度クラス属性があるかチェックして、それがディスクリプタなら __get__
が呼ばれ、ディスクリプタでなければその値自体が返ります。何も値が見つからなければ AttributeError
が送出されます。
type.__getattribute__
も同様に Objects/typeobject.c
の中の
PyType_Type
構造体で定義されています (http://hg.python.org/cpython/file/v3.4.1/Objects/typeobject.c#l3122 )。
これを Python の疑似コードであらわすと、以下のようになります:
def type_getattribute(cls, key):
"Emulate type_getattro() in Objects/typeobject.c"
meta = type(cls)
metaattr = PyType_Lookup(meta, key)
if metaattr:
if hasattr(metaattr, '__get__') and hasattr(metaattr, '__set__'):
# data descriptor
return metaattr.__get__(cls, meta)
attr = PyType_Lookup(cls, key)
if attr:
if hasattr(attr, '__get__'):
return attr.__get__(None, cls)
return attr
if metaattr:
if hasattr(metaattr, '__get__'):
return metaattr.__get__(cls, meta)
return metaattr
raise AttributeError
前半と後半は object
の場合と同じような処理をしているので割愛するとして (インスタンスに対するクラスがクラスに対するメタクラスに対応していることに注意してください)、真ん中のブロックが object
の場合と異なっています。 object
の場合は単に属性辞書の参照だけだったのが、クラスの場合は親クラスを辿って属性辞書を参照して、さらにそれがディスクリプタだったらディスクリプタの __get__
を呼び出しています。
ここまで見てきたことをまとめると、
- ディスクリプタは常にクラスの属性辞書から取得される
- ディスクリプタは常にクラスを伴って呼ばれる (
object
の場合はtype(self)
が、type
の場合はcls
が__get__
の引数になっています) - データディスクリプタはインスタンスの属性辞書よりも優先される
たとえばこんなコードがあったときに、 __dict__
に値を直接突っ込んでも、プロパティの方が優先されています。
class C(object):
@property
def x(self):
return 0
>>> o = C()
>>> o.__dict__['x'] = 1
>>> o.x
0
ディスクリプタの具体例
ここからは具体的なディスクリプタの例をいくつか見ていきます。
プロパティ
ディスクリプタプロトコルに従って、プロパティを以下のような Pure Python のコードとして定義することができます。
class Property(object):
"Emulate PyProperty_Type() in Objects/descrobject.c"
def __init__(self, fget=None, fset=None, fdel=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
def __get__(self, obj, klass=None):
if obj is None:
# via class
return self
if self.fget is not None:
return self.fget(obj)
raise AttributeError
def __set__(self, obj, value):
if self.fset is not None:
self.fset(obj, value)
raise AttributeError
def __delete__(self, obj):
if self.fdel is not None:
self.fdel(obj)
raise AttributeError
__get__
の中で、 obj
が None
だったら、つまりクラス経由で呼び出されたら自分自身を返します。コンストラクタで渡された fget
が None
でなければ fget
を呼び出して、 None
だったら AttributeError
を投げています。
静的メソッド
staticmethod
の疑似コードは以下の通りです。
class StaticMethod(object):
"Emulate PyStaticMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
def __get__(self, obj, klass=None):
return self.f
これは簡単で、 __get__
が呼ばれたときに常に関数自体を返しています。このため、 staticmethod
はクラス経由で呼んでもインスタンス経由で呼んでも、元の関数と同じ振る舞いをします。
クラスメソッド
classmethod
の疑似コードは以下の通りです。
class ClassMethod(object):
"Emulate PyClassMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
def __get__(self, obj, klass=None):
if klass is None:
klass = type(obj)
return types.MethodType(self.f, klass)
__get__
が呼ばれると、関数とクラスから MethodType
オブジェクトを作って返しています。実際には直後にこのオブジェクトの __call__
が呼ばれることになります。
インスタンスメソッド
インスタンスメソッドは、実はその実体は関数です。たとえばこんなクラスと関数があったときに、
class C(object):
pass
def f(self, x):
return x
関数 f
の __get__
を呼び出すと MethodType
オブジェクトが返ってきます。これを呼ぶと、あたかもインスタンスメソッドの呼び出しを行ったのと同じ結果が返ってきます。この場合、 f
はクラス C
と何の関係もない関数ですが、結果的にメソッドとして呼ばれていることになります。
obj = C()
# obj.f(1) をエミュレートする
meth = f.__get__(obj, C)
assert isinstance(meth, types.MethodType)
assert meth(1) == 1
関数はディスクリプタであるということを、もっと極端に表したのがこの例になります。
>>> def f(x, y): return x + y
...
>>> f
<function f at 0x10e51b1b8>
>>> f.__get__(1)
<bound method int.f of 1>
>>> f.__get__(1)(2)
3
ここで定義している関数 f
はメソッドでも何でもない単なる2引数の関数ですが、その __get__
を呼び出すと、 bound method が返ってきます。それに引数を渡して呼び出すと、関数呼び出しが行われるということが確認できます。このように、すべての関数はディスクリプタであって、クラス経由で呼んだ場合にディスクリプタの働きでそれがメソッドとして振る舞う、ということが分かります。
インスタンスメソッド、つまりディスクリプタとしての関数を疑似コードで表すとこんな感じになります。
class Function(object):
"Emulate PyFunction_Type() in Objects/funcobject.c"
def __get__(self, obj, klass=None):
if obj is None:
return self
return types.MethodType(self, obj)
クラス経由で呼ばれたときは自分自身を返して、インスタンス経由で呼ばれたときは関数とインスタンスから MethodType
オブジェクトを作って返しています。
MethodType.__call__
の疑似コードは以下の通りです。やっていることは単純で、 __self__
と __func__
を取り出して、関数の第一引数に self
を追加して関数を呼んでいるだけです。
def method_call(meth, *args, **kw):
"Emulate method_call() in Objects/classobject.c"
self = meth.__self__
func = meth.__func__
return func(self, *args, **kw)
ここまでの話をまとめると、
obj.func(x)
というメソッド呼び出しは、以下のような処理と等価です。
func = type(obj).__dict__['func']
meth = func.__get__(obj, type(obj))
meth.__call__(x)
これは、最終的に以下のような関数呼び出しと等価になります。
func(obj, x)
ここでちょっと話はそれますが、 Python でメソッドの第1引数がなぜ self
なのかということについて考えたいと思います。その理由は、これまでの話を踏まえると以下のように説明できます。 Python ではインスタンスメソッドの実体は関数であって、インスタンスメソッドの呼び出しがディスクリプタの働きによって最終的に単なる関数呼び出しに変換されます。単なる関数なので、 self
に相当するものを渡す場合に引数として渡すのが自然です。もし第1引数の self
を省略できるとしたら、関数呼び出しとメソッド呼び出しで異なる規約にしなければならず、言語仕様が複雑になります。関数とメソッドを別扱いするのではなく、ディスクリプタを使ってメソッド呼び出しを関数呼び出しに変換する Python の仕組みは、非常に巧妙だと思います。
Python 3 の場合、クラス経由でインスタンスメソッドを参照した場合は関数自体が返ってきますが、 Python 2 の場合は unbound method というものが返ってきます。関数自体を参照するには __func__
という属性を参照する必要があります。この書き方は Python 3 だとエラーになるので、もしこういうコードがある場合 Python 3 に移植する際に注意してください。 Python 3 ではそもそも unbound method という概念がなくなりました。
class C(object):
def f(self):
pass
$ python3
>>> C.f # == C.__dict__['f']
<function C.f at 0x10356ab00>
$ python2
>>> C.f # != C.__dict__['f']
<unbound method C.f>
>>> C.f.__func__ # == C.__dict__['f']
<function f at 0x10e02d050>
super
ディスクリプタが使われている別の例として super
があります。以下の例を見てください。
class C(object):
def x(self):
pass
class D(C):
def x(self):
pass
class E(D):
pass
obj = E()
assert super(D, obj).x == C.__dict__['x'].__get__(obj, D)
この例では、 super(D, obj).x
はクラス C
の属性辞書から x
に対応する値を取得してその __get__
に obj
と D
を引数にして呼び出すという意味になります。ここでポイントは、属性を取得するクラスが D
ではなく C
だということです。その鍵は super
クラスの __getattribute__
の実装にあります。
super.__getattribute__
を疑似コードで表すと以下のようになります。
def super_getattribute(su, key):
"Emulate super_getattro() in Objects/typeobject.c"
starttype = su.__self_class__
mro = iter(starttype.__mro__)
for cls in mro:
if cls is su.__self_class__:
break
# Note: mro is an iterator, so the second loop
# picks up where the first one left off!
for cls in mro:
if key in cls.__dict__:
attr = cls.__dict__[key]
if hasattr(attr, '__get__'):
return attr.__get__(su.__self__, starttype)
return attr
raise AttributeError
最初に指定されたクラスの mro 継承ツリーから、そのクラスの「次」(あるいは継承ツリーの「上」)のクラスを検索します。そしてそこを起点として継承ツリーを辿りながら属性辞書を参照して、見つかった属性がディスクリプタだったらディスクリプタ呼び出しを行っています。これが super
のからくりです。
super
はディスクリプタでもあります。ただし、現在の Python ではこのことはあまり有効に使われていないようです。 Python のソースコードの中で唯一、テストの中でこんなコードを見つけました: http://hg.python.org/cpython/file/v3.4.1/Lib/test/test_descr.py#l2308
調べてみると PEP 367 で self.__super__.foo()
という仕様が提案されていて、もしかしたらこれと関係するのかもしれません。ちなみにこの PEP は最終的に PEP 3135 として Python 3 で採用されましたが、その際は super()
の引数が省略できるという形になって、この書き方は採用されませんでした。
reify
最後にユーザ定義のディスクリプタの例を挙げます。
これはウェブフレームワーク Pyramid の中にある reify
のコードです。 reify
は、キャッシュ付きプロパティのようなもので、似たような機能は他のフレームワークにも存在しますが、 Pyramid の実装はディスクリプタを使った非常にスマートなものになっています。ポイントは __get__
メソッドの中で setattr
を実行している部分です。ここで関数呼び出しで得られた値をインスタンスの属性辞書に設定していて、これによって次回からはディスクリプタ呼び出しが発生しなくなります。なぜなら reify
は非データディスクリプタなので、インスタンスの属性辞書の方が優先されるからです。
まとめ
- ディスクリプタは、属性参照をカスタマイズするためのプロトコルです。
- ディスクリプタにはデータディスクリプタと非データディスクリプタがあって、優先順位が異なります。
- ディスクリプタは、プロパティやメソッドなどで使われている他、オリジナルのディスクリプタを定義することもできる汎用的なプロトコルです。