記述子ガイド

短いレビュー


この記事では、記述子とは何か、記述子プロトコルについて説明し、記述子の呼び出し方法を示します。 独自の作成について説明し、関数、プロパティ、静的メソッド、クラスメソッドなど、いくつかの組み込み記述子を調べます。 単純なアプリケーションを使用して、それぞれがどのように機能するかを示し、純粋なPythonコードを使用して記述子の内部実装と同等のものを提供します。

記述子がどのように機能するかを学習することで、より多くの作業ツールが開かれ、Pythonがどのように機能するかをよりよく理解し、そのデザインの優雅さを体験できます。


はじめにと定義


一般的に、記述子は、関連する動作( 英語バインディング動作)を持つオブジェクトの属性です。 アクセス動作が記述子プロトコルメソッドによってオーバーライドされるもの。 これらのメソッドは__get____set__および__delete__です。 これらのメソッドの少なくとも1つがオブジェクトに対して定義されている場合、それは記述子になります。

属性にアクセスするときの標準的な動作は、オブジェクトの辞書から属性を受け取り、設定し、削除することです。 たとえば、 axには次の属性検索チェーンがあります: a.__dict__['x'] 、次にtype(a).__dict__['x'] 、そしてtype(a)メタクラスを含まない基本クラス。 目的の値が、記述子を定義するメソッドが少なくとも1つあるオブジェクトである場合、Pythonは標準の検索チェーンを変更して、記述子メソッドの1つを呼び出すことができます。 これが発生する方法とタイミングは、オブジェクトに定義されている記述子メソッドによって異なります。 記述子は、新しいスタイルのオブジェクトまたはクラスに対してのみ呼び出されます( objectまたはtype継承する場合、クラスはクラスです)。

記述子は、広範囲の強力なプロトコルです。 それらは、プロパティ、メソッド、静的メソッド、クラスメソッド、およびsuper()呼び出しの背後にあるメカニズムです。 Python自体の内部に、バージョン2.2で導入された新しいスタイルのクラスが実装されています。 記述子により、基礎となるCコードの理解が簡単になり、Pythonプログラムに柔軟な新しいツールセットが提供されます。

記述子プロトコル


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

実際にはそれだけです。 これらのメソッドのいずれかを定義すると、オブジェクトは記述子と見なされ、属性として検索された場合に標準の動作をオーバーライドできます。

オブジェクトが__get____set__両方を一度に定義する場合、データ記述子と見なされます。 __get__のみが__get__いる記述子は、非データ記述子と呼ばれます。 それらはメソッドに使用されるためそう呼ばれますが、他の使用方法も可能です。

オブジェクトディクショナリに記述子と同じ名前のエントリが既にある場合、データ記述子と非データ記述子の検索動作の変更方法は異なります。 データ記述子が見つかると、オブジェクトの辞書からのエントリよりも早く呼び出されます。 データ記述子が同じ状況にある場合、オブジェクト辞書からのエントリがこの記述子より優先されます。

読み取り専用データ記述子を作成するには、 __get____set__両方を定義し、 __get__ __set__AttributeError例外をスローさせます。 この記述子をデータ記述子と見なすには、 __set__メソッドを定義して例外をスローするだけで十分です。

コール記述子


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

ただし、記述子を呼び出す最も一般的なバリアントは、属性にアクセスするときの自動呼び出しです。 たとえば、 obj.dobj辞書でdを探します。 d__get__メソッドを定義する場合、 d.__get__(obj)が呼び出されます。 呼び出しは、以下で説明する規則に従って行われます。

呼び出しの詳細は、 objとは異なります-オブジェクトまたはクラス。 いずれの場合でも、記述子は新しいスタイルのオブジェクトとクラスに対してのみ機能します。 クラスがobject子孫である場合、クラスは新しいスタイルクラスです。

オブジェクトの場合、アルゴリズムは、 object.__getattribute__を使用して実装されobject.__getattribute__ 。これは、 bxエントリをtype(b).__dict__['x'].__get__(b, type(b))変換します。 実装は、データ記述子がオブジェクト変数より優先され、オブジェクト変数が非データ記述子より優先され、 __getattr__メソッドが定義されている場合、最も低い優先度を持つ先行チェーンを通じて機能します。 完全なC実装は、 Objects/object.c PyObject_GenericGetAttr()にありObjects/object.c

クラスの場合、アルゴリズムは、 type.__getattribute__を使用して実装されtype.__getattribute__ 。これは、 BxエントリをB.__dict__['x'].__get__(None, B)変換します。 純粋なpythonでは、次のようになります。
 def __getattribute__(self, key): " type_getattro()  Objects/typeobject.c" v = object.__getattribute__(self, key) if hasattr(v, '__get__'): return v.__get__(None, self) return v 

