Python を支える技術 ディスクリプタ編 #pyconjp

# はじめに

この記事は、 2014年9月12-14日に開催された [PyCon JP 2014](https://pycon.jp/2014/) で発表した内容をまとめたものです。

# ディスクリプタとは

ディスクリプタとは、以下のようなメソッドを定義したオブジェクトのことです。

```py3
class Descriptor(object):
    def __get__(self, obj, type=None): pass
    def __set__(self, obj, value): pass
    def __delete__(self, obj): pass
```

Python では、特定の性質を持つオブジェクトが実装すべき一連のメソッドのことをプロトコルと呼びます (代表的なプロトコルとして [イテレータプロトコル](http://docs.python.jp/3.4/library/stdtypes.html#typeiter) などがあります)。ディスクリプタはそのようなプロトコルの一種です。 

プロパティや、メソッド(静的メソッド、クラスメソッド、インスタンスメソッド)、 ``super`` など、 Python の基本的な機能の背後にはこのディスクリプタが使われています。また、ディスクリプタは汎用的なプロトコルなので、ユーザが定義することも可能です。

ディスクリプタには大きく分けて2つの種類があります。

* データディスクリプタ
  * ``__get__`` と ``__set__`` の両方を定義しているディスクリプタ
* 非データディスクリプタ
  * ``__get__`` だけを定義しているディスクリプタ

データディスクリプタは通常の属性アクセスと同じような振る舞いをするもので、プロパティがその典型です。非データディスクリプタは典型的にはメソッド呼び出しで使われます。

データディスクリプタの中で ``__set__`` が呼び出されたときに ``AttributeError`` が送出されるものは「読み取り専用のデータディスクリプタ」と呼ばれます。 ``fset`` が定義されない読み取り専用のプロパティは、非データディスクリプタではなく読み取り専用のデータディスクリプタという分類になります。

この分類は属性アクセスの際の優先順位に影響します。具体的に、優先順位は以下の順になっています。

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

これについてはなぜそうなっているかあとで詳しく見ていきます。

# プロパティとの違い

ここで、ディスクリプタとプロパティはどう違うのかという疑問を持つかもしれません。

まず用途の違いを挙げると、プロパティは通常クラス定義の中でデコレータとして使い、そのクラスのインスタンスの属性アクセスをカスタマイズするために使います。一方、ディスクリプタは特定のクラスとは独立に定義されるもので、他のクラスの属性アクセスをカスタマイズするために使います。

より本質的な関係として、プロパティはディスクリプタの一種です。つまり、ディスクリプタの方が応用範囲が広いということで、逆に言うとプロパティはディスクリプタのよくある使い方に特化されたものと言うことができます。

# ``X.Y`` の裏で何が行われているか

ソースコードに ``X.Y`` と書いた場合、その単純な見た目に反して裏側で起きていることは複雑です。実際、 ``X`` がクラスかインスタンスか、 ``Y`` がプロパティかメソッドか通常の属性かによって、それぞれ何が起きるかが異なります。

## インスタンス属性の場合

インスタンス属性の場合、インスタンスの属性辞書 ``__dict__`` から指定されたキーに対応する値を参照するという意味になります。

```py3
class C(object):
    def __init__(self):
        self.x = 1
  
obj = C()
assert obj.x == obj.__dict__['x']
```


## クラス属性の場合

クラス属性の場合、クラス経由でもインスタンス経由でも、クラスの属性辞書から値を参照するという意味になります。

```py3
class C(object):
    x = 1

assert C.x == C.__dict__['x']

obj = C()
assert obj.x == C.__dict__['x']
```

ここまでは話は簡単です。


## プロパティの場合

プロパティの場合、クラスから参照した場合はプロパティ自身になり、インスタンスから参照した場合は関数の戻り値になります。

```py3
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__`` の第１引数が ``None`` 、インスタンス経由の場合はそのインスタンスになり、この違いによって得られる値が違ってきます。


## メソッドの場合

メソッドの場合も、基本的にプロパティと同じです。裏でディスクリプタが呼び出されるため、クラス経由で参照した場合とインスタンス経由で参照した場合で異なる値が得られます。

```py3
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\_\_](http://docs.python.jp/3.4/reference/datamodel.html#object.__getattribute__) をオーバーライドすると、クラスの全部の属性アクセスをカスタマイズできます。一方、ディスクリプタを使うと特定の属性アクセスをカスタマイズできるという点が違います。

さらに、組み込み型の ``__getattribute__`` の実装の中でディスクリプタを考慮した処理が行われていて、その結果としてディスクリプタが意図した通りに動くことになります。こちらの方が本質的な関係です。

``__getattribute__`` を実装する代表的なクラスは ``object`` と ``type`` それに ``super`` です。ここでは ``object`` と ``type`` を比較していきます。

Python の [ソースコード](http://hg.python.org/cpython/file/v3.4.1/Objects/typeobject.c#l4208) の中で ``object`` 型に対応した ``PyBaseObject_Type`` 構造体が定義されていて、この中で ``tp_getattro`` というスロットに ``PyObject_GenericGetAttr`` という関数が指定されているので、 ``object.__getattribute__`` はこの関数を呼び出します。

この関数の定義は [Objects/object.c](http://hg.python.org/cpython/file/v3.4.1/Objects/object.c#l1012) にありますが、これを Python の疑似コードで表すと以下のようになります:

```py3

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
```


大きく３つのブロックがあり、それぞれ 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 の疑似コードであらわすと、以下のようになります:

```py3

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__`` に値を直接突っ込んでも、プロパティの方が優先されています。

```py3
class C(object):
    @property
    def x(self):
        return 0

>>> o = C()
>>> o.__dict__['x'] = 1
>>> o.x
0
```


# ディスクリプタの具体例

ここからは具体的なディスクリプタの例をいくつか見ていきます。

## プロパティ

ディスクリプタプロトコルに従って、プロパティを以下のような Pure Python のコードとして定義することができます。

```py3
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`` の疑似コードは以下の通りです。

```py3
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`` の疑似コードは以下の通りです。

```py3
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__`` が呼ばれることになります。


## インスタンスメソッド

インスタンスメソッドは、実はその実体は関数です。たとえばこんなクラスと関数があったときに、

```py3
class C(object):
    pass

def f(self, x):
    return x
```

関数 ``f`` の ``__get__`` を呼び出すと ``MethodType`` オブジェクトが返ってきます。これを呼ぶと、あたかもインスタンスメソッドの呼び出しを行ったのと同じ結果が返ってきます。この場合、 ``f`` はクラス ``C`` と何の関係もない関数ですが、結果的にメソッドとして呼ばれていることになります。


```py3
obj = C()
# obj.f(1) をエミュレートする
meth = f.__get__(obj, C)
assert isinstance(meth, types.MethodType)
assert meth(1) == 1
```


関数はディスクリプタであるということを、もっと極端に表したのがこの例になります。

```pycon
>>> 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 が返ってきます。それに引数を渡して呼び出すと、関数呼び出しが行われるということが確認できます。このように、すべての関数はディスクリプタであって、クラス経由で呼んだ場合にディスクリプタの働きでそれがメソッドとして振る舞う、ということが分かります。

インスタンスメソッド、つまりディスクリプタとしての関数を疑似コードで表すとこんな感じになります。

```py3
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`` を追加して関数を呼んでいるだけです。

```py3
def method_call(meth, *args, **kw):
    "Emulate method_call() in Objects/classobject.c"
    self = meth.__self__
    func = meth.__func__
    return func(self, *args, **kw)
```

ここまでの話をまとめると、

```py3
obj.func(x)
```

というメソッド呼び出しは、以下のような処理と等価です。

```py3
func = type(obj).__dict__['func']
meth = func.__get__(obj, type(obj))
meth.__call__(x)
```

これは、最終的に以下のような関数呼び出しと等価になります。

```py3
func(obj, x)
```

ここでちょっと話はそれますが、 Python でメソッドの第１引数がなぜ ``self`` なのかということについて考えたいと思います。その理由は、これまでの話を踏まえると以下のように説明できます。 Python ではインスタンスメソッドの実体は関数であって、インスタンスメソッドの呼び出しがディスクリプタの働きによって最終的に単なる関数呼び出しに変換されます。単なる関数なので、 ``self`` に相当するものを渡す場合に引数として渡すのが自然です。もし第１引数の ``self`` を省略できるとしたら、関数呼び出しとメソッド呼び出しで異なる規約にしなければならず、言語仕様が複雑になります。関数とメソッドを別扱いするのではなく、ディスクリプタを使ってメソッド呼び出しを関数呼び出しに変換する Python の仕組みは、非常に巧妙だと思います。

Python 3 の場合、クラス経由でインスタンスメソッドを参照した場合は関数自体が返ってきますが、 Python 2 の場合は unbound method というものが返ってきます。関数自体を参照するには ``__func__`` という属性を参照する必要があります。この書き方は Python 3 だとエラーになるので、もしこういうコードがある場合 Python 3 に移植する際に注意してください。 Python 3 ではそもそも unbound method という概念がなくなりました。

```py3
class C(object):
      def f(self):
          pass
```

```shell-session
$ 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`` があります。以下の例を見てください。

```py3
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__`` を疑似コードで表すと以下のようになります。

```py3
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](http://legacy.python.org/dev/peps/pep-0367/) で ``self.__super__.foo()`` という仕様が提案されていて、もしかしたらこれと関係するのかもしれません。ちなみにこの PEP は最終的に [PEP 3135](http://legacy.python.org/dev/peps/pep-3135/) として Python 3 で採用されましたが、その際は ``super()`` の引数が省略できるという形になって、この書き方は採用されませんでした。

## reify

最後にユーザ定義のディスクリプタの例を挙げます。


http://docs.pylonsproject.org/docs/pyramid/en/latest/_modules/pyramid/decorator.html#reify

これはウェブフレームワーク Pyramid の中にある ``reify`` のコードです。 ``reify`` は、キャッシュ付きプロパティのようなもので、似たような機能は他のフレームワークにも存在しますが、 Pyramid の実装はディスクリプタを使った非常にスマートなものになっています。ポイントは ``__get__`` メソッドの中で ``setattr`` を実行している部分です。ここで関数呼び出しで得られた値をインスタンスの属性辞書に設定していて、これによって次回からはディスクリプタ呼び出しが発生しなくなります。なぜなら ``reify`` は非データディスクリプタなので、インスタンスの属性辞書の方が優先されるからです。

# まとめ

* ディスクリプタは、属性参照をカスタマイズするためのプロトコルです。
* ディスクリプタにはデータディスクリプタと非データディスクリプタがあって、優先順位が異なります。
* ディスクリプタは、プロパティやメソッドなどで使われている他、オリジナルのディスクリプタを定義することもできる汎用的なプロトコルです。