理解__get__和__set__及Python描述符
我正在努力理解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()
我为什么需要描述符类?
这里的
instance
和owner
是什么?(在__get__
中)。这些参数有什么用?我该如何调用/使用这个例子?
8 个回答
我想了解一下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__
),旨在通过点查找使用,就像它是实例的典型属性一样。对于一个拥有descriptor
对象的拥有者对象obj_instance
:
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))
一个数据描述符具有__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
- 首先查看该属性是否是实例类上的数据描述符,
- 如果不是,则查看该属性是否在
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()
- 我为什么需要描述符类?
你的描述符确保这个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
结论
我们已经讨论了定义描述符的属性、数据描述符和非数据描述符之间的区别、使用它们的内置对象,以及关于使用的具体问题。
那么,你会如何使用问题中的示例?我希望你不会。我希望你从我的第一个建议(一个简单的类属性)开始,如果觉得有必要,再转向第二个建议(属性装饰器)。
我为什么需要描述符类?
描述符类让你对属性的行为有更多的控制。如果你熟悉Java中的getter和setter,那么描述符就是Python实现类似功能的方式。一个好处是,它看起来就像普通的属性(语法没有变化)。所以你可以先用普通属性,等到需要一些复杂的功能时,再切换到描述符。
属性只是一个可以改变的值。而描述符则允许你在读取、设置(或删除)值时执行任意代码。比如,你可以用它把一个属性映射到数据库中的一个字段,这就像一种对象关系映射(ORM)。
另一个用法可能是在__set__
中拒绝接受新值,通过抛出异常来实现,这样就有效地让这个“属性”变成只读的。
instance
和owner
在这里是什么?(在__get__
中)。这些参数的目的是什么?
这个问题有点微妙(这也是我在这里写新答案的原因 - 我在寻找这个问题的答案时发现现有的答案并不太好)。
描述符是在类上定义的,但通常是从实例中调用的。当从实例调用时,instance
和owner
都会被设置(而且你可以通过instance
推导出owner
,所以看起来有点没必要)。但是当从类调用时,只有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中还有其他更好的方法可以实现同样的效果(例如,如果摄氏度是一个属性,这也是基本机制,但把所有源代码放在Temperature类里面),但这展示了可以做到的事情……
描述符是Python中property
类型的实现方式。简单来说,描述符就是实现了__get__
、__set__
等方法的一个东西,然后把它放到另一个类里(就像你在上面的温度类中做的那样)。举个例子:
temp=Temperature()
temp.celsius #calls celsius.__get__
当你访问你给描述符指定的属性(上面例子中的celsius
)时,就会调用相应的描述符方法。
在__get__
方法中,instance
是类的实例(所以在上面的例子中,__get__
会接收到temp
,而owner
是带有描述符的类,也就是Temperature
)。
你需要使用描述符类来封装它的逻辑。这样,如果描述符用来缓存一些耗时的操作(比如说),它可以把值存储在自己身上,而不是存储在它的类里。
官方的Python文档里有一篇关于描述符的文章,详细讲解了它们是如何工作的,还包括几个例子。
编辑:正如jchl在评论中提到的,如果你直接尝试Temperature.celsius
,那么instance
会是None
。