理解__get__和__set__及Python描述符

399 投票
8 回答
206768 浏览
提问于 2025-04-16 04:37

我正在努力理解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()
  1. 我为什么需要描述符类?

  2. 这里的instanceowner是什么?(在__get__中)。这些参数有什么用?

  3. 我该如何调用/使用这个例子?

8 个回答

101

我想了解一下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)的结果,这可以在正常的表达式、赋值或删除中使用。

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

一个数据描述符,比如property,可以根据对象的简单状态来延迟评估属性,这样实例可以使用比预先计算每个可能属性更少的内存。

另一个数据描述符,由__slots__创建的member_descriptor,通过让类将数据存储在一个可变的元组样的数据结构中,而不是更灵活但占用空间的__dict__,实现了节省内存(和更快的查找)。

非数据描述符、实例和类方法,通过它们的非数据描述符方法__get__获取隐式的第一个参数(通常命名为selfcls),这就是静态方法知道不需要隐式第一个参数的原因。

大多数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是包含描述符对象实例的类的实例。selfdescriptor的实例(对于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
  • 一般函数

非数据描述符

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

>>> 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
  1. 首先查看该属性是否是实例类上的数据描述符,
  2. 如果不是,则查看该属性是否在obj_instance__dict__中,然后
  3. 最后回退到非数据描述符。

这种查找顺序的结果是,像函数/方法这样的非数据描述符可以被实例重写

总结和下一步

我们已经了解到描述符是具有__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()
  1. 我为什么需要描述符类?

你的描述符确保这个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)
  1. 这里的实例和拥有者是什么?(在get中)。这些参数的目的是什么?

instance是调用描述符的拥有者的实例。拥有者是使用描述符对象来管理数据点访问的类。有关定义描述符的特殊方法的描述,请参见本答案第一段旁边的描述性变量名称。

  1. 我该如何调用/使用这个示例?

这是一个演示:

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

结论

我们已经讨论了定义描述符的属性、数据描述符和非数据描述符之间的区别、使用它们的内置对象,以及关于使用的具体问题。

那么,你会如何使用问题中的示例?我希望你不会。我希望你从我的第一个建议(一个简单的类属性)开始,如果觉得有必要,再转向第二个建议(属性装饰器)。

146

我为什么需要描述符类?

描述符类让你对属性的行为有更多的控制。如果你熟悉Java中的getter和setter,那么描述符就是Python实现类似功能的方式。一个好处是,它看起来就像普通的属性(语法没有变化)。所以你可以先用普通属性,等到需要一些复杂的功能时,再切换到描述符。

属性只是一个可以改变的值。而描述符则允许你在读取、设置(或删除)值时执行任意代码。比如,你可以用它把一个属性映射到数据库中的一个字段,这就像一种对象关系映射(ORM)。

另一个用法可能是在__set__中拒绝接受新值,通过抛出异常来实现,这样就有效地让这个“属性”变成只读的。

instanceowner在这里是什么?(在__get__中)。这些参数的目的是什么?

这个问题有点微妙(这也是我在这里写新答案的原因 - 我在寻找这个问题的答案时发现现有的答案并不太好)。

描述符是在类上定义的,但通常是从实例中调用的。当从实例调用时,instanceowner都会被设置(而且你可以通过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类里面),但这展示了可以做到的事情……

194

描述符是Python中property类型的实现方式。简单来说,描述符就是实现了__get____set__等方法的一个东西,然后把它放到另一个类里(就像你在上面的温度类中做的那样)。举个例子:

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

当你访问你给描述符指定的属性(上面例子中的celsius)时,就会调用相应的描述符方法。

__get__方法中,instance是类的实例(所以在上面的例子中,__get__会接收到temp,而owner是带有描述符的类,也就是Temperature)。

你需要使用描述符类来封装它的逻辑。这样,如果描述符用来缓存一些耗时的操作(比如说),它可以把值存储在自己身上,而不是存储在它的类里。

官方的Python文档里有一篇关于描述符的文章,详细讲解了它们是如何工作的,还包括几个例子。

编辑:正如jchl在评论中提到的,如果你直接尝试Temperature.celsius,那么instance会是None

撰写回答