web-dev-qa-db-ja.com

__get__および__set__およびPython記述子について

私は試行 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. この例をどのように呼び出し/使用しますか?

278
Matt Bronson

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

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

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

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

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

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

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

126
li.davidm

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

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

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

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

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

これは非常に微妙です(そして、ここで新しい答えを書いている理由-私は同じことを疑問に思ってこの質問を見つけましたが、既存の答えをそれほど見つけませんでした)。

記述子はクラスで定義されますが、通常はインスタンスから呼び出されます。インスタンスから呼び出されると、instanceownerの両方が設定されます(また、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クラス内に置くプロパティである場合)できます...

95
andrew cooke

Pythonの記述子が何であり、何のために役立つかを理解しようとしています。

記述子は、次の特別なメソッドのいずれかを持つクラス属性(プロパティやメソッドなど)です。

  • __get__(メソッド/関数などの非データ記述子メソッド)
  • __set__(たとえば、プロパティインスタンスのデータ記述子メソッド)
  • __delete__(データ記述子メソッド)

これらの記述子オブジェクトは、他のオブジェクトクラス定義の属性として使用できます。 (つまり、クラスオブジェクトの__dict__に存在します。)

記述子オブジェクトを使用して、通常の式、割り当て、さらには削除のドット付きルックアップ(foo.descriptorなど)の結果をプログラムで管理できます。

関数/メソッド、バインドメソッド、propertyclassmethod、およびstaticmethodはすべて、これらの特別なメソッドを使用して、ドットルックアップによるアクセス方法を制御します。

データ記述子は、propertyのように、オブジェクトのより単純な状態に基づいて属性の遅延評価を可能にし、インスタンスが使用する場合よりも少ないメモリを使用できるようにします可能な各属性を事前に計算しました。

member_descriptor によって作成された別のデータ記述子__slots__は、より柔軟でスペースを消費する__dict__ではなく、クラスが可変のタプルのようなデータ構造にデータを格納できるようにすることで、メモリを節約できます。

通常、インスタンス、クラス、および静的メソッドである非データ記述子は、非データ記述子メソッド__get__から暗黙の最初の引数(通常、それぞれclsおよびselfという名前)を取得します。

Pythonのほとんどのユーザーは、簡単な使用法のみを学習する必要があり、記述子の実装をさらに学習または理解する必要はありません。

詳細:記述子とは?

記述子は、次のメソッド(__get____set__、または__delete__)のいずれかを持つオブジェクトであり、インスタンスの典型的な属性であるかのようにドット検索を介して使用することを目的としています。所有者オブジェクト、obj_instance、およびdescriptorオブジェクトの場合:

  • obj_instance.descriptor呼び出し
    descriptor.__get__(self, obj_instance, owner_class)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は、クラスに記述子オブジェクトのインスタンスが含まれるインスタンスです。 selfdescriptorのインスタンスです(おそらくobj_instanceのクラスに1つだけです)

これをコードで定義するために、オブジェクトは、その属性セットが必要な属性のいずれかと交差する場合、記述子です。

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____delete__があります。
ANon-Data-Descriptorには__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はデータ記述子です。

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

点線のルックアップ順序

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

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

記述子により、Temperatureのこのクラス属性に対して常にfloatが使用され、delを使用して属性を削除できないことが保証されます。

>>> t1 = Temperature()
>>> del t1.celsius
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: __delete__

そうでない場合、記述子は所有者クラスと所有者のインスタンスを無視し、代わりに記述子に状態を保存します。単純なクラス属性を使用して、すべてのインスタンス間で状態を簡単に共有できます(常にクラスにフロートとして設定し、決して削除しないか、コードのユーザーに快適である限り)。

class Temperature(object):
    celsius = 0.0

これにより、例とまったく同じ動作が得られます(以下の質問3への応答を参照)が、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)
  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番目の提案(プロパティデコレータ)に進んでください。

55
Aaron Hall

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

Fluent Python Buciano Ramalhoに触発

このようなクラスがあるイメージング

class LineItem:
     price = 10.9
     weight = 2.1
     def __init__(self, name, price, weight):
          self.name = name
          self.price = price
          self.weight = weight

