我试图了解什么是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()



为什么需要描述符类?
这里的instanceowner是什么? (在__get__中)。这些参数的目的是什么?
如何调用/使用此示例?


#1 楼

描述符是如何实现Python的property类型的。描述符仅实现__get____set__等,然后将其添加到其定义中的另一个类中(就像您上面对Temperature类所做的一样)。例如:

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


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

instance__get__中是该类的实例(因此,上面的__get__将收到temp,而owner是具有描述符的类(因此它将是Temperature)。

您需要使用描述符类进行封装这样,如果描述符用于缓存某些昂贵的操作(例如),它可以将值存储在自身而不是其类上。

关于描述符的文章可以是在此处找到。

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

评论


自我和实例之间有什么区别?

– Lemma Prism
19 Mar 12'19在2:43

'instance'可以是任何类的实例,self是相同类的实例。

–初学者
19年7月8日在11:27

@LemmaPrism self是描述符实例,instance是描述符所在的类(如果实例化)的实例(instance .__ class__是所有者)。

– Tcll
19年8月23日在22:07

根据代码celsius = Celsius(),Temperature.celsius的值为0.0。调用描述符Celsius,因此其实例具有分配给Temperature类属性celsius的初始值0.0。

–天使萨拉查
2月8日6:00

#2 楼


为什么需要描述符类?


它使您可以更好地控制属性的工作方式。例如,如果您习惯于使用Java中的getter和setter,那么这就是Python的方法。优点之一是,它对用户的外观就像一个属性(语法没有变化)。因此,您可以从一个普通的属性开始,然后在需要执行某些操作时切换到描述符。

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

另一种用法可能是通过在__set__中抛出异常来拒绝接受新值–有效地使“属性”变为只读。


这里的instanceowner是什么? (在__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中实现相同的效果(例如,如果celsius是一个属性,这是相同的基本机制,但是将所有源都放在Temperature类中),但这显示了可以完成的工作...

评论


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

–音乐爱好者
2015年6月11日在5:04



确保您有一个温度对象。进行后续操作会使事情变得混乱。 t1 =温度(190)打印t1.celsius t1.celsius = 100打印t1.fahrenheit现在,当您检查t.celcius和t.fahrenheit时,它们也会被修改。摄氏温度是115,华氏温度是32。这显然是错误的。埃里克

–艾珊·巴特(Ishan Bhatt)
16-3-7在11:33



@IshanBhatt:我认为这是由于上述musiphil指出的错误。另外,这不是我的答案

–埃里克
16 Mar 7 '16 at 14:52

#3 楼


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


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



__get__(非数据描述符方法,例如,方法/函数)

__set__(数据描述符方法,例如,方法属性实例)

__delete__(数据描述符方法)

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

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

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

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

member_descriptor创建的另一个数据描述符__slots__通过允许类将数据存储在可变的类似tuple的数据结构中来代替节省空间的__dict__,从而节省了内存。 >
非数据描述符(通常是实例,类和静态方法)从其非数据描述符方法cls获得其隐式第一个参数(分别分别称为self__get__)。

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的类可能只是一个实例)

要用代码定义此对象,如果对象的属性集与任何必需的属性相交,则该对象为描述符:

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


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

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


Builtin描述符对象示例:


__delete__
classmethod
staticmethod
一般功能

非数据描述符

我们可以看到propertyclassmethod是非数据描述符:

>>> is_descriptor(classmethod), is_data_descriptor(classmethod)
(True, False)
>>> is_descriptor(staticmethod), is_data_descriptor(staticmethod)
(True, False)


两者都具有staticmethod方法:

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


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

>>> def foo(): pass
... 
>>> is_descriptor(foo), is_data_descriptor(foo)
(True, False)


数据描述符__get__


然而,property是数据描述符:

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


点分查找顺序

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

obj_instance.attribute



首先,上面的内容看一下该属性是否是实例类
上的数据描述符。如果不是,则看该属性是否在propertyobj_instance中,然后它最终退回到非数据描述符。

此查找顺序的结果是实例可以覆盖诸如函数/方法之类的非数据描述符。

回顾和下一步

我们了解到描述符是具有__dict____get____set__中的任何一个的对象。这些描述符对象可用作其他对象类定义的属性。现在,我们将以您的代码为例来研究它们的用法。


问题中的代码分析

这里是您的代码,然后是您的代码每个问题和答案:

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




为什么需要描述符类?


>
您的描述符确保__delete__的此类属性始终为浮点,并且不能使用Temperature删除该属性:

>>> 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的回答),但是使用内置的Pythons(del),并且会被认为更惯用:

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)




