記述子ハウツーガイド—Pythonドキュメント

提供:Dev Guides
< PythonPython/docs/3.8/howto/descriptor
移動先:案内検索

記述子ハウツーガイド

著者
レイモンドヘッティンガー
コンタクト

概要

記述子を定義し、プロトコルを要約し、記述子がどのように呼び出されるかを示します。 カスタム記述子と、関数、プロパティ、静的メソッド、クラスメソッドなどのいくつかの組み込みPython記述子を調べます。 純粋なPythonの同等物とサンプルアプリケーションを提供することにより、それぞれがどのように機能するかを示します。

記述子について学ぶことで、より大きなツールセットにアクセスできるだけでなく、Pythonがどのように機能するかをより深く理解し、そのデザインの優雅さを理解することができます。


定義と紹介

一般に、記述子は「バインディング動作」を持つオブジェクト属性であり、その属性アクセスは記述子プロトコルのメソッドによってオーバーライドされています。 それらのメソッドは、__get__()__set__()、および__delete__()です。 これらのメソッドのいずれかがオブジェクトに対して定義されている場合、それは記述子であると言われます。

属性アクセスのデフォルトの動作は、オブジェクトのディクショナリから属性を取得、設定、または削除することです。 たとえば、a.xには、a.__dict__['x']type(a).__dict__['x']の順に始まり、メタクラスを除くtype(a)の基本クラスまで続くルックアップチェーンがあります。 ルックアップされた値が記述子メソッドの1つを定義するオブジェクトである場合、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

これですべてです。 これらのメソッドのいずれかを定義すると、オブジェクトは記述子と見なされ、属性として検索されたときにデフォルトの動作をオーバーライドできます。

オブジェクトが__set__()または__delete__()を定義している場合、それはデータ記述子と見なされます。 __get__()のみを定義する記述子は、非データ記述子と呼ばれます(通常、メソッドに使用されますが、他の用途も可能です)。

データ記述子と非データ記述子は、インスタンスのディクショナリのエントリに関してオーバーライドが計算される方法が異なります。 インスタンスのディクショナリにデータ記述子と同じ名前のエントリがある場合、データ記述子が優先されます。 インスタンスのディクショナリに非データ記述子と同じ名前のエントリがある場合、ディクショナリエントリが優先されます。

読み取り専用のデータ記述子を作成するには、__get__()__set__()の両方を定義し、__set__()が呼び出されたときに AttributeError を発生させます。 __set__()メソッドを例外発生プレースホルダーで定義するだけで、データ記述子にすることができます。


記述子の呼び出し

記述子は、そのメソッド名で直接呼び出すことができます。 たとえば、d.__get__(obj)です。

または、属性へのアクセス時に記述子が自動的に呼び出されるのが一般的です。 たとえば、obj.dobjの辞書でdを検索します。 dがメソッド__get__()を定義している場合、d.__get__(obj)は以下にリストされている優先順位規則に従って呼び出されます。

呼び出しの詳細は、objがオブジェクトであるかクラスであるかによって異なります。

オブジェクトの場合、機械は object .__ getattribute __()にあり、b.xtype(b).__dict__['x'].__get__(b, type(b))に変換します。 実装は、インスタンス変数よりもデータ記述子に優先順位を与え、非データ記述子よりもインスタンス変数に優先順位を与え、提供されている場合は__getattr__()に最低の優先順位を割り当てる優先順位チェーンを介して機能します。 完全なC実装は、:source: `Objects / object.c`PyObject_GenericGetAttr()にあります。

クラスの場合、機械はtype.__getattribute__()にあり、B.xB.__dict__['x'].__get__(None, B)に変換します。 純粋な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の直後にある基本クラスAobj.__class__.__mro__で検索し、A.__dict__['m'].__get__(obj, B)を返します。 記述子でない場合、mは変更されずに返されます。 辞書にない場合、mobject .__ getattribute __()を使用した検索に戻ります。

実装の詳細は、:source: `Objects / typeobject.c`super_getattro()にあります。 純粋なPythonの同等物は、 Guidoのチュートリアルにあります。

