Reputation: 1544
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...
Upvotes: 20
Views: 15362
Reputation: 4252
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
Upvotes: 0
Reputation: 490
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.
Upvotes: 0
Reputation: 20705
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
Upvotes: 32