pythonic class instance attribute calculated from other attributes
Asked Answered
G

3

20

I have a class instance with attributes that are calculated from other attributes. The attributes will change throughout the life of the instance. All attributes are not necessarily defined when the object is initialized.

what is the pythonic way to calculate attributes from other attributes?

This is a simple example, the calculations have numerous input variables ("a" below) and calculations ("b" & "c").

a = something
b = function of a (a+5)
c = function of a and b (a*b)

I've tried numerous implementations. Here is a decent one to communicate my intention.

 class CalcAttr(object):

    def __init__(self):
        self._a = None
        self._b = None
        self._c = None

    @property
    def a(self):
        return self._a
    @a.setter
    def a(self,value):
        self._a = value

    @property
    def b(self):
        self.calc_b()
        return self._b

    @property
    def c(self):
        self.calc_c()
        return self._c


    def calc_b(self):
        self._b = self._a + 5

    def calc_c(self):
        self._c = self._a * self._b

def test():
    abc = CalcAttr()
    a = 5
    return abc.c

Note: t.c works if I first call t.b first.

> >>> t=abc.test()
> >>> t.c Traceback (most recent call last):   File "<stdin>", line 1, in <module>   File "abc.py", line 22, in c
>     self.calc_c()   File "abc.py", line 29, in calc_c
>     self._c = int(self._a) * int(self._b) TypeError: int() argument must be a string or a number, not 'NoneType'
> >>> t.b 10
> >>> t.c 50
> >>>

Keep in mind most of the real calculations are dependent on multiple attribures (5-10 input variables & as many calculated ones).

My next iteration will include a "calculate_model" function that will populate all calculated attributes after checking that all inputs are defined. Maybe that will be the pyhonic answer?

Thanks!

Update - working solution

I created a method that calculates each attribute in order:

def calc_model(self):
    self.calc_b()
    self.calc_c()

Each calculated attribute calls that method

@property
def c(self):
    self.calc_model()
    return self._c

I'm not sure if this is proper, but it works as desired...

Grosbeak answered 30/11, 2015 at 3:37 Comment(2)
What's the question?Marcellmarcella
Updated to make it more clear.Grosbeak
M
31

If I understand your question correctly, you should compute b and c in their getters. You should also probably require that the user passes a value for a in the initializer, since b and c can't be computed without a. Also, it doesn't seem like there is much of a reason to keep _a, _b, and _c around -- unless b and c are expensive to compute and you'd like to cache them.

For example:

class CalcAttr(object):

    def __init__(self, a):
        self.a = a

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

    @property
    def c(self):
        return self.a * self.b

Such that

>>> x = CalcAttr(42)
>>> x.c
1974
Marcellmarcella answered 30/11, 2015 at 4:4 Comment(1)
Thanks for your direction. I read my code as making the calculations in the getter (through a separate function). Initializing the values won't work, they are appended at different times throughout the life of the object. I'll work with what you posted, thank you!Grosbeak
M
0

I understand what @jme suggested in the accepted answer is more elegant, but I still try to fix the original example and get it to work. Here is the code.

class CalcAttr(object):

   def __init__(self):
       self._a = None
       self._b = None
       self._c = None

   @property
   def a(self):
       return self._a
   @a.setter
   def a(self,value):
       self._a = value   

   @property
   def b(self):
       self.calc_b()
       return self._b

   @property
   def c(self):
       self.calc_c()
       return self._c

   def calc_b(self):
       self._b = self._a + 5
    
   def calc_c(self):
       self._c = self.a * self.b

def test():
    abc = CalcAttr()
    abc.a = 5
    return abc.c

test()

The code will work and 50 is the resulted value.

Midcourse answered 27/3, 2022 at 9:34 Comment(0)
R
0

I needed this a number of times without so much boilerplate so I attempted to create a solution for it. @computed_property can be used like:

class CalcAttr(object, has_computed_properties):
    a: int

    @computed_property
    def b(self):
        return self.a + 5

    @computed_property
    def c(self):
        return self.a * self.b
# computed_tests.py

class TestComputedProperty(unittest.TestCase):
    def test_so_issue(self):
        so = CalcAttr()
        so.a = 5
        self.assertEqual(so.b, 10)
        self.assertEqual(so.c, 50)

if __name__ == '__main__':
    unittest.main()
$ python3 computed_tests.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK
Reachmedown answered 21/1, 2024 at 9:52 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.