覚えておくべき重要な部分:
super()呼び出した後に返されるオブジェクトには、 __getattribute__メソッドの独自の実装もあり、それを使用して記述子を呼び出します。 super(B, obj).m()呼び出しは、 obj.__class__.__mro__基本クラスA obj.__class__.__mro__ 、その直後にB続き、 A.__dict__['m'].__get__(obj, A)を返します。 これが記述子でない場合、 m変更されずに返されます。 m辞書にない場合、 object.__getattribute__介して検索に戻りobject.__getattribute__

注:Python 2.2では、 mがデータ記述子である場合にのみ、 super(B, obj).m()__get__ 。 Python 2.3では、古いスタイルのクラスを使用する場合を除いて、データ記述子も呼び出されません。 実装の詳細は、 Objects/typeobject.c super_getattro()にありObjects/typeobject.c 。純粋なpythonの同等物は、Guidoマニュアルにあります

上記の詳細は、記述子呼び出しアルゴリズムが、 objecttypeおよびsuper__getattribute__()メソッドを使用して実装されることを説明しています。 クラスは、 objectから継承する場合、または同様の機能を実装するメタクラスを持つ場合、このアルゴリズムを継承しobject 。 これにより、クラスは__getattribute__()をオーバーライドする場合、記述子呼び出しをオフにできます。

ハンドルの例


次のコードは、オブジェクトがデータ記述子であるクラスを作成します。オブジェクトが行うことは、 getまたはset呼び出しごとgetメッセージを出力することだけです。 __getattribute__オーバーライドすることは、属性ごとにこれを実行できる代替アプローチです。 ただし、個々の属性のみを観察する場合は、記述子を使用する方が簡単です。
 class RevealAccess(object): """ ,     ,     ,     . """ def __init__(self, initval=None, name='var'): self.val = initval self.name = name def __get__(self, obj, objtype): print '', self.name return self.val def __set__(self, obj, val): print '' , self.name self.val = val >>> class MyClass(object): x = RevealAccess(10, 'var "x"') y = 5 >>> m = MyClass() >>> mx  var "x" 10 >>> mx = 20  var "x" >>> mx  var "x" 20 >>> my 5 

このシンプルなプロトコルは、エキサイティングな可能性を提供します。 それらのいくつかは非常に頻繁に使用されるため、別々の機能に結合されました。 プロパティ、関連メソッドと非関連メソッド、静的メソッド、およびクラスメソッドはすべてこのプロトコルに基づいています。

プロパティ


property()呼び出しは、属性にアクセスしながら必要な関数を呼び出すデータ記述子を作成するのに十分です。 彼の署名は次のとおりです。
 property(fget=None, fset=None, fdel=None, doc=None) --> ,   

ドキュメントでは、 property()を使用して管理属性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, "  'x'.") 

純粋なpythonのpropertyに相当するので、 property()記述子プロトコルを使用してどのように実装されるかが明確になります。
 class Property(object): " PyProperty_Type()  Objects/descrobject.c" def __init__(self, fget=None, fset=None, fdel=None, doc=None): self.fget = fget self.fset = fset self.fdel = fdel self.__doc__ = doc def __get__(self, obj, objtype=None): if obj is None: return self if self.fget is None: raise AttributeError, " " return self.fget(obj) def __set__(self, obj, value): if self.fset is None: raise AttributeError, "   " self.fset(obj, value) def __delete__(self, obj): if self.fdel is None: raise AttributeError, "   " self.fdel(obj) 

property()の組み込み実装は、属性にアクセスするためのインターフェースがあり、いくつかの変更が発生したときに役立ちます。その結果、メソッドの介入が必要になりました。

たとえば、スプレッドシートクラスは、 Cell('b10').value介してセル値にアクセスできます。 プログラムのその後の変更の結果、セルにアクセスするたびにこの値が再計算されるようにする必要がありましたが、プログラマーは属性に直接アクセスするクライアントコードを変更したくありません。 この問題は、 property()を使用して作成されるデータ記述子を使用してvalue属性をラップすることで解決できます。
 class Cell(object): . . . def getvalue(self, obj): "     " self.recalc() return obj._value value = property(getvalue) 

関数とメソッド


Pythonでは、すべてのオブジェクト指向機能は機能的なアプローチを使用して実装されます。 これは、非データ記述子を使用して完全に非表示で実行されます。

