了解__get__ 和 __set__python描述符

Understanding __get__ and __set__ and Python descriptors

我试图理解python的描述符是什么,它们对什么有用。然而,我却在失败。我知道它们是如何工作的,但我的疑问是。请考虑以下代码:

1
2
3
4
5
6
7
8
9
10
11
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()
  • 为什么我需要描述符类?

  • 什么是instanceowner?(在__get__中)。这些参数的用途是什么?

  • 我如何调用/使用这个例子?


  • 描述符是如何实现Python的property类型的。描述符只实现__get____set__等,然后添加到它的定义中的另一个类(正如上面对temperature类所做的那样)。例如:

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

    访问您将描述符分配给的属性(在上面的示例中为celsius)将调用适当的描述符方法。

    __get__中的instance是类的实例(如上所述,__get__将接收temp,而owner是带有描述符的类(因此它将是Temperature)。

    您需要使用描述符类来封装驱动它的逻辑。这样,如果描述符用于缓存一些昂贵的操作(例如),那么它可以将值存储在自身而不是类上。

    这里有一篇关于描述符的文章。

    编辑:正如JCHL在评论中指出的那样,如果你只是简单地尝试Temperature.celsiusinstance将是None


    Why do I need the descriptor class?

    它为您提供了对属性工作方式的额外控制。例如,如果你已经习惯了Java中的吸气剂和定位器,那么Python就是这样做的。一个优点是它在用户看来就像一个属性(语法没有变化)。因此,您可以从一个普通属性开始,然后,当您需要做一些花哨的事情时,切换到一个描述符。

    属性只是一个可变值。描述符允许您在读取或设置(或删除)值时执行任意代码。所以您可以想象使用它将属性映射到数据库中的字段,例如,一种ORM。

    另一种用法可能是通过在__set__中抛出异常来拒绝接受新的值——实际上使"属性"为只读。

    What is instance and owner here? (in __get__). What is the purpose of these parameters?

    这是非常微妙的(我在这里写一个新的答案的原因-我发现这个问题的同时还想知道同样的事情,但没有发现现有的答案那么好)。

    描述符是在类上定义的,但通常是从实例调用的。当从一个实例中调用时,instanceowner都被设置了(并且您可以从instance中算出owner,所以这似乎是毫无意义的)。但是当从类中调用时,只设置了owner–这就是它存在的原因。

    这只需要用于__get__,因为它是类上唯一可以调用的。如果设置类值,则设置描述符本身。同样的删除。这就是为什么在那里不需要owner

    How would I call/use this example?

    好吧,这里有一个很酷的技巧,使用类似的类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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。给出:

    1
    2
    100.0
    32.0

    现在还有其他更好的方法可以在python中实现相同的效果(例如,如果Celsius是一个属性,这是相同的基本机制,但将所有源都放在temperature类中),但这表明可以做些什么……


    I am trying to understand what Python's descriptors are and what they can be useful for.

    描述符是具有以下任何特殊方法的类属性(如属性或方法):好的。

    • __get__(非数据描述符方法,例如在方法/函数上)
    • __set__(数据描述符方法,例如在属性实例上)
    • __delete__(数据描述符法)

    这些描述符对象可以用作其他对象类定义的属性。(也就是说,它们生活在类对象的__dict__中。)好的。

    描述符对象可用于以编程方式管理普通表达式、赋值甚至删除中的点式查找(如foo.descriptor)的结果。好的。

    函数/方法、绑定方法、propertyclassmethodstaticmethod都使用这些特殊方法来控制如何通过点式查找访问它们。好的。

    数据描述符(如property)允许基于对象的简单状态对属性进行延迟评估,允许实例使用比预计算每个可能属性更少的内存。好的。

    另一个数据描述符是__slots__创建的member_descriptor,它允许类将数据存储在类似于可变元组的数据结构中,而不是更灵活但占用空间的__dict__,从而节省内存。好的。

    非数据描述符,通常是实例、类和静态方法,从它们的非数据描述符方法__get__中得到它们的隐式第一参数(通常分别称为clsself。好的。

    大多数Python用户只需要学习简单的用法,而不需要进一步学习或理解描述符的实现。好的。深度:描述符是什么?

    描述符是具有以下任何方法(__get____set____delete__的对象,旨在通过点式查找使用,就好像它是实例的典型属性一样。对于所有者对象,obj_instancedescriptor对象:好的。

    • 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是类中包含描述符对象实例的实例。self是描述符的实例(可能只是obj_instance类的一个描述符)。好的。

    要用代码定义这个,如果一个对象的属性集与任何所需的属性相交,则该对象就是一个描述符:好的。

    1
    2
    3
    4
    5
    6
    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))

    数据描述符具有__set__和/或__delete__。非数据描述符既没有__set__也没有__delete__。好的。

    1
    2
    3
    4
    5
    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是非数据描述符:好的。

    1
    2
    3
    4
    >>> is_descriptor(classmethod), is_data_descriptor(classmethod)
    (True, False)
    >>> is_descriptor(staticmethod), is_data_descriptor(staticmethod)
    (True, False)

    两者都只有__get__方法:好的。

    1
    2
    >>> has_descriptor_attrs(classmethod), has_descriptor_attrs(staticmethod)
    (set(['__get__']), set(['__get__']))

    请注意,所有函数也是非数据描述符:好的。

    1
    2
    3
    4
    >>> def foo(): pass
    ...
    >>> is_descriptor(foo), is_data_descriptor(foo)
    (True, False)

    数据描述符,property

    但是,property是一个数据描述符:好的。

    1
    2
    3
    4
    >>> is_data_descriptor(property)
    True
    >>> has_descriptor_attrs(property)
    set(['__set__', '__get__', '__delete__'])

    点式查找顺序

    这些是重要的区别,因为它们会影响点式查找的查找顺序。好的。

    1
    obj_instance.attribute
  • 首先,上面查看该属性是否是实例类上的数据描述符,
  • 如果不是,它会查看属性是否在obj_instance__dict__中,然后
  • 它最终返回到非数据描述符。
  • 这种查找顺序的结果是,函数/方法等非数据描述符可以被实例重写。好的。回顾和下一步

    我们已经了解到描述符是任何__get____set____delete__的对象。这些描述符对象可以用作其他对象类定义的属性。现在,我们将以您的代码为例,看看它们是如何使用的。好的。从问题中分析代码

    下面是您的代码,后面是您的问题和答案:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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()
  • Why do I need the descriptor class?
  • 您的描述符确保您总是有一个针对Temperature的类属性的浮点,并且您不能使用del删除该属性:好的。

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

    否则,描述符将忽略所有者类和所有者实例,而将状态存储在描述符中。您可以使用一个简单的类属性轻松地在所有实例之间共享状态(只要您总是将其设置为类的浮点值,并且从不删除它,或者对代码的用户这样做感到满意):好的。

    1
    2
    class Temperature(object):
        celsius = 0.0

    这将使您获得与示例完全相同的行为(请参见下面问题3的回答),但使用了内置的pythons(property),并被认为更惯用:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    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)
  • What is instance and owner here? (in get). What is the purpose of these parameters?
  • instance是调用描述符的所有者的实例。所有者是描述符对象用于管理对数据点的访问的类。有关更具描述性的变量名,请参阅此答案第一段旁边定义描述符的特殊方法的说明。好的。

  • How would I call/use this example?
  • 下面是一个演示:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    >>> t1 = Temperature()
    >>> t1.celsius
    0.0
    >>> t1.celsius = 1
    >>>
    >>> t1.celsius
    1.0
    >>> t2 = Temperature()
    >>> t2.celsius
    1.0

    不能删除属性:好的。

    1
    2
    3
    4
    >>> del t2.celsius
    Traceback (most recent call last):
      File"<stdin>", line 1, in <module>
    AttributeError: __delete__

    您不能分配一个不能转换为浮点的变量:好的。

    1
    2
    3
    4
    5
    >>> 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修饰器,它在hood下使用相同的描述符,但将行为引入owner类的实现中(同样,如上所定义):好的。

    1
    2
    3
    4
    5
    6
    7
    8
    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
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    >>> 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

    结论

    我们已经介绍了定义描述符的属性、数据描述符和非数据描述符之间的区别、使用它们的内置对象以及关于使用的特定问题。好的。

    那么,你将如何使用这个问题的例子呢?我希望你不会这样做。我希望你从我的第一个建议(一个简单的类属性)开始,如果你觉得有必要的话,继续到第二个建议(属性修饰器)。好的。好啊。


    Why do I need the descriptor class?

    灵感来源于布西亚诺·拉玛尔霍创作的流畅的Python。

    想象你有这样的班级

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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

    我们应该验证权重和价格,避免给它们分配一个负数,如果我们使用描述符作为代理,我们可以写更少的代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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:

    1
    2
    3
    4
    5
    6
    7
    8
    class LineItem(object):
         weight = Quantity()
         price = Quantity()

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

    我们可以扩展数量类来进行更常见的验证


    在详细介绍描述符之前,了解Python中的属性查找是如何工作的可能很重要。这假定类没有元类,并且它使用EDOCX1的默认实现(两者都可以用于"自定义"行为)。好的。

    属性查找(在python 3.x或python 2.x中的新样式类)的最佳说明是理解python元类(ionel的代码日志)。图像使用:代替"不可自定义的属性查找"。好的。

    这表示在Classinstance上查找属性foobar:好的。

    enter image description here好的。

    这里有两个重要条件:好的。

    • 如果instance的类有一个属性名的条目,并且它有__get____set__
    • 如果instance没有属性名的条目,但类有一个条目,并且它有__get__

    这就是描述符的由来:好的。

    • 同时具有__get____set__的数据描述符。
    • 只有__get__的非数据描述符。

    在这两种情况下,返回值都将通过调用__get__,实例作为第一个参数,类作为第二个参数。好的。

    对于类属性查找,查找更加复杂(请参见例如类属性查找(在上面提到的博客中))。好的。

    让我们转到您的具体问题:好的。

    Why do I need the descriptor class?

    Ok.

    在大多数情况下,您不需要编写描述符类!不过,您可能是非常普通的最终用户。例如函数。函数是描述符,这就是如何将函数用作方法,并将self隐式地作为第一个参数传递。好的。

    1
    2
    3
    4
    5
    6
    def test_function(self):
        return self

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

    如果在实例上查找test_method,您将得到一个"绑定方法":好的。

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

    同样,您也可以通过手动调用函数的__get__方法来绑定函数(实际上不建议这样做,只是为了说明目的):好的。

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

    甚至可以称之为"自绑定方法":好的。

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

    注意,我没有提供任何参数,并且函数返回了我绑定的实例!好的。

    函数是非数据描述符!好的。

    数据描述符的一些内置示例是property。忽略gettersetterdeleterproperty描述符是(从描述符如何引导"属性"):好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    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为需要类访问而不是实例访问的方法提供抽象(这有点简化)。好的。

    另一个例子是类属性。好的。

    一个有趣的例子(使用Python3.6中的__set_name__)也可以是只允许特定类型的属性:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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

    然后可以在类中使用描述符:好的。

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

    和它一起玩:好的。

    1
    2
    3
    4
    5
    6
    7
    >>> t = Test()
    >>> t.int_prop = 10
    >>> t.int_prop
    10

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

    或"懒惰的财产":好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    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

    在这些情况下,将逻辑移动到一个公共描述符中可能是有意义的,但是也可以用其他方法来解决它们(但可能需要重复一些代码)。好的。

    What is instance and owner here? (in __get__). What is the purpose of these parameters?

    Ok.

    这取决于如何查找属性。如果查找实例上的属性,则:好的。

    • 第二个参数是在其上查找属性的实例
    • 第三个参数是实例的类

    如果查找类上的属性(假设描述符是在类上定义的):好的。

    • 第二个论点是None
    • 第三个参数是查找属性的类

    因此,基本上,如果您想在进行类级查找时自定义行为,第三个参数是必要的(因为instanceNone)。好的。

    How would I call/use this example?

    Ok.

    您的示例基本上是一个属性,它只允许转换为float的值,并且在类的所有实例之间(以及在类上)共享这些值,尽管在类上只能使用"读取"访问,否则您将替换描述符实例:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    >>> 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

    这就是为什么描述符通常使用第二个参数(instance)来存储值以避免共享它。然而,在某些情况下,可能需要在实例之间共享一个值(尽管目前我无法想到场景)。然而,对于温度等级的摄氏度性质来说,这几乎没有意义…除了纯粹的学术练习。好的。好啊。


    您将看到https://docs.python.org/3/howto/descriptor.html属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    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__)


    我尝试了安德鲁·库克的答案中的代码(建议做一些小改动)。(我运行的是python 2.7)。

    代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #!/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 __name__ =="__main__":

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

    结果:

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

    对于3之前的python,请确保从对象子类,这将使描述符正确工作,因为get magic不适用于旧样式类。