【问题标题】:Updating Dependent Attributes After Mutation变异后更新依赖属性
【发布时间】:2021-05-02 00:17:16
【问题描述】:

假设我有以下课程:

import math


class LineSegment:
    def __init__(
        self,
        origin,
        termination,
    ):
        self.origin = origin
        self.termination = termination
        self.length = self.calculate_length()

    def calculate_length(self):
        return math.sqrt(
            (self.origin.x - self.termination.x) ** 2
            + (self.origin.y - self.termination.y) ** 2
        )


class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

LineSegment 类的一个对象由 Point 类的两个对象组成。现在,假设我这样初始化一个对象:

this_origin = Point(x=0, y=0)
this_termination = Point(x=1, y=1)
this_line_segment = LineSegment(origin=this_origin, termination=this_termination)

注意:线段的初始化会自动计算其长度。这对代码库的其他部分至关重要,并且无法更改。我可以看到它的长度是这样的:

print(this_line_segment.length)    # This prints "1.4142135623730951" to the console.

现在,我需要改变 this_line_segment 的子对象的一个​​参数:

this_line_segment.origin.x = 1

但是,this_line_segments 长度属性不会根据新原点的 x 坐标进行更新:

print(this_line_segment.length)    # This still prints "1.4142135623730951" to the console.

当类所依赖的属性之一发生变化时,实现更新类属性的pythonic方法是什么?

【问题讨论】:

  • length 定义为属性并在读取访问时更新/计算它。请参阅示例here,该示例验证写入数据。类似,但您将计算代码放在属性 getter 而不是 setter 上。

标签: python class object parameters attributes


【解决方案1】:

选项 1:Getter 和 Setter 方法

在其他面向对象的编程语言中,您希望的行为(在访问实例变量的值时添加额外的逻辑)通常由对象中所有实例变量的“getter”和“setter”方法实现:

class LineSegment:
    def __init__(
        self,
        origin,
        termination,
    ):
        self._origin = origin
        self._termination = termination

    # getter method for origin
    def get_origin(self):
        return self._origin

    # setter method for origin
    def set_origin(self,new_origin):
        self._origin = new_origin

    # getter method for termination
    def get_termination(self):
        return self._termination

    # setter method for termination
    def set_termination(self,new_termination):
        self._termination = new_termination

    def get_length(self):
        return math.sqrt(
            (self.get_origin().x - self.get_termination().x) ** 2
            + (self.get_origin().y - self.get_termination().y) ** 2
        ) #Calls the getters here, rather than the instance vars in case
          # getter logic is added in the future

这样每次get()length 变量时都会执行额外的长度计算,而不是this_line_segment.origin.x = 1,你这样做:

new_origin = this_line_segment.get_origin()
new_origin.x = 1
this_line_segment.set_origin(new_origin)
print(this_line_segment.get_length())

(请注意,我在变量前面使用_ 表示它们是私有的,并且只能通过getter 和setter 访问。例如,变量length 永远不应该由用户设置——只能通过LineSegment 类。)

然而,显式的 getter 和 setter 显然是在 Python 中管理变量的笨拙方式,其中宽松的访问保护使直接访问它们更加透明。

选项 2:@property 装饰器

添加获取和设置逻辑的更 Pythonic 方式是 @property decorator,正如 @progmatico 在他们的评论中指出的那样,它在访问实例变量时调用修饰的 getter 和 setter 方法。由于我们需要做的就是在需要时计算长度,所以我们现在可以将其他实例变量公开:

class LineSegment:
    def __init__(
        self,
        origin,
        termination,
    ):
        self.origin = origin
        self.termination = termination
    
    # getter method for length
    @property
    def length(self):
        return math.sqrt(
            (self.origin.x - self.termination.x) ** 2
            + (self.origin.y - self.termination.y) ** 2
        )

及用法:

this_line_segment = LineSegment(origin=Point(x=0,y=0), 
                                termination=Point(x=1,y=1))
print(this_line_segment.length) # Prints 1.4142135623730951

this_line_segment.origin.x = 1
print(this_line_segment.length) # Prints 1.0

在 Python 3.7.7 中测试。

注意:我们必须在 length getter 中进行长度计算,而不是在初始化 LineSegment 时进行。因为Point 对象是可变的,并且改变它不会调用LineSegment 的setter 方法,所以我们不能在setter 方法中为origin 和termination 实例变量进行长度计算,因此也不能在初始化中进行。尽管我们可以在选项 1 中这样做,但它会导致一种反模式,在这种模式下,在实例变量相互依赖的情况下,我们必须为对象的每个实例变量重新计算 setter 中的每个其他实例变量。

【讨论】: