368

Pythonの記述子とは何か、そしてそれらが何に役立つのかを理解しようとしています。私はそれらがどのように機能するかを理解していますが、ここに私の疑問があります。次のコードを検討してください。

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()
  1. 記述子クラスが必要なのはなぜですか?

  2. instanceこことは何ownerですか?(で__get__)。これらのパラメータの目的は何ですか?

  3. この例をどのように呼び出す/使用しますか?

4

8 に答える 8

167

記述子は、Python のproperty型がどのように実装されるかです。記述子は単に 、 などを実装し__get____set__その定義で別のクラスに追加されます (上記の Temperature クラスで行ったように)。例えば:

temp=Temperature()
temp.celsius #calls celsius.__get__

記述子を割り当てたプロパティ (celsius上記の例) にアクセスすると、適切な記述子メソッドが呼び出されます。

instancein__get__はクラスのインスタンスです(上記のように、__get__を受け取りますがtempownerは記述子を持つクラスです(したがって、 になりますTemperature)。

記述子クラスを使用して、それを駆動するロジックをカプセル化する必要があります。そうすれば、ディスクリプタが高価な操作をキャッシュするために使用される場合 (たとえば)、そのクラスではなく、それ自体に値を格納できます。

記述子に関する記事は、ここにあります。

編集: jchl がコメントで指摘したように、単に試してみるとTemperature.celsiusinstanceになりますNone

于 2010-09-26T17:08:18.473 に答える
138

なぜ記述子クラスが必要なのですか?

これにより、属性の動作をさらに制御できます。たとえば、Java のゲッターとセッターに慣れている場合、それは Python のやり方です。利点の 1 つは、ユーザーには属性のように見えることです (構文に変更はありません)。したがって、通常の属性から始めて、何か特別なことをする必要がある場合は、記述子に切り替えることができます。

属性は単なる変更可能な値です。記述子を使用すると、値の読み取りまたは設定 (または削除) 時に任意のコードを実行できます。したがって、これを使用して属性をデータベースのフィールドにマップすることを想像できます。たとえば、一種の ORM です。

別の用途として、例外をスローして新しい値の受け入れを拒否することも考えられ__set__ます。つまり、「属性」を読み取り専用にすることです。

instanceこことは何ownerですか?(中__get__)。これらのパラメータの目的は何ですか?

これはかなり微妙です(そして、ここに新しい回答を書いている理由-同じことを疑問に思っているときにこの質問を見つけましたが、既存の回答はそれほど素晴らしいとは思いませんでした)。

記述子はクラスで定義されますが、通常はインスタンスから呼び出されます。インスタンスから呼び出された場合、instanceとの両方ownerが設定されます (そして、そこownerから解決できるinstanceので、ちょっと無意味に思えます)。しかし、クラスから呼び出されると、owner設定されるだけです。これが存在する理由です。

これは__get__、クラスで呼び出すことができる唯一のものであるため、必要です。クラス値を設定すると、記述子自体が設定されます。削除についても同様です。そのため、ownerそこには必要ありません。

この例をどのように呼び出し/使用しますか?

さて、同様のクラスを使用したクールなトリックを次に示します。

class Celsius:

    def __get__(self, instance, owner):
        return 5 * (instance.fahrenheit - 32) / 9

    def __set__(self, instance, value):
        instance.fahrenheit = 32 + 9 * value / 5


class Temperature:

    celsius = Celsius()

    def __init__(self, initial_f):
        self.fahrenheit = initial_f


t = Temperature(212)
print(t.celsius)
t.celsius = 0
print(t.fahrenheit)

(私は Python 3 を使用しています。Python 2 の場合、これらの区分が/ 5.0と であることを確認する必要があります/ 9.0)。それは与える:

100.0
32.0

現在、Pythonで同じ効果を達成するための間違いなくより良い方法が他にもあります(たとえば、摂氏がプロパティである場合、これは同じ基本メカニズムですが、すべてのソースをTemperatureクラス内に配置します)が、何ができるかを示しています...

于 2013-08-04T00:41:58.917 に答える
87

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
  • 機能全般

非データ記述子

classmethodstaticmethodが非データ記述子であることがわかります。

>>> 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

ただし、propertyData-Descriptor は次のとおりです。

>>> is_data_descriptor(property)
True
>>> has_descriptor_attrs(property)
set(['__set__', '__get__', '__delete__'])

点線の検索順序

これらは、ドット ルックアップのルックアップ順序に影響するため、重要な違いです。

obj_instance.attribute
  1. 最初に、上記は属性がインスタンスのクラスのデータ記述子であるかどうかを確認します。
  2. そうでない場合は、属性が の にあるかどうかを確認しobj_instanceます__dict__
  3. 最終的に非データ記述子にフォールバックします。

この検索順序の結果は、関数/メソッドのような非データ記述子がインスタンスによってオーバーライドされる可能性があることです。

要約と次のステップ

記述子は__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()
  1. なぜ記述子クラスが必要なのですか?

記述子は、このクラス属性の 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)
  1. ここでのインスタンスと所有者とは何ですか? (getで)。これらのパラメータの目的は何ですか?

