短いレビュー
この記事では、記述子とは何か、記述子プロトコルについて説明し、記述子の呼び出し方法を示します。 独自の作成について説明し、関数、プロパティ、静的メソッド、クラスメソッドなど、いくつかの組み込み記述子を調べます。 単純なアプリケーションを使用して、それぞれがどのように機能するかを示し、純粋な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.d
は
obj
辞書で
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
覚えておくべき重要な部分:
- 記述子は
__getattribute__
メソッドを使用して呼び出されます __getattribute__
オーバーライド__getattribute__
、自動記述子呼び出し__getattribute__
停止します__getattribute__
は、新しいスタイルのクラスとオブジェクト内でのみ使用可能ですobject.__getattribute__
とtype.__getattribute__
は、 __get__
異なる呼び出しを__get__
- データ記述子は常にオブジェクト変数よりも優先されます
- オブジェクト変数のためにデータ記述子が利点を失うことはありません
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の
マニュアルにあり
ます 。
上記の詳細は、記述子呼び出しアルゴリズムが、
object
、
type
および
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']
通訳者の結論は、関連するメソッドと接続されていないメソッドが2つの異なるタイプであることを示しています。 この方法で実装できたとしても、実際には、
Objects/classobject.c
の
PyMethod_Type
実装には、
im_self
フィールド
im_self
値
im_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(*引数) |
staticmethod | f(*引数) | 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