item = LineItem("Apple", 2.9, 2.1)
item.price = -0.9  # it's price is negative, you need to refund to your customer even you delivered the Apple :(
item.weight = -0.8 # negative weight, it doesn't make sense

負の数を割り当てることを避けるために、重量と価格を検証する必要があります。

class Quantity(object):
    __index = 0

    def __init__(self):
        self.__index = self.__class__.__index
        self._storage_name = "quantity#{}".format(self.__index)
        self.__class__.__index += 1

    def __set__(self, instance, value):
        if value > 0:
            setattr(instance, self._storage_name, value)
        else:
           raise ValueError('value should >0')

   def __get__(self, instance, owner):
        return getattr(instance, self._storage_name)

次に、クラスLineItemを次のように定義します。

class LineItem(object):
     weight = Quantity()
     price = Quantity()

     def __init__(self, name, weight, price):
         self.name = name
         self.weight = weight
         self.price = price

さらに、Quantityクラスを拡張して、より一般的な検証を行うことができます

6
wllbll

記述子の詳細に入る前に、Pythonの属性ルックアップがどのように機能するかを知ることが重要かもしれません。これは、クラスにメタクラスがなく、__getattribute__のデフォルト実装を使用することを前提としています(両方とも動作を「カスタマイズ」するために使用できます)。

この場合の属性ルックアップ(Python 3.xまたはPython 2.xの新しいスタイルのクラス)の最良の例は nderstanding Pythonメタクラス(ionelのコードログ) 。イメージは、「カスタマイズ不可能な属性ルックアップ」の代わりとして:を使用します。

これは、foobarinstanceの属性Classのルックアップを表します。

enter image description here

ここでは2つの条件が重要です。

  • instanceのクラスに属性名のエントリがあり、__get__および__set__がある場合。
  • instanceに属性名のnoエントリがあるが、クラスに1つあり、__get__がある場合。

それが記述子の出番です。

  • __get____set__の両方を持つデータ記述子
  • __get__のみを持つ非データ記述子

どちらの場合も、戻り値は、インスタンスを最初の引数として、クラスを2番目の引数として呼び出される__get__を経由します。

ルックアップは、クラス属性のルックアップではさらに複雑です(たとえば、 上記のブログのクラス属性のルックアップ )。

特定の質問に移りましょう:

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

ほとんどの場合、記述子クラスを記述する必要はありません!ただし、おそらく非常に定期的なエンドユーザーです。例の関数。関数は記述子です。つまり、関数は、selfが暗黙的に最初の引数として渡されるメソッドとして使用できます。

def test_function(self):
    return self

class TestClass(object):
    def test_method(self):
        ...

インスタンスでtest_methodを検索すると、「バウンドメソッド」が返されます。

>>> instance = TestClass()
>>> instance.test_method
<bound method TestClass.test_method of <__main__.TestClass object at ...>>

同様に、__get__メソッドを手動で呼び出して関数をバインドすることもできます(実際にはお勧めしません。単に説明のためだけです):

>>> test_function.__get__(instance, TestClass)
<bound method test_function of <__main__.TestClass object at ...>>

これを「自己バインドメソッド」と呼ぶこともできます。

>>> test_function.__get__(instance, TestClass)()
<__main__.TestClass at ...>

引数を指定せず、関数はバインドしたインスタンスを返しました。

関数は、非データ記述子です!

データ記述子のいくつかの組み込み例は、propertyです。 gettersetter、およびdeleterを無視すると、property記述子は( Descriptor HowTo Guide "Properties" )から:

class Property(object):
    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)

これはデータ記述子であるため、propertyの「名前」を検索するたびに呼び出され、@property@name.setter、および@name.deleter(存在する場合)で装飾された関数に単純に委任します。

標準ライブラリには、staticmethodclassmethodなどの他の記述子がいくつかあります。

記述子のポイントは簡単です(ほとんど必要ありませんが):属性アクセスのための抽象的な共通コード。 propertyはインスタンス変数アクセスの抽象化、functionはメソッドの抽象化を提供し、staticmethodはインスタンスアクセスを必要としないメソッドの抽象化を提供し、classmethodはインスタンスアクセスではなくクラスアクセスを必要とするメソッドの抽象化を提供します(これは少し簡略化されています)。

