デスクリプタ HowTo ガイド

著者

Raymond Hettinger

問い合わせ先

<python at rcn dot com>

概要

デスクリプタを定義し、プロトコルを要約し、デスクリプタがどのように呼び出されるか示します。カスタムのデスクリプタや、関数、プロパティ、静的メソッド、クラスメソッドを含む、いくつかの組み込み Python デスクリプタを考察します。等価な pure Python 版やサンプルアプリケーションを与えることにより、それぞれがどのように働くかを示します。

デスクリプタについて学ぶことにより、新しいツールセットが使えるようになるだけでなく、Python の仕組みや、洗練された設計のアプリケーションについてのより深い理解が得られます。

定義と導入

一般に、デスクリプタは "束縛動作 (binding behavior)" をもつオブジェクト属性で、その属性アクセスが、デスクリプタプロトコルのメソッドによってオーバーライドされたものです。このメソッドは、 __get__(), __set__(), および __delete__() です。これらのメソッドのいずれかが、オブジェクトに定義されていれば、それはデスクリプタと呼ばれます。

属性アクセスのデフォルトの振る舞いは、オブジェクトの辞書の属性の取得、設定、削除です。例えば a.x は、まず a.__dict__['x']、それから type(a).__dict__['x']、さらに type(a) のメタクラスを除く基底クラスへと続くというように探索が連鎖します。見つかった値が、デスクリプタメソッドのいずれかを定義しているオブジェクトなら、Python はそのデフォルトの振る舞いをオーバーライドし、代わりにデスクリプタメソッドを呼び出します。これがどの連鎖順位で行われるかは、どのデスクリプタメソッドが定義されているかに依ります。

デスクリプタは、強力な、多目的のプロトコルです。これはプロパティ、メソッド、静的メソッド、クラスメソッド、そして super() の背後にある機構です。これはバージョン 2.2 で導入された新スタイルクラスを実装するために、Python のいたるところで使われています。デスクリプタは、基幹にある C コードを簡潔にし、毎日の Python プログラムに、柔軟な新しいツール群を提供します。

デスクリプタプロトコル

descr.__get__(self, obj, type=None) --> value

descr.__set__(self, obj, value) --> None

descr.__delete__(self, obj) --> None

これで全てです。これらのメソッドのいずれかを定義すれば、オブジェクトはデスクリプタとみなされ、探索された際のデフォルトの振る舞いをオーバーライドできます。

あるオブジェクトが __get__()__set__() の両方を定義していたら、それはデータデスクリプタとみなされます。 __get__() だけを定義しているデスクリプタは、非データデスクリプタと呼ばれます (これらは典型的にはメソッドに使われますが、他の使い方も出来ます)。

データデスクリプタと非データデスクリプタでは、オーバーライドがインスタンスの辞書のエントリに関してどのように計算されるかが異なります。インスタンスの辞書に、データデスクリプタと同名の項目があれば、データデスクリプタの方が優先されます。インスタンスの辞書に、非データデスクリプタと同名の項目があれば、辞書の項目の方が優先されます。

読み出し専用のデータデスクリプタを作るには、 __get__()__set__() の両方を定義し、 __set__() が呼び出されたときに AttributeError が送出されるようにしてください。例外を送出する __set__() メソッドをプレースホルダとして定義すれば、データデスクリプタにするのに十分です。

デスクリプタの呼び出し

デスクリプタは、メソッド名で直接呼ぶことも出来ます。例えば、d.__get__(obj) です。

または、一般的に、デスクリプタは属性アクセスから自動的に呼び出されます。例えば、 obj.dobj の辞書から d を探索します。 d がメソッド __get__() を定義していたら、以下に列挙する優先順位に従って、 d.__get__(obj) が呼び出されます。

呼び出しの詳細は、obj がオブジェクトかクラスかに依ります。

オブジェクトでは、その機構は b.xtype(b).__dict__['x'].__get__(b, type(b)) に変換する object.__getattribute__() にあります。データデスクリプタの優先度はインスタンス変数より高く、インスタンス変数の優先度は非データデスクリプタより高く、(提供されていれば) __getattr__() の優先度が最も低いように実装されています。完全な C 実装は、 Objects/object.cPyObject_GenericGetAttr() で見つかります。

クラスでは、その機構は B.xB.__dict__['x'].__get__(None, B) に変換する type.__getattribute__() にあります。 pure Python では、このようになります:

def __getattribute__(self, key):
    "Emulate type_getattro() in Objects/typeobject.c"
    v = object.__getattribute__(self, key)
    if hasattr(v, '__get__'):
        return v.__get__(None, self)
    return v

