sccrthlt
sccrthlt

Reputation: 4344

One instance attribute referencing another, with updates after class instantiation

I am looking for best practices on setting one instance attribute that references another instance attribute after the class has been instantiated.

For example:

class Foo:
    def __init__(self):
        self.a = 1
        self.b = self.a + 1
>>> obj_foo = Foo()
>>> obj_foo.a
1
>>> obj_foo.b
2
>>> obj_foo.a = 5
>>> obj_foo.a
5
>>> obj_foo.b
2 # I want this to be 6

Is this bad practice for one instance attribute to reference another?

I can see how implementing a method to check for and update dependent instance attributes, but this seems like a lot of overhead/hacky. Any assistance is greatly appreciated!

Upvotes: 2

Views: 72

Answers (1)

Mad Physicist
Mad Physicist

Reputation: 114320

It seems like you don't actually want to store the value of b at all, but instead want to generate it based on the value of a dynamically. Luckily, there's a property class/decorator that you can use just for this purpose:

class Foo:
    def __init__(self, a=1):
        self.a = a

    @property
    def b(self):
        return self.a + 1

This will create a read-only property b that will behave just like a normal attribute when you access it as foo.b, but will a because it is a descriptor. It will re-compute the value based on whatever foo.a is set to.

Your fears about calling a method to do the computation every time are not entirely unjustified. Using the . operator already performs some fairly expensive lookups, so your toy case is fine as shown above. But you will often run into cases that require something more than just adding 1 to the argument. In that case, you'll want to use something like caching to speed things up. For example, you could make a into a settable property. Whenever the value of a is updated, you can "invalidate" b somehow, like setting a flag, or just assigning None to the cached value. Now, your expensive computation only runs when necessary:

class Foo:
    def __init__(self, a=1):
        self._a = a

    @property
    def a(self):
        return self._a

    @a.setter
    def a(self, value):
        self._a = value
        self._b = None

    @property
    def b(self):
        if self._b is None:
            # Placeholder for expensive computation here
            self._b = self._a + 1
        return self._b

In this example, setting self.a = a in __init__ will trigger the setter for the property foo.a, ensuring that the attribute foo._b always exists.

Upvotes: 6

Related Questions