理解__get__和__set__以及Python描述符

Mat*_*son 287 python descriptor

试图了解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()
Run Code Online (Sandbox Code Playgroud)
  1. 为什么我需要描述符类?请使用此示例或您认为更好的示例进行说明.

  2. 什么是instanceowner这里?(in __get__).所以我的问题是,第三个参数的目的是什么?

  3. 我该怎么称呼/使用这个例子?

li.*_*idm 134

描述符是Python property类型的实现方式.描述符只是实现__get__,__set__等等,然后在其定义中添加到另一个类中(如上所述,使用Temperature类).例如:

temp=Temperature()
temp.celsius #calls celsius.__get__
Run Code Online (Sandbox Code Playgroud)

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

instancein __get__是类的实例(如上所述,__get__将接收temp,而owner具有描述符的类(所以它将是Temperature).

您需要使用描述符类来封装为其提供支持的逻辑.这样,如果描述符用于缓存一些昂贵的操作(例如),它可以将值存储在自身而不是其类.

有关描述符的文章可以在这里找到.

编辑:正如jchl在评论中指出的,如果你只是尝试Temperature.celsius,instance将会None.

  • @LemmaPrism `self` 是描述符实例,`instance` 是描述符所在类的实例(如果实例化)(`instance.__class__ 是所有者`)。 (5认同)
  • self和instance之间有什么区别? (3认同)
  • “instance”可以是任何类的实例,self 将是同一类的实例。 (2认同)

and*_*oke 99

为什么我需要描述符类?请使用此示例或您认为更好的示例进行说明.

它使您可以更好地控制属性的工作方式.例如,如果你习惯于java中的getter和setter,那么它就是python的做法.一个优点是它向用户看起来就像一个属性(语法没有变化).所以你可以从一个普通的属性开始,然后,当你需要做一些奇特的事情时,切换到一个描述符.

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

另一种用法可能是拒绝通过抛出异常来接受新值__set__- 有效地使"属性"只读.

什么是实例和所有者?(in instance).所以我的问题是,第三个参数的目的是什么?

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

描述符是在类上定义的,但通常是从实例调用的.当它从一个实例调用都owner__get__设置(你可以计算出instanceowner这样看来有点毫无意义).但是当从一个类中调用时,只会owner被设置 - 这就是它存在的原因.

这只是需要instance因为它是唯一一个可以在类上调用的.如果设置类值,则设置描述符本身.类似地删除.这就是为什么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)
Run Code Online (Sandbox Code Playgroud)

(我正在使用python 3;对于python 2,你需要确保这些划分是__get__owner).这给了:

100.0
32.0
Run Code Online (Sandbox Code Playgroud)

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

  • 转换是错误的:它们应为C = 5(F-32)/ 9,F = 32 + 9C / 5。 (2认同)

Aar*_*all 57

我试图了解Python的描述符是什么以及它们对什么有用.

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

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

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

描述符对象可用于以编程方式管理foo.descriptor正常表达式,赋值,甚至删除中的虚线查找(例如)的结果.

函数/方法,绑定方法,property,classmethodstaticmethod所有使用这些特殊的方法来控制它们是如何通过点查找访问.

一个数据描述符,比如property,可以允许基于对象的简单状态属性的懒惰评估,允许实例使用较少的内存比如果你预先计算每个可能的属性.

另一个数据描述符a member_descriptor,__slots__通过允许类将数据存储在可变的类似于元组的数据结构中而不是更灵活但占用空间来节省内存__dict__.

非数据描述符(通常是实例,类和静态方法)从它们的非数据描述符方法中获取它们的隐式第一个参数(通常分别命名clsself分别)__get__.

大多数Python用户只需要学习简单的用法,而无需进一步学习或理解描述符的实现.

深度:什么是描述符?

描述符是具有以下任何方法(__get__,__set____delete__)的对象,旨在通过点查找使用,就好像它是实例的典型属性一样.对于owner-object obj_instance,使用descriptor对象:

  • obj_instance.descriptor调用
    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描述符的实例(可能只是一个类的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))
