首页 > 解决方案 > 此类中的描述符发生了什么?

问题描述

我完成了一个练习题,您在其中构建了一个类 Temperature(),它接受摄氏度、开尔文或华氏度的温度,并可以返回其他比例的转换值。我使用描述符来做到这一点,这是我第一次接触到它们。经过大量搜索,我最终得到了一个正确的答案,基本上在网上找到了一个我修改过的类似课程。问题是,我仍然无法真正理解实现类时发生的事情。我阅读了有关描述符的文档并观看了一些有关 MRO 和托管属性的视频,但我仍然无法完全理解发生了什么。

class Celsius():
    def __init__(self, val=0):
        self.val=val
    def __get__(self, instance, owner):
        return self.val
    def __set__(self, instance, value):
        self.val=value

class Fahrenheit():
    def __get__(self, instance, owner):
        return instance.celsius * 1.8 + 32
    def __set__(self, instance, value):
        instance.celsius=(value-32)/1.8

class Kelvin():
    def __get__(self, instance, owner):
        return (instance.celsius + 273.15)
    def __set__(self, instance, value):
        instance.celsius = (value - 273.15)


class Temperature():
    celsius = Celsius()
    fahrenheit = Fahrenheit()
    kelvin = Kelvin()

我特别不明白

t1=Temperature()
t1.kelvin=50

被执行。当我将 kelvin 属性设置为 50 时,调用__set__Kelvin() 的方法对吗?我不明白在这种情况下 50 的值存储在哪里。而且我也不明白instance.celsius = (value - 273.15)__set__分配时如何处理t1.kelvin=50。我在这里不了解一些非常基本的东西,但是在阅读了一些内容后我有点卡住了。

标签: pythondescriptor

解决方案


首先:这与“描述”而不是“方法解析顺序”有关,“方法解析顺序”是为搜索超类的方法和属性而保留的表达式。

然后,针对您的具体疑问:

当我将 kelvin 属性设置为 50 时,调用__set__Kelvin() 的方法对吗?我不明白在这种情况下 50 的值存储在哪里。

是的,该方法被调用。在这种情况下,不存储正确的值“50” 。在这个设计中,Temperature实例必须提供一个单一的、有效的温度测量值,无论所需的比例如何,它都可以保持一致并且可以写入和读取。为该值的内部表示选择的刻度是摄氏度(尽管问题中显示的摄氏度描述符不正确,请参阅下文)。因此,开尔文和华氏度的读取和写入都不存储它们的值:写入将刻度转换为摄氏温度,并设置摄氏温度值,并通过读取摄氏温度值的__get__方法将其转换回所需的刻度。换句话说:Kelvin 和 F. 的值是“计算的”。

而且我也不明白当我分配 t1.kelvin=50 时如何处理集合中的 instance.celsius = (value - 273.15)。

它的处理方式与任何其他对温度实例“摄氏度”的赋值相同: Celsius.set运行中的代码 - 在这种情况下,它获得的值是传递给“instance.kelvin”的初始值减去 273.15: expression (value - 273.15) 没有什么特别之处(实际上不需要括号)。它将传递的值转换为摄氏度。instance.celsius指向描述符的事实 使=运算符的行为有所不同:__set__调用摄氏度中的方法 - 并接收减法运算的结果(这恰好是摄氏度中的正确值)

错误的代码:现在,请注意一件事:描述符在类主体中实例化 - 并且为该类的所有实例共享。这意味着 的所有实例Temperature都共享您的类的同一实例Celsius:因此它不能将其值作为“自我”的属性。如果您Temperature在代码中按原样创建两个实例,您将看到它们不是独立的,因为对所有比例的读取和写入都是在 的val属性中执行的Temperature.celsius。正确的做法是保留在 and中收到的实例参数中的值__get____set__描述符的方法。为了避免递归,通常通过在实例上保留不同的属性名称来完成 - 这可以通过添加“_”前缀来完成。

在这种情况下,celsius描述符只是一个“无操作”,甚至不可能存在。此外,由于您正在编写描述符,如果它将内部属性硬编码为“_celsius”,如本例所示,同一类中的多个描述符实例会干扰另一个:必须使用动态描述符的名称。

class Celsius():
    def __init__(self, name):
        self.name=name
        # The name could be set automatically
        # by having a __set_name__ method, but let's
        # go one step at a time
    def __get__(self, instance, owner):
        if not instance: 
            #when retrieved from the class, "instance" is None, then
            # return the descriptor itself,
            # and do not attempt to fetch values
            return self
        return getattr(instance, "_" + self.name)
    def __set__(self, instance, value):
        setattr(instance, "_" + self.name)

除此之外,我建议您在 getter 和 setter 中添加“打印”调用并尝试它,直到您掌握您在此处询问的内容的流程:

class Celsius():
    def __init__(self, name):
        self.name=name
        # The name could be set automatically
        # by having a __set_name__ method, but let's
        # go one step at a time
    def __get__(self, instance, owner):
        if not instance:
            return self
        print(f"celsius getter, picking the internal value from '_{self.name}'")
        return getattr(instance, "_" + self.name)
    def __set__(self, instance, value):
        print(...)
        setattr(instance, "_" + self.name)
class Fahrenheit():
    def __get__(self, instance, owner):
        if not instance: 
            return self
        print(...)
        return instance.celsius * 1.8 + 32
    def __set__(self, instance, value):
        print(...)
        instance.celsius=(value-32)/1.8

class Kelvin():
    def __get__(self, instance, owner):
        if not instance: 
            return self
        print(...)
        return (instance.celsius + 273.15)
    def __set__(self, instance, value):
        print(...)
        instance.celsius = (value - 273.15)


class Temperature():
    celsius = Celsius("celsius")
    fahrenheit = Fahrenheit()
    kelvin = Kelvin()

最后:请记住,对于不可重用且适合单个属性的代码,例如在此示例中,Python 已经提供property了一个描述符,但已经填满,因此您只需担心 getter 中的逻辑和setter,不要担心处理描述符实例本身:你的方法已经在“self”中获得了对实例的引用。


推荐阅读