instance記述子を呼び出している所有者のインスタンスです。所有者は、データ ポイントへのアクセスを管理するために記述子オブジェクトが使用されるクラスです。よりわかりやすい変数名については、この回答の最初の段落の横にある記述子を定義する特別なメソッドの説明を参照してください。

  1. この例をどのように呼び出し/使用しますか?

ここにデモンストレーションがあります:

>>> 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 番目の提案 (プロパティ デコレータ) に進んでいただければ幸いです。

于 2016-01-01T07:22:20.007 に答える
1

わかりやすい(例あり)__get__ & __set__ & __call__ 授業での説明、 とはOwner, Instance

飛び込む前に確認しておくべきいくつかのポイント:

  1. __get__ __set____name__内部属性、つまり(クラス/所有者クラスの名前)、変数などを機能/保存するためにクラスの記述子と呼ばれます__dict__。所有者とは何かについては後で説明します
  2. 記述子は、より一般的にはデザイン パタラーで使用されます。たとえば、デコレーター (物事を抽象化するため) などです。冗長性を減らし、読みやすくするために、ソフトウェア アーキテクチャの設計でより頻繁に使用されると考えることができます (皮肉なことに思えます)。したがって、SOLID と DRY の原則を順守します。
  3. SOLID と DRY の原則に従わなければならないソフトウェアを設計していない場合、それらはおそらく必要ありませんが、それらを理解しておくことは常に賢明です。

1. 次のコードを検討してください。

class Method:
    def __init__(self, name):
        self.name = name
    def __call__(self, instance, arg1, arg2):
        print(f"{self.name}: {instance} called with {arg1} and {arg2}")


class MyClass:
    method = Method("Internal call")

instance = MyClass()


instance.method("first", "second")

# Prints:TypeError: __call__() missing 1 required positional argument: 'arg2'

そのため、instance.method("first", "second")が呼び出されると__call__、 Method クラスから method が呼び出され (call method はクラス オブジェクトを関数のように呼び出し可能にします - クラス インスタンスが呼び出されるたびにインスタンス化され__call__ます)、次の引数が割り当てられます: instance: "first", arg1: "second"、最後の arg2 は省略されます、これはエラーを出力します:TypeError: __call__() missing 1 required positional argument: 'arg2'

2.どうやって解決するの?

  • 最初の引数 (インスタンス、arg1、arg2) として__call__取るので、何の?instanceinstance

  • Instance記述子クラス (メソッド) を呼び出しているメイン クラス (MyClass) のインスタンスです。それで、instance = MyClass()instanceownerですか?ディスクリプタ クラスを保持するクラス -ですが、ディスクリプタ クラスにはそれを . として認識するMyClassメソッドがありません。そこでメソッドが必要になります。以下のコードをもう一度考えてみましょう。(Method)instance__get__



from types import MethodType
class Method:
    def __init__(self, name):
        self.name = name
    def __call__(self, instance, arg1, arg2):
        print(f"{self.name}: {instance} called with {arg1} and {arg2}")
    def __set__(self, instance, value):
        self.value = value
        instance.__dict__["method"] = value
    def __get__(self, instance, owner):
        if instance is None:
            return self
        print (instance, owner)
        return MethodType(self, instance)   


class MyClass:
    method = Method("Internal call")

instance = MyClass()


instance.method("first", "second") 
# Prints: Internal call: <__main__.MyClass object at 0x7fb7dd989690> called with first and second

ドキュメントによると、今のところ設定を忘れてください:

__get__「所有者クラス (クラス属性アクセス) またはそのクラスのインスタンス (インスタンス属性アクセス) の属性を取得するために呼び出されます。」

もしあなたがそうするなら:instance.method.__get__(instance)

Prints:<__main__.MyClass object at 0x7fb7dd9eab90> <class '__main__.MyClass'>

これはインスタンスを意味します: そのオブジェクトは、それ自体MyClassでありinstance 、それ自体ですOwnerMyClass

3.__set__説明:

__set__クラスオブジェクトに値を設定するために使用され__dict__ます(コマンドラインを使用するとしましょう)。setの内部値を設定するコマンドは次のとおりinstance.descriptor = 'value'ですmethod

  • (コードでは、記述子のオブジェクトをinstance.__dict__["method"] = value更新するだけです)__dict__

  • そうしてください:がメソッドに設定されているinstance.method = 'value'かどうかを確認するために、記述子のオブジェクトにアクセスできます。実行: 印刷:value = 'value'__set____dict__methodinstance.method.__dict__{'_name': 'Internal call', 'value': 'value'}

  • または、prints を使用して値を確認することも でき__dict__ます 。vars(instance.method){'name': 'Internal call', 'value': 'value'}

于 2021-11-19T11:17:35.063 に答える