这里的实例和所有者是什么? (获得)。这些参数的目的是什么?



property是正在调用描述符的所有者的实例。所有者是使用描述符对象管理对数据点的访问的类。有关更多描述性的变量名称,请参见此答案第一段旁边的定义描述符的特殊方法的说明。



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


这里是一个演示:

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


并且您不能分配不能转换为浮点数的变量:

>>> 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程序员完成此结果的预期方式是使用instance装饰器,该装饰器在后台使用相同的描述符,但将行为引入了owner类的实现中(再次定义如上):

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


结论

我们已经介绍了定义描述符的属性,数据描述符和非数据描述符之间的区别,使用它们的内置对象以及有关使用的特定问题。 br />
那么,您将如何使用问题的示例?我希望你不会。希望您从我的第一个建议(一个简单的class属性)开始,并转至第二个建议(属性装饰器),如果您认为有必要的话。

评论


很好,我从这个答案中学到了很多(当然也从别人那里学到了)。关于此语句的问题“大多数有经验的Python程序员完成此结果的预期方式……”。您在语句之前和之后定义的Temeperature类是相同的。我想念你在这里得到什么吗?

–耶洛·沃(Yolo Voe)
18年4月26日在15:47



@YoloVoe不,是的,我添加了一些括号括弧,以强调这是上面的重复。

–亚伦·霍尔♦
18年4月26日在15:57

这是一个惊人的答案。我需要再读几次,但我觉得我对Python的理解只增加了几个档次

–卢卡斯·杨(Lucas Young)
2月28日8:43

#4 楼

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

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

这表示在foobarinstance上查找属性Class



这里有两个条件很重要:


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

这就是描述符的所在:



具有__get____set__两者。

仅包含__get__的非数据描述符。在两种情况下,返回值均通过__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 ...>


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

函数是Non -data描述符!

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

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@property@name.setter装饰的函数(如果存在)。

标准库中还有其他几个描述符,例如@name.deleterstaticmethod

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

另一个示例是类属性。

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

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


这些是将逻辑转换为通用逻辑的情况描述符可能有意义,但是也可以用其他方法解决它们(但可能需要重复一些代码)。


__set_name__instance是什么? (在owner中)。这些参数的目的是什么?


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


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

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


第二个参数是__get__

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

因此,基本上,如果要在执行类级查询时自定义行为,则第三个参数是必需的(因为Noneinstance)。


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


您的示例基本上是一个属性,仅允许可以转换为None,并且在类的所有实例之间共享(并且在该类上共享-尽管只能在类上使用“读取”访问权限,否则您将替换描述符实例):

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


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

评论


不知道是否应将图形的透明背景确实在暗模式下遭受影响,是否应将其报告为stackoverflow的错误。

– Tshirtman
4月18日18:44

@Tshirtman我认为这是图像本身的问题。它不是完全透明的...我从博客中获取了它,并且不知道如何在适当的透明背景下重新创建它。太糟糕了,它看起来很暗背景:(

–MSeifert
4月18日20:17

#5 楼


为什么需要描述符类?


受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


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

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类以执行更常见的验证

评论


有趣的用例,因为它显示了如何使用描述符与多个用户实例进行交互。我最初不明白要点:必须在类名称空间中创建带有描述符的属性(例如weight = Quantity(),但必须仅使用self在实例名称空间中设置值(例如self.weight = 4) ,否则属性将被反弹到新值,并且描述符将被丢弃。

–分钟
17年8月22日在23:26

我无法理解一件事。您正在将weight = Quantity()定义为类变量,并且其__get__和__set__在实例变量上起作用。怎么样?

–技术专家
4月23日7:58

#6 楼

我尝试(根据建议进行了一些小的更改)安德鲁·库克答案中的代码。 (我正在运行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 __name__ == "__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魔术不适用于旧样式类。

评论


描述符仅适用于新样式类。对于python 2.x,这意味着从“对象”派生您的类,这是Python 3中的默认设置。

– Ivo van der Wijk
15年1月26日在11:18

#7 楼

您会看到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__)
 


评论


这不会回答问题或提供任何有用的信息。

–塞巴斯蒂安·尼尔森(Sebastian Nielsen)
1月23日12:42