Pythonディスクリプタ解析

7744 ワード

構文の概要
一般に、ディスクリプタ(descriptor)は、ディスクリプタプロトコルメソッドによって書き換えられる「バインド動作」を持つオブジェクト属性(object attribute)である.これらの方法は、__get__()__set__()、および__delete__()である.オブジェクトが上記のいずれかのメソッドを定義している場合は、ディスクリプタです.ディスクリプタプロトコルの具体的な形式は次のとおりです.
descr.__get__(self, obj, type=None) --> value

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

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

ディスクリプタは本質的にクラスオブジェクトであり、ディスクリプタプロトコルの3つの方法のうち少なくとも1つを定義する.この3つの方法は、クラスのインスタンスが1つの所有者クラス(owner class)内に存在する場合にのみ有効であり、すなわち、記述器は所有者クラスまたはその親クラスの辞書__dict__に存在しなければならない.ここでは、ディスクリプタプロトコルを定義するディスクリプタクラスと、ディスクリプタを使用する所有者クラスの2つのクラスについて説明します.
ディスクリプタは装飾器として使用されることが多く、両者が混同されることが多い.ディスクリプタクラスは、パラメータを持たないディスクリプタクラスと同様に、関数オブジェクトにパラメータとして渡され、ディスクリプタクラスがcallableのインスタンスを返し、ディスクリプタがディスクリプタインスタンスを返します.
上の言葉を覚えて、次に例を挙げて説明します.
@Property
Pythonに内蔵されているproperty関数は最も有名な記述器の一つと言え、ほとんどの記述器の文章がそれを例にしています.propertyはCで実現されていますが、ここには等価な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はどうやって使いますか?次の例を見てください.
class C(object):
    def __init__(self):
        self._x = None

    @Property
    def x(self):
        """I'm the 'x' property."""
        return self._x

    @x.setter
    def x(self, value):
        assert value > 0
        self._x = value

    @x.deleter
    def x(self):
        del self._x

ソースコードと使用法を組み合わせてPropertyを解析した.@Propertyの使い方は装飾器です.等価性を次のように変換できます.
x = Property(x)

関数xは、位置パラメータとしてProperty.__init__()fgetに与えられ、新しいxが関数ではなく、__get__()法を完全に実現した記述器の例を得た.@x.setterの使い方は少し違います.実際には、上述したディスクリプタインスタンスxsetterメソッドを用いて、新しいインスタンスを再作成する.このとき、変数xは再び更新され、__get__()および__set__()の方法を完全に実装する新しい記述器を指す.setterメソッドに伝達される関数名はxでなければならない.そうでなければyであれば、装飾器の性質に従って、
y = x.setter(y)

新しいディスクリプタはyによって参照され、需要と一致しない.Propertyは、クラス「メンバー変数」にアクセスするようにget、setメソッドにアクセスする能力を提供する.
In [123]: c = C()

In [124]: c.x = 1

In [125]: c.x
Out[125]: 1

In [126]: c.x = 0
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
 in ()
----> 1 c.x = 0

 in __set__(self, obj, value)
     20         if self.fset is None:
     21             raise AttributeError("can't set attribute")
---> 22         self.fset(obj, value)
     23
     24     def __delete__(self, obj):

 in x(self, value)
     10     @x.setter
     11     def x(self, value):
---> 12         assert value > 0
     13         self._x = value
     14

AssertionError:

一般的な属性アクセスとは異なり、c.xがアクセスするのは単純な属性ではなく、x.__get__(c)に相当し、様々な複雑な方法を呼び出して属性を検査し、包装することができる.
では、ディスクリプタはどのようにアクセスされているのでしょうか.
呼び出しディスクリプタ
2種類の記述器がある:__get__()__set__()の方法を同時に定義した記述器を資料記述器(data descriptor)と呼び、__get__()のみを定義した記述器を非資料記述器(non-data descriptor)と呼ぶ.非資料記述器は、一般的なstaticmethodおよびclassmethodのようなクラスの方法によく用いられる.
前述したように、記述器は、所有者クラスまたはそのインスタンスで呼び出されることが多い.
インスタンスオブジェクトの場合、object.__getattribute__()c.xtype(c).__dict__['x'].__get__(c, type(c))に変換します.インスタンスにディスクリプタと重複するプロパティxがある場合はどうしますか?資料記述器と非資料記述器の違いは、インスタンス辞書に対する優先度が異なることである.ディスクリプタとインスタンス辞書の属性が重複すると、アクセス優先度によって、資料ディスクリプタ>同名インスタンス辞書の属性>非資料ディスクリプタが優先度が小さい場合に大きく上書きされます.上記のクラスCでは、資料記述器xに優先的にアクセスする.次に、クラスの方法は、実際には__get__()のみを実装した非資料記述器であるため、インスタンスcfooという方法と属性が同時に定義されている場合、c.fooは方法ではなく属性にアクセスする.
クラスの場合、type.__getattribute__()C.xC.__dict__['x'].__get__(None, C)に変換する.
覚えておきたいことはいくつかあります.
  • ディスクリプタは、__getattribute__()メソッドによって呼び出される
  • .
    したがって、
  • は、リロード__getattribute__()がディスクリプタの自動呼び出しを妨げる可能性がある
  • である.
  • __getattribute__()は、objectから継承する新式クラスのうち
  • のみに存在する.
  • object.__getattribute__()type.__getattribute__()__get__()に対する呼び出しが異なる
  • .
  • 資料記述器は、常に実例辞書をカバーする、すなわち、資料記述器具には最高優先度
  • がある.
  • 非資料記述器は、実例辞書によって覆われる可能性がある、すなわち、非資料記述器は、最低優先度
  • を有する.
    非資料記述器とクラスメソッド
    Pythonのオブジェクト向けの特徴は,関数ベースの環境に基づいている.Pythonは非資料記述器で両者をシームレスに結合した.
    メソッドと一般関数の唯一の違いは、一般メソッドの最初のパラメータが現在のインスタンス、すなわち通常selfと命名された変数を参照することである.
    Pythonの関数は、__get__()を実現した非資料記述器と考えられ、Pythonで記述すると、
    class Function(object):
        . . .
        def __get__(self, obj, objtype=None):
            "Simulate func_descr_get() in Objects/funcobject.c"
            return types.MethodType(self, obj, objtype)
    

    関数が属性としてアクセスされると、非資料記述器は関数を方法に変え、インスタンス呼び出しobj.f(*args)f(obj, *args)に変換し、クラス呼び出しklass.f(*args)f(*args)に変換する.
    バインドと変換の詳細については、次の表を参照してください.
    へんかん
    オブジェクトから呼び出す
    クラスから呼び出す
    関数#カンスウ#
    f(obj, *args)
    f(*args)
    スタティツクメソッド
    f(*args)
    f(*args)
    クラスメソッド
    f(type(obj), *args)
    f(klass, *args)
    静的メソッドは特殊なメソッドであり、インスタンス化することなくクラスで直接呼び出されることができ、この場合、合法的なselfを提供することは当然できない.そのためには、staticmethod記述器が必要であり、その__get__()が返す関数はインスタンスパラメータを必要とせず、実際にはそのまま返すことができ、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
    

    クラスメソッドは、現在のインスタンスselfを必要としない別の特殊な方法であるが、現在のクラスklass(通常はclsとも書く)が必要であり、純粋な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
    

    参考資料
  • Descriptor HowTo Guideとその中国語翻訳