上記の詳細は、記述子のメカニズムがオブジェクトタイプ、および super()__getattribute__()メソッドに埋め込まれていることを示しています。 クラスは、オブジェクトから派生した場合、または同様の機能を提供するメタクラスがある場合に、この機構を継承します。 同様に、クラスは__getattribute__()をオーバーライドすることにより、記述子の呼び出しをオフにすることができます。


記述子の例

次のコードは、getまたはsetごとにメッセージを出力するデータ記述子をオブジェクトとするクラスを作成します。 __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を介してセル値へのアクセスを許可する場合があります。 その後のプログラムの改善では、アクセスのたびにセルを再計算する必要があります。 ただし、プログラマーは、属性に直接アクセスする既存のクライアントコードに影響を与えたくありません。 解決策は、値属性へのアクセスをプロパティデータ記述子でラップすることです。

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

関数とメソッド

Pythonのオブジェクト指向機能は、関数ベースの環境に基づいて構築されています。 非データ記述子を使用して、2つはシームレスにマージされます。

クラスディクショナリは、メソッドを関数として格納します。 クラス定義では、メソッドは def または lambda を使用して記述されます。これは関数を作成するための通常のツールです。 メソッドは、最初の引数がオブジェクトインスタンス用に予約されているという点で、通常の関数とのみ異なります。 Pythonの慣例により、インスタンス参照は self と呼ばれますが、 this またはその他の変数名と呼ばれることもあります。

メソッド呼び出しをサポートするために、関数には、属性アクセス中にメソッドをバインドするための__get__()メソッドが含まれています。 これは、すべての関数が非データ記述子であり、オブジェクトから呼び出されたときにバインドされたメソッドを返すことを意味します。 純粋な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 and
# the bound instance.
>>> d.f.__func__
<function D.f at 0x1012e5ae8>
>>> d.f.__self__
<__main__.D object at 0x1012e1f98>

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

非データ記述子は、関数をメソッドにバインドする通常のパターンを変化させるための単純なメカニズムを提供します。

要約すると、関数には__get__()メソッドがあり、属性としてアクセスしたときにメソッドに変換できます。 非データ記述子は、obj.f(*args)呼び出しをf(obj, *args)に変換します。 klass.f(*args)を呼び出すとf(*args)になります。

このチャートは、バインディングとその2つの最も有用なバリアントをまとめたものです。

変身 オブジェクトから呼び出されます クラスから呼び出されます
関数 f(obj、* args) f(* args)
staticmethod f(* args) f(* args)
classmethod f(type(obj)、* args) f(klass、* args)


静的メソッドは、変更なしで基になる関数を返します。 c.fまたはC.fのいずれかを呼び出すことは、object.__getattribute__(c, "f")またはobject.__getattribute__(C, "f")を直接検索することと同じです。 その結果、関数はオブジェクトまたはクラスのいずれからも同じようにアクセスできるようになります。

静的メソッドの適切な候補は、self変数を参照しないメソッドです。

たとえば、統計パッケージには、実験データのコンテナクラスが含まれる場合があります。 このクラスは、データに依存する平均、平均、中央値、およびその他の記述統計を計算するための通常のメソッドを提供します。 ただし、概念的には関連しているがデータに依存しない便利な関数がある場合があります。 たとえば、erf(x)は、統計作業で発生する便利な変換ルーチンですが、特定のデータセットに直接依存しません。 オブジェクトまたはクラスs.erf(1.5) --> .9332またはSample.erf(1.5) --> .9332から呼び出すことができます。

staticmethodsは変更なしで基になる関数を返すため、呼び出し例は刺激的ではありません。

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

非データ記述子プロトコルを使用すると、 staticmethod()の純粋なPythonバージョンは次のようになります。

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)

この動作は、関数がクラス参照のみを持つ必要があり、基になるデータを気にしない場合に役立ちます。 classmethodsの用途の1つは、代替クラスコンストラクターを作成することです。 Python 2.3では、クラスメソッド dict.fromkeys()は、キーのリストから新しい辞書を作成します。 純粋な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()の純粋な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