1

Newbie Python questions: I want to model something that can have a several inter-connected instance variables and if I set one, I want to recalculate the others. Any can be set.

E.g. a circle - each instance has an area, circumference and radius (let's ignore the diameter). If I set the area, I want the circumference & radius re-calculated and vice-versa. Should I :

class circle:
 def area(self):
 return cmath.pi*self.radius*self.radius
 def circumference(self):
 return 2 * cmath.pi * self.radius
 def radius(self):
 return self.circumference/(2*cmath.pi)
circle1 = circle()
circle1.radius = 5
print "Circle1's area is {0} and its circumference is {1}".format(circle1.area, circle1.circumference) 

Now there's nothing in the code sample to calculate the area once the radius has been set, so do I need @property with setters and getters for the radius, area and circumference to calculate the other attributes when this one is set or am I barking up completely the wrong tree?

asked May 11, 2015 at 14:45

2 Answers 2

5

Instead of keeping 3 member fields and synchronizing them, you only need to store one actual value in memory - the radius is a good choice in this example - and always use it:

import cmath
class Circle(object):
 def __init__(self, radius=0.0):
 self._radius = radius
 @property
 def radius(self):
 return self._radius
 @radius.setter
 def radius(self, radius):
 self._radius = float(radius)
 @property
 def area(self):
 return cmath.pi * self._radius * self._radius
 @area.setter
 def area(self, area):
 self._radius = (area / cmath.pi) ** 0.5
 @property
 def circumference(self):
 return 2 * cmath.pi * self._radius
 @circumference.setter
 def circumference(self, circumference):
 self._radius = circumference / cmath.pi / 2
circle1 = Circle()
circle1.radius = 5
print "Circle1's radius is {0}, it's area is {1} and it's circumference is {2}".format(circle1.radius, circle1.area, circle1.circumference)
circle1.area = 5
print "Circle1's radius is {0}, it's area is {1} and it's circumference is {2}".format(circle1.radius, circle1.area, circle1.circumference)
circle1.circumference = 5
print "Circle1's radius is {0}, it's area is {1} and it's circumference is {2}".format(circle1.radius, circle1.area, circle1.circumference)

This solution seems too obvious though - is there a specific reason you want to have separate member fields? You can usually find a smaller number of values you can calculate the rest from...

Jace Browning
2,2351 gold badge16 silver badges25 bronze badges
answered May 11, 2015 at 16:32
7
  • Hi Idan - yes, this is a simplified case. I'm actually modelling a file allocation on an IBM mainframe. The file has a 'block size' (blksize) attribute which determines how much space the file actually occupies on disk. Changing the blksize changes how many tracks of disk space the file uses. So the file will have blksize and numTracks attributes from which you can calculate the number of bytes in the file. However, you might wish to reallocate the file with a different blksize and want to know how many tracks it'll take up with the new blksize. Commented May 11, 2015 at 19:57
  • Sounds like you'll want to keep numBytes(or however you want to call it) - since that's the actual size of the file visible to the users - and blksize, since that's a configuration, and calculate numTracks from them whenever it is queried. If you want a setter for numTracks, it should only modify blksize since it makes more sense to do that than to trim the actual data... Commented May 11, 2015 at 22:40
  • 1
    Idan - a better analogy might have been a rectangle, with width and height and area. Change one whilst keeping another constant and you'll have to recalculate things. I guess I'm really asking if using setters and getters is the accepted Python way of doing things? I.e. In the setter for one property, recalculate any dependant properties? I guess I'd have to be careful not to get into a lops, if the setter for A recalculated B and the setter for B recalculated A? Commented May 12, 2015 at 8:47
  • This is a question of design. Getters are easy, but you need to decide which attributes should have setters and what these setters should do. In the case of the rectangle, for example, you need to decide if the area setter should change the width only, the height only, both while keeping proportion or whatever formula you want - and it's also acceptable to say it doesn't have a setter. But keeping the 3 attributes attributes as actual member fields is not a good idea. Encapsulation allows you to keep consistency, but why bother when you can avoid the possibility of inconsistencies altogether? Commented May 12, 2015 at 9:44
  • 1
    A member field is property that's actually stored in memory as part of the object(instead of being calculated on the spot). I don't know how "Pythonic" calculated properties are, but they are part of Python, with the built-in @property decorator, and I see no reason not to use them. Commented May 12, 2015 at 14:18
0

A fool-proof and robust way of dealing with dependent attributes is to use my small pure-python module exactly for handling acyclic dependencies in python class attributes, also known as dataflow programming.

Sometimes it is not feasible to simply update all attributes when one is changed. Ideally, changing an attribute doesn't cause any updates. Only getting an attribute should cause updates, and only the required updates.

That is exactly what the module does. It is very easy to implement in any class definition.

For instance, consider an attribute dependency structure shown below:

enter image description here

This dependency structure is completely defined using the library's descriptors:

 class DataflowSuccess():
 # The following defines the directed acyclic computation graph for these attributes.
 a1 = IndependentAttr(init_value = 1, name = 'a1')
 a2 = DeterminantAttr(dependencies = ['a1'], calc_func = 'update_a2', name = 'a2')
 a3 = DeterminantAttr(dependencies = ['a2'], calc_func = 'update_a3', name = 'a3')
 a4 = DeterminantAttr(dependencies = ['a1','a2'], calc_func = 'update_a4', name = 'a4')
 a5 = DeterminantAttr(dependencies = ['a1','a2','a3','a6'], calc_func = 'update_a5', name = 'a5')
 a6 = IndependentAttr(init_value = 6, name = 'a6')
 a7 = DeterminantAttr(dependencies = ['a4','a5'], calc_func = 'update_a7', name = 'a7')
 # ...... define the update functions update_a2, update_a3 etc
 def update_a2():
 ....

The module takes care of the rest. Changing an attribute causes its children (attributes it affects) (and their children, etc) to know their values need to be recalculated in the future. When the value of an attribute is requested by calling __get__ (for instance by executing obj.a5), only the required updates occur, all automatically behind the scenes.

If it doesn't suit your needs, at least it is a helpful example of how the descriptor functionalities work. This example is available at the github page. Descriptors provide lots of flexibility for modifying how __get__ and __set__ and __del__ work for your class attributes, which can solve dependency problems.

answered Jan 16, 2019 at 9:45

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.