我试图理解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)的结果。
函数/方法、绑定方法、属性、类方法和staticmethod都使用这些特殊的方法来控制如何通过点查找访问它们。
数据描述符(如属性)允许基于更简单的对象状态对属性进行延迟计算,从而允许实例使用比预先计算每个可能属性更少的内存。
另一个数据描述符是由__slots__创建的member_descriptor,通过让类将数据存储在可变的元组类数据结构中,而不是更灵活但占用空间的__dict__,可以节省内存(和更快的查找)。
非数据描述符,实例和类方法,从它们的非数据描述符方法__get__中获得它们的隐式第一个参数(通常分别命名为self和cls)——这就是静态方法如何知道不要有隐式第一个参数。
大多数Python用户只需要学习描述符的高级用法,而不需要进一步学习或理解描述符的实现。
但是了解描述符的工作原理可以让人对掌握Python更有信心。
什么是描述符?
描述符是具有以下任何方法(__get__, __set__或__delete__)的对象,旨在通过点查找来使用,就像它是实例的典型属性一样。对于带有描述符对象的所有者对象obj_instance:
obj_instance.descriptor调用
描述符。__get__(self, obj_instance, owner_class)返回一个值
这就是属性上的所有方法和get的工作方式。
Obj_instance.descriptor = value调用
描述符。__set__(self, obj_instance, value)返回None
这就是属性的setter的工作方式。
Del obj_instance.descriptor调用
描述符。__delete__(self, obj_instance)返回None
这就是属性上的删除器的工作方式。
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))
数据描述符有__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))
内置描述符对象示例:
classmethod
staticmethod
财产
一般函数
数据描述符
我们可以看到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)
数据描述符,属性
然而,属性是一个数据描述符:
>>> is_data_descriptor(property)
True
>>> has_descriptor_attrs(property)
set(['__set__', '__get__', '__delete__'])
点查找顺序
这些是重要的区别,因为它们会影响点查找的查找顺序。
obj_instance.attribute
首先,上面的代码查看属性是否是实例类上的Data-Descriptor,
如果不是,它会查看该属性是否在obj_instance的__dict__中,然后
它最终会退回到非数据描述符。
这种查找顺序的结果是,像函数/方法这样的非数据描述符可以被实例覆盖。
概述和下一步
我们已经了解到,描述符是具有__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()
为什么我需要描述符类?
你的描述符确保你总是有一个float的class属性Temperature,并且你不能使用del删除这个属性:
>>> t1 = Temperature()
>>> del t1.celsius
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: __delete__
否则,您的描述符将忽略所有者类和所有者的实例,而是将状态存储在描述符中。你可以通过一个简单的class属性轻松地在所有实例之间共享状态(只要你总是将它设置为类的float,并且永远不会删除它,或者你的代码的用户愿意这样做):
class Temperature(object):
celsius = 0.0
这将使您的行为与您的示例完全相同(参见下面对问题3的响应),但使用了python的内置(属性),并且将被认为更习惯:
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)
这里的实例和所有者是什么?(得到)。这些参数的目的是什么?
Instance是调用描述符的所有者的实例。所有者是一个类,其中描述符对象用于管理对数据点的访问。关于更多描述性变量名,请参阅本答案第一段旁边定义描述符的特殊方法的描述。
我如何调用/使用这个例子?
下面是一个演示:
>>> 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程序员实现这一结果的预期方式是使用属性装饰器,它在底层使用相同的描述符,但将行为带入所有者类的实现中(同样,如上所定义):
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
结论
我们已经讨论了定义描述符的属性、数据描述符和非数据描述符之间的区别、使用它们的内置对象以及关于使用的特定问题。
那么,你会怎么用这个问题的例子呢?我希望你不会。我希望您从我的第一个建议(一个简单的类属性)开始,如果您认为有必要,可以转向第二个建议(属性装饰器)。