憶えておくべき重要な点は:

  • デスクリプタは __getattribute__() メソッドによって呼び出される

  • __getattribute__() をオーバーライドすると、自動的なデスクリプタの呼び出しが行われなくなる

  • object.__getattribute__()type.__getattribute__() では、 __get__() の呼び出しが異なる。

  • データデスクリプタは、必ずインスタンス辞書をオーバーライドする。

  • 非データデスクリプタは、インスタンス辞書にオーバーライドされることがある。

super() が返したオブジェクトにはさらにデスクリプタを起動するカスタム __getattribute__() メソッドがあります。 super(B, obj).m() の呼び出しは、 B に続き基底クラス A を見つけるために obj.__class__.__mro__ を探索し、 A.__dict__['m'].__get__(obj, B) を返します。デスクリプタではない場合、 m が変更されずに返されます。辞書になければ、 mobject.__getattribute__() を使用した探索に戻ります。

実装の詳細は、 Objects/typeobject.csuper_getattro() と、 Guido's Tutorial にある等価な pure Python 版を参照してください。

上述の詳細は、デスクリプタの機構が、 __getattribute__() メソッドに埋めこまれ、 object, type, そして super() に使われているということを表しています。クラスは、 object から導出されたとき、または、同じような機能を提供するメタクラスをもつとき、この機構を継承します。同様に、 __getattribute__() をオーバーライドすることで、デスクリプタの呼び出しを無効にできます。

デスクリプタの例

以下のコードは、オブジェクトが取得と設定のたびにメッセージを表示するデータデスクリプタであるようなクラスを生成します。代わりに __getattribute__() をオーバーライドすると、全ての属性に対してこれができます。しかし、このデスクリプタは、少数の選ばれた属性を監視するのに便利です:

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
...
>>> m = MyClass()
>>> m.x
Retrieving var "x"
10
>>> m.x = 20
Updating var "x"
>>> m.x
Retrieving var "x"
20
>>> m.y
5

このプロトコルは単純ですが、ワクワクする可能性も秘めています。ユースケースの中には、あまりに一般的なので個別の関数の呼び出しにまとめられたものもあります。プロパティ、束縛のメソッド、静的メソッド、そしてクラスメソッドは、全てデスクリプタプロトコルに基づいています。

プロパティ

property() を呼び出すことで、属性へアクセスすると関数の呼び出しを引き起こす、データデスクリプタを簡潔に組み立てられます。シグネチャはこうです:

property(fget=None, fset=None, fdel=None, doc=None) -> property attribute

このドキュメントでは、管理された属性 x を定義する典型的な使用法を示します:

class C(object):
    def getx(self): return self.__x
    def setx(self, value): self.__x = value
    def delx(self): del self.__x
    x = property(getx, setx, delx, "I'm the 'x' property.")

デスクリプタの見地から property() がどのように実装されているかを見るために、等価な Python 版をここに挙げます:

class Property(object):
    "Emulate PyProperty_Type() in Objects/descrobject.c"

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)

組み込みの property() 関数は、ユーザインタフェースへの属性アクセスが与えられ、続く変更がメソッドの介入を要求するときに役立ちます。

例えば、スプレッドシートクラスが、Cell('b10').value でセルの値を取得できるとします。続く改良により、プログラムがアクセスの度にセルの再計算をすることを要求しました。しかしプログラマは、その属性に直接アクセスする既存のクライアントコードに影響を与えたくありません。この解決策は、property データデスクリプタ内に値属性へのアクセスをラップすることです:

class Cell(object):
    . . .
    def getvalue(self):
        "Recalculate the cell before returning value"
        self.recalc()
        return self._value
    value = property(getvalue)

関数とメソッド

Python のオブジェクト指向機能は、関数に基づく環境の上に構築されています。非データデスクリプタを使って、この 2 つはシームレスに組み合わされています。

クラス辞書は、メソッドを関数として保存します。クラス定義内で、メソッドは、関数を使うのに便利なツール、 deflambda を使って書かれます。メソッドの標準の関数との唯一の違いは、第一引数がオブジェクトインスタンスのために予約されていることです。 Python の慣習では、このインスタンスの参照は self と呼ばれますが、 this その他の好きな変数名で呼び出せます。

メソッドの呼び出しをサポートするために、関数の __get__() メソッドは属性アクセス時にメソッドを束縛します。これにより、すべての関数は、それがオブジェクトによって呼び出されたとき、束縛メソッドを返す非データデスクリプタになります。pure Python では、これはこのようにはたらきます:

class Function(object):
    . . .
    def __get__(self, obj, objtype=None):
        "Simulate func_descr_get() in Objects/funcobject.c"
        if obj is None:
            return self
        return types.MethodType(self, obj)

