Python の記述子とは何か、そしてそれらが何に役立つかを理解しようとしています。
記述子は、インスタンス属性 (スロット、プロパティ、メソッドなど) を管理するクラス名前空間内のオブジェクトです。例えば:
class HasDescriptors:
__slots__ = 'a_slot' # creates a descriptor
def a_method(self): # creates a descriptor
"a regular method"
@staticmethod # creates a descriptor
def a_static_method():
"a static method"
@classmethod # creates a descriptor
def a_class_method(cls):
"a class method"
@property # creates a descriptor
def a_property(self):
"a property"
# even a regular function:
def a_function(some_obj_or_self): # creates a descriptor
"create a function suitable for monkey patching"
HasDescriptors.a_function = a_function # (but we usually don't do this)
皮肉なことに、記述子は、「記述子メソッド」として知られる次の特別なメソッドのいずれかを持つオブジェクトです。
__get__
: 非データ記述子メソッド (メソッド/関数など)
__set__
: プロパティ インスタンスやスロットなどのデータ記述子メソッド
__delete__
: プロパティまたはスロットで再び使用されるデータ記述子メソッド
これらの記述子オブジェクトは、他のオブジェクト クラスの名前空間の属性です。つまり__dict__
、クラス オブジェクトの に存在します。
foo.descriptor
記述子オブジェクトは、通常の式、代入、または削除でドット ルックアップ (例: ) の結果をプログラムで管理します。
関数/メソッド、バインドされたメソッド、、、property
およびclassmethod
すべてstaticmethod
は、これらの特別なメソッドを使用して、ドット ルックアップを介してアクセスする方法を制御します。
のようなデータ記述子property
は、オブジェクトのより単純な状態に基づいて属性の遅延評価を可能にし、可能性のある各属性を事前に計算した場合よりもインスタンスが使用するメモリを少なくすることができます。
によって作成された別のデータ記述子は、より柔軟ですがスペースを消費する ではなく、変更可能なタプルのようなデータ構造にクラスがデータを格納するようにすることで、メモリの節約 (および高速なルックアップ) を可能にします。member_descriptor
__slots__
__dict__
非データ記述子、インスタンス メソッドおよびクラス メソッドは、非データ記述子メソッドから暗黙的な最初の引数 (通常はそれぞれself
およびという名前) を取得します。これが、静的メソッドが暗黙的な最初の引数を持たないことを認識する方法です。cls
__get__
Python のほとんどのユーザーは、記述子の高レベルの使用方法を学ぶだけでよく、記述子の実装についてさらに学習したり理解したりする必要はありません。
しかし、ディスクリプタがどのように機能するかを理解することで、Python を使いこなす自信がつきます。
詳細: 記述子とは
__get__
記述子は、次のメソッド ( 、__set__
、または)のいずれかを持つオブジェクトで__delete__
あり、インスタンスの典型的な属性であるかのようにドット ルックアップを介して使用することを目的としています。obj_instance
オブジェクトを持つ所有者オブジェクトのdescriptor
場合:
obj_instance.descriptor
これは、すべてのメソッドと on
descriptor.__get__(self, obj_instance, owner_class)
aプロパティvalue
がどのように機能するかです。get
obj_instance.descriptor = value
これが
descriptor.__set__(self, obj_instance, value)
プロパティの動作の仕方です。None
setter
del obj_instance.descriptor
これが
descriptor.__delete__(self, obj_instance)
プロパティの動作の仕方です。None
deleter
obj_instance
記述子オブジェクトのインスタンスを含むクラスのインスタンスです。記述子self
のインスタンスです(おそらく のクラスの 1 つにすぎません) 。obj_instance
これをコードで定義するには、オブジェクトの属性のセットが必要な属性のいずれかと交差する場合、オブジェクトは記述子です。
def has_descriptor_attrs(obj):
return set(['__get__', '__set__', '__delete__']).intersection(dir(obj))
def is_descriptor(obj):
"""obj can be instance of descriptor or the descriptor class"""
return bool(has_descriptor_attrs(obj))
Data Descriptorには__set__
and/orがあり__delete__
ます。非データ記述子
には ももありません。__set__
__delete__
def has_data_descriptor_attrs(obj):
return set(['__set__', '__delete__']) & set(dir(obj))
def is_data_descriptor(obj):
return bool(has_data_descriptor_attrs(obj))
組み込み記述子オブジェクトの例:
classmethod
staticmethod
property
- 機能全般
非データ記述子
classmethod
とstaticmethod
が非データ記述子であることがわかります。
>>> is_descriptor(classmethod), is_data_descriptor(classmethod)
(True, False)
>>> is_descriptor(staticmethod), is_data_descriptor(staticmethod)
(True, False)
__get__
どちらも次の方法しかありません。
>>> has_descriptor_attrs(classmethod), has_descriptor_attrs(staticmethod)
(set(['__get__']), set(['__get__']))
すべての関数は非データ記述子でもあることに注意してください。
>>> def foo(): pass
...
>>> is_descriptor(foo), is_data_descriptor(foo)
(True, False)
データ記述子、property
ただし、property
Data-Descriptor は次のとおりです。
>>> is_data_descriptor(property)
True
>>> has_descriptor_attrs(property)
set(['__set__', '__get__', '__delete__'])
点線の検索順序
これらは、ドット ルックアップのルックアップ順序に影響するため、重要な違いです。
obj_instance.attribute
- 最初に、上記は属性がインスタンスのクラスのデータ記述子であるかどうかを確認します。
- そうでない場合は、属性が の にあるかどうかを確認し
obj_instance
ます__dict__
。
- 最終的に非データ記述子にフォールバックします。
この検索順序の結果は、関数/メソッドのような非データ記述子がインスタンスによってオーバーライドされる可能性があることです。
要約と次のステップ
記述子は__get__
、 、__set__
、またはのいずれかを持つオブジェクトであることを学びました__delete__
。これらの記述子オブジェクトは、他のオブジェクト クラス定義の属性として使用できます。次に、コードを例として使用して、それらがどのように使用されるかを見ていきます。
質問からのコードの分析
コードは次のとおりです。その後に、それぞれに対する質問と回答が続きます。
class Celsius(object):
def __init__(self, value=0.0):
self.value = float(value)
def __get__(self, instance, owner):
return self.value
def __set__(self, instance, value):
self.value = float(value)
class Temperature(object):
celsius = Celsius()
- なぜ記述子クラスが必要なのですか?
記述子は、このクラス属性の float を常に保持し、属性を削除するためにTemperature
使用できないことを保証します。del
>>> t1 = Temperature()
>>> del t1.celsius
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: __delete__
それ以外の場合、記述子は所有者クラスと所有者のインスタンスを無視し、代わりに状態を記述子に格納します。単純なクラス属性を使用して、すべてのインスタンス間で状態を簡単に共有できます (常にクラスに float として設定し、決して削除しないか、コードのユーザーがそうすることに慣れている場合)。
class Temperature(object):
celsius = 0.0
これにより、例とまったく同じ動作が得られます (以下の質問 3 への回答を参照) property
。
class Temperature(object):
_celsius = 0.0
@property
def celsius(self):
return type(self)._celsius
@celsius.setter
def celsius(self, value):
type(self)._celsius = float(value)
- ここでのインスタンスと所有者とは何ですか? (getで)。これらのパラメータの目的は何ですか?
instance
記述子を呼び出している所有者のインスタンスです。所有者は、データ ポイントへのアクセスを管理するために記述子オブジェクトが使用されるクラスです。よりわかりやすい変数名については、この回答の最初の段落の横にある記述子を定義する特別なメソッドの説明を参照してください。
- この例をどのように呼び出し/使用しますか?
ここにデモンストレーションがあります:
>>> t1 = Temperature()
>>> t1.celsius
0.0
>>> t1.celsius = 1
>>>
>>> t1.celsius
1.0
>>> t2 = Temperature()
>>> t2.celsius
1.0
次の属性は削除できません。
>>> del t2.celsius
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: __delete__
また、float に変換できない変数を代入することはできません。
>>> t1.celsius = '0x02'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 7, in __set__
ValueError: invalid literal for float(): 0x02
それ以外の場合、ここにあるのはすべてのインスタンスのグローバルな状態であり、任意のインスタンスに割り当てることによって管理されます。
ほとんどの経験豊富な Python プログラマーがこの結果を達成するために期待される方法は、property
デコレーターを使用することです。これは、内部で同じ記述子を使用しますが、所有者クラスの実装に動作をもたらします (これも上で定義したとおりです)。
class Temperature(object):
_celsius = 0.0
@property
def celsius(self):
return type(self)._celsius
@celsius.setter
def celsius(self, value):
type(self)._celsius = float(value)
これは、元のコードとまったく同じ予想される動作をします。
>>> t1 = Temperature()
>>> t2 = Temperature()
>>> t1.celsius
0.0
>>> t1.celsius = 1.0
>>> t2.celsius
1.0
>>> del t1.celsius
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't delete attribute
>>> t1.celsius = '0x02'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 8, in celsius
ValueError: invalid literal for float(): 0x02
結論
記述子を定義する属性、データ記述子と非データ記述子の違い、それらを使用する組み込みオブジェクト、および使用に関する特定の質問について説明しました。
繰り返しになりますが、質問の例をどのように使用しますか? 私はあなたがしないことを願っています。最初の提案 (単純なクラス属性) から始めて、必要に応じて 2 番目の提案 (プロパティ デコレータ) に進んでいただければ幸いです。