Search code examples
pythonclassinstance-variables

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!


Solution

  • 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.