1

Let's say I have the following classes:

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

An object of the LineSegment class is composed of two objects of the Point class. Now, let's say I initialize an object as so:

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

Note: The initialization of the line segment automatically calculates its length. This is critical to other parts of the codebase, and cannot be changed. I can see its length like this:

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

Now, I need to mutate one parameter of this_line_segment's sub-objects:

this_line_segment.origin.x = 1

However, the this_line_segments length attribute does not update based on the new origin's x coordinate:

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

What is the pythonic way to implement updating a class's attributes when one of the attributes they are dependent upon changes?

wingedNorthropi
  • 149
  • 2
  • 16
  • 2
    Define `length` as a property and update/calculate it on read access. See an example [here](https://stackoverflow.com/questions/49517010/descriptors-in-effective-python/49558351#49558351) that validates data on writing. It is similar but you put your calculation code on the property getter instead of the setter. – progmatico Jun 20 '21 at 16:20

1 Answers1

1

Option 1: Getter and Setter Methods

In other object-oriented programming languages, the behavior you desire, adding additional logic when accessing the value of an instance variable, is typically implemented by "getter" and "setter" methods on all instance variables in the object:

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

So that the extra length calculation is performed every time you get() the length variable, and instead of this_line_segment.origin.x = 1, you do:

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

(Note that I use _ in front of variables to denote that they are private and should only be accessed via getters and setters. For example, the variable length should never be set by the user--only through the LineSegment class.)

However, explicit getters and setters are clearly a clunky way to manage variables in Python, where the lenient access protections make accessing them directly more transparent.

Option 2: The @property decorator

A more Pythonic way to add getting and setting logic is the @property decorator, as @progmatico points out in their comment, which calls decorated getter and setter methods when an instance variable is accessed. Since all we need to do is calculate the length whenever it is needed, we can leave the other instance variables public for now:

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
        )

And usage:

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

Tested in Python 3.7.7.

Note: We must do the length calculation in the length getter and not upon initialization of the LineSegment. We can't do the length calculation in the setter methods for the origin and termination instance variables and thus also in the initialization because the Point object is mutable, and mutating it does not call LineSegment's setter method. Although we could do this in Option 1, it would lead to an antipattern, in which we would have to recalculate every other instance variable in the setter for each instance variable of an object in the cases for which the instance variables depend on one another.

abeta201
  • 233
  • 1
  • 14