別の例は、 クラスプロパティ です。

1つの楽しい例(Python 3.6の__set_name__を使用)は、特定のタイプのみを許可するプロパティにすることもできます。

class TypedProperty(object):
    __slots__ = ('_name', '_type')
    def __init__(self, typ):
        self._type = typ

    def __get__(self, instance, klass=None):
        if instance is None:
            return self
        return instance.__dict__[self._name]

    def __set__(self, instance, value):
        if not isinstance(value, self._type):
            raise TypeError(f"Expected class {self._type}, got {type(value)}")
        instance.__dict__[self._name] = value

    def __delete__(self, instance):
        del instance.__dict__[self._name]

    def __set_name__(self, klass, name):
        self._name = name

次に、クラスで記述子を使用できます。

class Test(object):
    int_prop = TypedProperty(int)

少し遊んでみてください:

>>> t = Test()
>>> t.int_prop = 10
>>> t.int_prop
10

>>> t.int_prop = 20.0
TypeError: Expected class <class 'int'>, got <class 'float'>

または「遅延プロパティ」:

class LazyProperty(object):
    __slots__ = ('_fget', '_name')
    def __init__(self, fget):
        self._fget = fget

    def __get__(self, instance, klass=None):
        if instance is None:
            return self
        try:
            return instance.__dict__[self._name]
        except KeyError:
            value = self._fget(instance)
            instance.__dict__[self._name] = value
            return value

    def __set_name__(self, klass, name):
        self._name = name

class Test(object):
    @LazyProperty
    def lazy(self):
        print('calculating')
        return 10

>>> t = Test()
>>> t.lazy
calculating
10
>>> t.lazy
10

これらは、ロジックを共通の記述子に移動することが理にかなっている場合ですが、他の手段でそれらを解決することもできます(ただし、いくつかのコードを繰り返して).

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

属性の検索方法によって異なります。インスタンスの属性を検索する場合:

  • 2番目の引数は、属性を検索するインスタンスです
  • 3番目の引数はインスタンスのクラスです

クラスの属性を検索する場合(記述子がクラスで定義されていると仮定):

  • 2番目の引数はNoneです
  • 3番目の引数は、属性を検索するクラスです

したがって、クラスレベルのルックアップを行うときに動作をカスタマイズする場合は、基本的に3番目の引数が必要です(instanceNoneであるため)。

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

あなたの例は基本的に、floatに変換できる値のみを許可し、クラスのすべてのインスタンス間(およびクラス上)で共有されるプロパティです。ただし、クラスの「読み取り」アクセスのみ使用できます。インスタンス):

>>> t1 = Temperature()
>>> t2 = Temperature()

>>> t1.celsius = 20   # setting it on one instance
>>> t2.celsius        # looking it up on another instance
20.0

>>> Temperature.celsius  # looking it up on the class
20.0

そのため、記述子は一般に2番目の引数(instance)を使用して値を保存し、共有を避けます。ただし、インスタンス間で値を共有することが必要な場合もあります(現時点ではシナリオを考えることはできませんが)。ただし、温度クラスの摂氏温度プロパティについては、純粋にアカデミックな演習を除いて、事実上意味がありません。

5
MSeifert

https://docs.python.org/3/howto/descriptor.html#properties が表示されます

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__)
0
Yonks Somarl

Andrew Cookeの答えからコードを(提案されたように小さな変更を加えて)試しました。 (python 2.7を実行しています)。

コード:

#!/usr/bin/env python
class Celsius:
    def __get__(self, instance, owner): return 9 * (instance.Fahrenheit + 32) / 5.0
    def __set__(self, instance, value): instance.Fahrenheit = 32 + 5 * value / 9.0

class Temperature:
    def __init__(self, initial_f): self.Fahrenheit = initial_f
    celsius = Celsius()

if __== "__main__":

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

結果:

C:\Users\gkuhn\Desktop>python test2.py
<__main__.Celsius instance at 0x02E95A80>
212

3より前のPythonを使用して、古いスタイルクラスではgetマジックが機能しないため、記述子が正しく機能するオブジェクトのサブクラスを作成してください。

0
Gregory Kuhn