インタプリタを起動すると、この関数デスクリプタが実際にどうはたらくかを見られます:

>>> class D(object):
...     def f(self, x):
...         return x
...
>>> d = D()

# Access through the class dictionary does not invoke __get__.
# It just returns the underlying function object.
>>> D.__dict__['f']
<function D.f at 0x00C45070>

# Dotted access from a class calls __get__() which just returns
# the underlying function unchanged.
>>> D.f
<function D.f at 0x00C45070>

# The function has a __qualname__ attribute to support introspection
>>> D.f.__qualname__
'D.f'

# Dotted access from an instance calls __get__() which returns the
# function wrapped in a bound method object
>>> d.f
<bound method D.f of <__main__.D object at 0x00B18C90>>

# Internally, the bound method stores the underlying function,
# the bound instance, and the class of the bound instance.
>>> d.f.__func__
<function D.f at 0x1012e5ae8>
>>> d.f.__self__
<__main__.D object at 0x1012e1f98>
>>> d.f.__class__
<class 'method'>

静的メソッドとクラスメソッド

非データデスクリプタは、関数をメソッドに束縛する、各種の一般的なパターンに、単純な機構を提供します。

まとめると、関数は __get__() メソッドを持ち、属性としてアクセスされたとき、メソッドに変換されます。この非データディスクリプタは、 obj.f(*args) の呼び出しを f(obj, *args) に変換します。 klass.f(*args) を呼び出すと f(*args) になります。

このチャートは、束縛と、その 2 つの異なる便利な形をまとめています:

変換

オブジェクトから呼び出される

クラスから呼び出される

function

f(obj, *args)

f(*args)

静的メソッド

f(*args)

f(*args)

クラスメソッド

f(type(obj), *args)

f(klass, *args)

静的メソッドは、下にある関数をそのまま返します。 c.fC.f は、 object.__getattribute__(c, "f")object.__getattribute__(C, "f") を直接探索するのと同じです。結果として、関数はオブジェクトとクラスから同じようにアクセスできます。

静的メソッドにすると良いのは、 self 変数への参照を持たないメソッドです。

例えば、統計パッケージに、実験データのコンテナがあるとします。そのクラスは、平均、メジアン、その他の、データに依る記述統計を計算する標準メソッドを提供します。しかし、概念上は関係があっても、データには依らないような便利な関数もあります。例えば、 erf(x) は統計上の便利な変換ルーチンですが、特定のデータセットに直接には依存しません。これは、オブジェクトからでもクラスからでも呼び出せます: s.erf(1.5) --> .9332 または Sample.erf(1.5) --> .9332

静的メソッドは下にある関数をそのまま返すので、呼び出しの例は面白くありません:

>>> class E(object):
...     def f(x):
...         print(x)
...     f = staticmethod(f)
...
>>> print(E.f(3))
3
>>> print(E().f(3))
3

非データデスクリプタプロトコルを使うと、pure Python 版の staticmethod() は以下のようになります:

class StaticMethod(object):
    "Emulate PyStaticMethod_Type() in Objects/funcobject.c"

    def __init__(self, f):
        self.f = f

    def __get__(self, obj, objtype=None):
        return self.f

静的メソッドとは違って、クラスメソッドは関数を呼び出す前にクラス参照を引数リストの先頭に加えます。このフォーマットは、呼び出し元がオブジェクトでもクラスでも同じです:

>>> class E(object):
...     def f(klass, x):
...         return klass.__name__, x
...     f = classmethod(f)
...
>>> print(E.f(3))
('E', 3)
>>> print(E().f(3))
('E', 3)

この振る舞いは、関数がクラス参照のみを必要とし、下にあるデータを考慮しないときに便利です。クラスメソッドの使い方の一つは、代わりのクラスコンストラクタを作ることです。Python 2.3 では、クラスメソッド dict.fromkeys() は新しい辞書をキーのリストから生成します。等価な pure Python 版は:

class Dict(object):
    . . .
    def fromkeys(klass, iterable, value=None):
        "Emulate dict_fromkeys() in Objects/dictobject.c"
        d = klass()
        for key in iterable:
            d[key] = value
        return d
    fromkeys = classmethod(fromkeys)

これで一意なキーを持つ新しい辞書が以下のように構成できます:

>>> Dict.fromkeys('abracadabra')
{'a': None, 'r': None, 'b': None, 'c': None, 'd': None}

非データデスクリプタプロトコルを使った、 classmethod() の pure Python 版はこのようになります:

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)
        def newfunc(*args):
            return self.f(klass, *args)
        return newfunc