クラス辞書はメソッドを関数として保存します。 クラスを定義するとき、メソッドは、関数を作成するための標準ツールであるdefおよびlambdaを使用して記述されます。 これらの関数と通常の関数の唯一の違いは、最初の引数がオブジェクトインスタンス用に予約されていることです。 この引数は通常selfと呼ばれますが、 thisまたは変数に名前を付けるために使用できる他の単語と呼ばれることもあります。

メソッド呼び出しをサポートするために、関数には__get__メソッドが含まれています。これにより、属性の検索時に自動的に非データ記述子になります。 関数は、この記述子が呼び出された内容に応じて、バインドされたメソッドまたは無関係のメソッドを返します。
 class Function(object): . . . def __get__(self, obj, objtype=None): " func_descr_get()  Objects/funcobject.c" return types.MethodType(self, obj, objtype) 

インタープリターを使用すると、関数記述子が実際にどのように機能するかを確認できます。
 >>> class D(object): def f(self, x): return x >>> d = D() >>> D.__dict__['f'] #     <function f at 0x00C45070> >>> Df #       <unbound method Df> >>> df #        <bound method Df of <__main__.D object at 0x00B18C90>> 

通訳者の結論は、関連するメソッドと接続されていないメソッドが2つの異なるタイプであることを示しています。 この方法で実装できたとしても、実際には、 Objects/classobject.cPyMethod_Type実装には、 im_selfフィールドim_selfim_selfか、 NULL (Cに相当する)値None )。

したがって、メソッド呼び出しの効果はim_selfフィールドに依存します。 インストールされている場合(つまり、メソッドが接続されている場合)、元の関数( im_funcフィールドに格納されている)が、最初の引数がオブジェクトインスタンスの値に設定されたim_funcで呼び出されます。 接続されていない場合、元の関数を変更せずにすべての引数が渡されます。 instancemethod_call()の実際のC実装instancemethod_call()いくつかの型チェックなどを含むためinstancemethod_call()もう少し複雑です。

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


関数をメソッドにバインドするためのさまざまなオプションの単純なメカニズムを提供するデータ記述子はありません。

もう一度繰り返します。 関数には__get__メソッドがあり、属性を検索し、記述子を自動的に呼び出すときにメソッドになります。 データ記述子はobj.f(*args)の呼び出しをf(obj, *args)呼び出しに変換せず、 obj.f(*args)の呼び出しはf(obj, *args) klass.f(*args)なります。

次の表に、バインドと最も一般的な2つのオプションを示します。
変換オブジェクトを介して呼び出されますクラスを介して呼び出されます
記述子機能f(obj、* args)f(*引数)
staticmethodf(*引数)f(*引数)
クラスメソッドf(タイプ(obj)、* args)f(klass、* args)

静的メソッドは、関数を変更せずに返します。 cfまたはCf呼び出しは、 object.__getattribute__(c, "f")またはobject.__getattribute__(C, "f")呼び出しと同等です。 その結果、関数はオブジェクトとクラスの両方から等しくアクセスできます。

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

たとえば、統計パッケージには実験データのクラスが含まれる場合があります。 このクラスは、データに依存する平均、期待値、中央値などの統計を計算するための通常のメソッドを提供します。 ただし、概念的に関連しているがデータに依存しない他の機能がある場合があります。 たとえば、 erf(x)は統計に必要な単純な変換関数ですが、このクラスの特定のデータセットには依存しません。 オブジェクトまたはクラスから呼び出すことができます: s.erf(1.5) --> 0.9332またはSample.erf(1.5) --> 0.9332

staticmethod()は関数を変更せずに返すため、この例は驚くことではありません。
 >>> class E(object): def f(x): print x f = staticmethod(f) >>> print Ef(3) 3 >>> print E().f(3) 3 

非データ記述子プロトコルを使用する場合、純粋なPythonではstaticmethod()は次のようになります。
 class StaticMethod(object): " PyStaticMethod_Type()  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 Ef(3) ('E', 3) >>> print E().f(3) ('E', 3) 

この動作は、関数が常にクラスへの参照を必要とし、データを必要としない場合に便利です。 classmethod()を使用する1つの方法は、代替クラスコンストラクターを作成することです。 Python 2.3では、クラスメソッドdict.fromkeys()はキーのリストから新しい辞書を作成します。 純粋なpythonでの同等物は次のようになります。
 class Dict: . . . def fromkeys(klass, iterable, value=None): " dict_fromkeys()  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} 

非データ記述子プロトコルを使用する場合、純粋なPythonではclassmethod()は次のようになります。
 class ClassMethod(object): " PyClassMethod_Type()  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 

Source: https://habr.com/ru/post/J122082/


All Articles