【问题标题】:What is happening with the descriptors in this class?此类中的描述符发生了什么?
【发布时间】:2021-09-01 13:06:04
【问题描述】:

我完成了一个练习题,您在其中构建了一个温度()类,它接受摄氏、开尔文或华氏温度,并可以返回其他比例的转换值。我使用描述符来做到这一点,这是我第一次接触到它们。经过大量搜索,我最终得到了一个正确的答案,基本上在网上找到了一个我修改过的类似课程。问题是,我仍然无法真正理解实现类时发生的事情。我阅读了有关描述符的文档并观看了一些有关 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 时,这会调用 Kelvin() 的 __set__ 方法吗?我不明白在这种情况下 50 的值存储在哪里。而且我也不明白当我分配t1.kelvin=50 时如何处理__set__ 中的instance.celsius = (value - 273.15)。我在这里不太了解一些非常基本的东西,但是在阅读了一些内容后我有点卡住了。

【问题讨论】:

  • 这里与 MRO 无关 - 您没有子类的层次结构。

标签: python descriptor


【解决方案1】:

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

那么,针对您的具体疑问:

当我将 kelvin 属性设置为 50 时,它会调用 __set__ 方法 开尔文()正确吗?我不明白 50 的值在哪里 然后存储在这种情况下。

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

我也不明白 instance.celsius = (value - 273.15) 在 set 在我分配 t1.kelvin=50 时被处理。

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

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

在这种情况下,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”中获得对实例的引用。

【讨论】:

  • 我非常感谢您的广泛回答。我发现我离理解这一点比我最初想象的要远得多。我想我得把你写的东西重读几遍,哈哈。非常感谢您的帮助。
  • 按照我的指示修复你的 odcde,并填写“打印”调用,这应该会有所帮助。
  • 所以我尝试按照您在阅读文档后建议的方式实现__set_name__ 方法。你会说这是一种改进吗? pastebin.com/LPiKAci7 当我检查 vars() 的多个实例时,每个实例都包含作为私有属性的数据。这将解决您提到的问题,对吗?
  • 是的 - 这两件事都在改进。当然,保持每个实例中的值更为重要。 __set_name__ 很不错,它主要允许您不需要显式传递描述符的名称。
【解决方案2】:

t1.kelvin = 50 等价于

Temperature.kelvin.__set__(t1, 50)

赋值不会立即创建或修改名为kelvin 的实例属性;首先,检查t1 的类型以查找名为kelvin 的描述符,当找到时,将调用该描述符的__set__ 方法。

因此,您可以看到被分配的是t1.celsius = 50 - 273.15

50 本身并不存储在任何地方。如果您随后尝试类似

print(t1.kelvin)

结果调用是Temperature.kelvin.__get__(t1, Temperature),它返回t1.celsius + 273.15,有效地撤消了首先设置t1.celsius 值的减法。

换句话说,所有三个描述符通过在设置时将它们的特定值标准化为摄氏度,并在检索时将摄氏度温度转换回所需的刻度来“合作”。

【讨论】:

  • 这很有帮助。我不明白开尔文最初是通过首先获得摄氏度来获得的。当你这样布置它时,它是有道理的。在这和 jsbueno 给出的带有打印语句的代码之间,我想我应该能够弄清楚事情是如何发生的。非常感谢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-11-22
  • 1970-01-01
  • 1970-01-01
  • 2014-12-20
相关资源
最近更新 更多