我试图了解 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,可以允许基于对象的更简单状态对属性进行延迟评估,与预先计算每个可能的属性相比,允许实例使用更少的内存。
另一个数据描述符,由__slots__ 创建的member_descriptor,通过让类将数据存储在类似元组的可变数据结构而不是更灵活的数据结构中来节省内存(和更快的查找)但占用空间__dict__。
非数据描述符、实例和类方法从它们的非数据描述符方法__get__ 中获取它们的隐式第一个参数(通常分别命名为self 和cls - 这就是静态方法知道的方式不要有一个隐含的第一个参数。
大多数 Python 用户只需要学习描述符的高级用法,无需进一步学习或理解描述符的实现。
但是了解描述符的工作原理可以让人们更加自信地掌握 Python。
深入了解:什么是描述符?
描述符是具有以下任何方法(__get__、__set__ 或 __delete__)的对象,旨在通过点查找来使用,就好像它是实例的典型属性一样。对于所有者对象obj_instance,带有descriptor 对象:
-
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 是 descriptor 的实例(可能只是 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))
Data Descriptor 有一个__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__'])
点查找顺序
这些distinctions 很重要,因为它们会影响虚线查找的查找顺序。
obj_instance.attribute
- 首先看一下属性是否是实例类上的Data-Descriptor,
- 如果不是,则查看属性是否在
obj_instance的__dict__中,然后
- 它最终退回到非数据描述符。
这种查找顺序的结果是,像函数/方法这样的非数据描述符可以是overridden by instances。
回顾和后续步骤
我们了解到,描述符是具有__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 的回复),但使用 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)
- 这里的实例和所有者是什么? (在 get 中)。这些参数的用途是什么?
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
结论
我们已经介绍了定义描述符的属性、数据描述符和非数据描述符之间的区别、使用它们的内置对象以及有关使用的具体问题。
同样,您将如何使用问题的示例?我希望你不会。我希望您从我的第一个建议(一个简单的类属性)开始,如果您觉得有必要,请继续执行第二个建议(属性装饰器)。