我试图了解 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
普通表达式、赋值或删除中的点查找(例如)的结果。
函数/方法、绑定方法、、property
等都使用这些特殊方法来控制如何通过点分查找来访问它们。classmethod
staticmethod
数据描述符,如property
,可以允许基于对象的更简单状态对属性进行延迟评估,与预先计算每个可能的属性相比,允许实例使用更少的内存。
另一个数据描述符由.member_descriptor
创建,通过__slots__
让类将数据存储在类似元组的可变数据结构中而不是更灵活但占用空间的__dict__
.
非数据描述符、实例和类方法从它们的非数据描述符方法中获取它们的隐式第一个参数(通常分别命名为self
和) ,这就是静态方法知道没有隐式第一个参数的方式。cls
__get__
大多数 Python 用户只需要学习描述符的高级用法,不需要进一步学习或理解描述符的实现。
但是了解描述符的工作原理可以让人们对自己掌握 Python 更有信心。
深入:什么是描述符?
描述符是具有以下任何方法( 、 或 )的对象__get__
,__set__
旨在__delete__
通过点查找来使用,就好像它是实例的典型属性一样。对于所有者对象,obj_instance
,带有descriptor
对象:
obj_instance.descriptor
调用
descriptor.__get__(self, obj_instance, owner_class)
返回 avalue
这是所有方法和get
on 属性的工作方式。
obj_instance.descriptor = value
调用
descriptor.__set__(self, obj_instance, value)
返回None
这就是setter
on 属性的工作方式。
del obj_instance.descriptor
调用
descriptor.__delete__(self, obj_instance)
返回None
这就是deleter
on 属性的工作方式。
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
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__'])
虚线查找顺序
这些是重要的区别,因为它们会影响点查找的查找顺序。
obj_instance.attribute
- 首先,上面查看属性是否是实例类上的 Data-Descriptor,
- 如果不是,它会查看属性是否在
obj_instance
's__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()
- 为什么我需要描述符类?
您的描述符确保您始终拥有此类属性的浮点数Temperature
,并且您不能使用它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 的回复),但使用 Pythons 内置函数 ( 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)
- 这里的实例和所有者是什么?(在得到)。这些参数的目的是什么?
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 程序员实现此结果的预期方式是使用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
结论
我们已经介绍了定义描述符的属性、数据描述符和非数据描述符之间的区别、使用它们的内置对象以及有关使用的具体问题。
再说一次,您将如何使用问题的示例?我希望你不会。我希望你会从我的第一个建议(一个简单的类属性)开始,如果你觉得有必要的话,继续进行第二个建议(属性装饰器)。