Run Code Online (Sandbox Code Playgroud)

数据描述符具有一个__set__和/或__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))
Run Code Online (Sandbox Code Playgroud)

内置描述符对象示例:

  • classmethod
  • staticmethod
  • property
  • 功能一般

非数据描述符

我们可以看到classmethod并且staticmethod是非数据描述符:

>>> is_descriptor(classmethod), is_data_descriptor(classmethod)
(True, False)
>>> is_descriptor(staticmethod), is_data_descriptor(staticmethod)
(True, False)
Run Code Online (Sandbox Code Playgroud)

两者都只有这个__get__方法:

>>> has_descriptor_attrs(classmethod), has_descriptor_attrs(staticmethod)
(set(['__get__']), set(['__get__']))
Run Code Online (Sandbox Code Playgroud)

请注意,所有函数都是非数据描述符:

>>> def foo(): pass
... 
>>> is_descriptor(foo), is_data_descriptor(foo)
(True, False)
Run Code Online (Sandbox Code Playgroud)

数据描述符, property

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

>>> is_data_descriptor(property)
True
>>> has_descriptor_attrs(property)
set(['__set__', '__get__', '__delete__'])
Run Code Online (Sandbox Code Playgroud)

虚线查找顺序

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

obj_instance.attribute
Run Code Online (Sandbox Code Playgroud)
  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()
Run Code Online (Sandbox Code Playgroud)
  1. 为什么我需要描述符类?请使用此示例或您认为更好的示例进行说明.

您的描述符确保您始终具有此类属性的浮点数Temperature,并且您不能使用它del来删除该属性:

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

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

class Temperature(object):
    celsius = 0.0
Run Code Online (Sandbox Code Playgroud)

这会使您获得与您的示例完全相同的行为(请参阅下面对问题3的回复),但使用Pythons builtin(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)
Run Code Online (Sandbox Code Playgroud)
  1. 什么是instanceproperty这里?(in __get__).所以我的问题是,第三个参数的目的是什么?

__set__是调用描述符的所有者的实例.所有者是使用描述符对象来管理对数据点的访问的类.有关更多描述性变量名称,请参阅本答案第一段旁边定义描述符的特殊方法的描述.

  1. 我该怎么称呼/使用这个例子?

这是一个演示:

>>> t1 = Temperature()
>>> t1.celsius
0.0
>>> t1.celsius = 1
>>> 
>>> t1.celsius
1.0
>>> t2 = Temperature()
>>> t2.celsius
1.0
Run Code Online (Sandbox Code Playgroud)

您无法删除该属性:

>>> del t2.celsius
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: __delete__
Run Code Online (Sandbox Code Playgroud)

并且您无法分配无法转换为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
Run Code Online (Sandbox Code Playgroud)

否则,您所拥有的是所有实例的全局状态,通过分配给任何实例来管理.

大多数有经验的Python程序员实现这一结果的预期方式是使用__delete__装饰器,它使用相同的描述符,但将行为带入所有者类的实现(同样,如上所定义):

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)
Run Code Online (Sandbox Code Playgroud)

它与原始代码段具有完全相同的预期行为:

>>> 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
Run Code Online (Sandbox Code Playgroud)

结论

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

那么,你会如何使用这个问题的例子呢?我希望你不会.我希望你从我的第一个建议(一个简单的类属性)开始,如果你认为有必要,继续第二个建议(属性装饰器).

  • 这是一个令人惊奇的答案。我需要再读几遍,但我觉得我对 Python 的理解刚刚提升了几个档次 (3认同)
  • 很好,我从这个答案中学到了最多(当然也从其他人那里学到了)。关于“大多数经验丰富的 Python 程序员实现这一结果的预期方式......”这一陈述的问题。您在语句之前和之后定义的温度类是相同的。我错过了你在这里得到的东西吗? (2认同)

MSe*_*ert 13

在详细介绍描述符之前,了解Python中的属性查找如何工作可能很重要。这假定该类没有元类,并且使用的默认实现__getattribute__(均可用于“自定义”行为)。

在这种情况下,属性查找(在Python 3.x中或在Python 2.x中用于新样式类)的最佳说明来自于了解Python元类(ionel的代码日志)。该图像:代替“不可自定义的属性查找”。

这代表一个属性的查找foobarinstanceClass

在此处输入图片说明

这里有两个条件很重要:

  • 如果的类instance具有属性名称的条目,并且具有__get____set__
  • 如果instance已经没有了属性名称条目,但类有一个和它有__get__

这就是描述符的所在:

  • 具有__get__和的数据描述符__set__
  • 仅具有的非数据描述符__get__

在这两种情况下,返回的值都会__get__以实例作为第一个参数,而类作为第二个参数进行调用。

对于类属性查找,查找甚至更加复杂(例如,请参见类属性查找(在上述博客中))。

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

为什么需要描述符类?

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

def test_function(self):
    return self

class TestClass(object):
    def test_method(self):
        ...
Run Code Online (Sandbox Code Playgroud)

如果您查看test_method实例,您将获得“绑定方法”:

>>> instance = TestClass()
>>> instance.test_method
<bound method TestClass.test_method of <__main__.TestClass object at ...>>
Run Code Online (Sandbox Code Playgroud)

同样,您也可以通过__get__手动调用函数的方法来绑定函数(不建议这样做,仅出于说明目的):

>>> test_function.__get__(instance, TestClass)
<bound method test_function of <__main__.TestClass object at ...>>
Run Code Online (Sandbox Code Playgroud)

您甚至可以将其称为“自绑定方法”:

>>> test_function.__get__(instance, TestClass)()
<__main__.TestClass at ...>
Run Code Online (Sandbox Code Playgroud)

请注意,我没有提供任何参数,该函数确实返回了绑定的实例!

函数是非数据描述符

数据描述符的一些内置示例为property。忽略gettersetterdeleterproperty描述符是(来自描述符方法指南“属性”):

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)
Run Code Online (Sandbox Code Playgroud)

因为它是一个数据描述符它的调用,只要你抬头看的“名字” property,它只是委托给装饰的功能@property@name.setter以及@name.deleter(如果存在的话)。

还有一些其他的描述符在标准库中,例如staticmethodclassmethod

描述符的要点很容易(尽管您很少需要它们):用于属性访问的抽象通用代码。property是实例变量访问function的抽象,staticmethod提供方法的抽象,为不需要实例访问classmethod的方法提供抽象,为需要类访问而不是实例访问的方法提供抽象(这有点简化)。

另一个示例是class属性

一个有趣的示例(__set_name__从Python 3.6 使用)也可以是仅允许特定类型的属性:

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
Run Code Online (Sandbox Code Playgroud)

然后,您可以在类中使用描述符:

class Test(object):
    int_prop = TypedProperty(int)
Run Code Online (Sandbox Code Playgroud)

并玩一点:

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

>>> t.int_prop = 20.0
TypeError: Expected class <class 'int'>, got <class 'float'>
Run Code Online (Sandbox Code Playgroud)

或“懒惰的财产”:

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
Run Code Online (Sandbox Code Playgroud)

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

什么是instanceowner这里?(在中__get__)。这些参数的目的是什么?

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

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

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

  • 第二个参数是 None
  • 第三个参数是您在其中查找属性的类

因此,如果您想在进行类级查找时自定义行为(因为instanceis None),则基本上需要第三个参数。

我将如何调用/使用此示例?

您的示例基本上是一个属性,该属性仅允许将值转换为该值,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
Run Code Online (Sandbox Code Playgroud)

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


wll*_*bll 6

为什么我需要描述符类?请使用此示例或您认为更好的示例进行说明.

受Buciano Ramalho的Fluent Python启发

想象你有这样一个类

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
Run Code Online (Sandbox Code Playgroud)

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

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)
Run Code Online (Sandbox Code Playgroud)

然后像这样定义类LineItem:

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

     def __init__(self, name, weight, price):
         self.name = name
         self.weight = weight
         self.price = price
Run Code Online (Sandbox Code Playgroud)

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