2

I would like to define a class that does something like:

Class computer():
    def __init__(self, x):
        # compute first the 'helper' properties
        self.prop1 = self.compute_prop1(x)
        self.prop2 = self.compute_prop2(x)
        # then compute the property that depends on 'helpers'
        self.prop3 = self.compute_prop3(x)

    def compute_prop1(self, x):
        return x
    def compute_prop2(self, x):
        return x*x
    def compute_prop3(self, x):
        return self.prop1 + self.prop2

Then, when I initialize an instance, I get all properties computed in order (first helpers, then everything depending on helpers later):

>>> computer = Computer(3)
>>> computer.__dict__
{'prop1': 3, 'prop2': 9, 'prop3': 12}

However, I think there is a better practice of writing this code, for example using decorators. Could you please give me some hints? Thank you!

Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
Lilianna
  • 337
  • 3
  • 12
  • 1
    Do note that maintaining the insertion order of dictionaries is only a language specification for Python 3.7+. Be careful not to rely on this if you will every be using an older version of Python. – Brian61354270 Feb 28 '20 at 16:04
  • 1
    If you later need `prop1` and `prop2` this code looks fine. If you do not, you could directly use them at call time like `self.prop = self.compute(self.compute_prop1(x), self.compute_prop2(x))` or `prop1 = ... ; prop2 = ...; self.prop = self.compute(prop1, prop2)` (semi colons should be newlines but code formatting in comments is limited...) – Serge Ballesta Feb 28 '20 at 16:11

2 Answers2

4

Here's your class using properties instead (with an added method for returning each property):

Class PropertyComputer:
    def __init__(self, x):
        self._x = x

    @property
    def prop1(self):
        return self._x

    @property
    def prop2(self):
        return self._x * self._x

    @property
    def prop3(self):
        return self.prop1 + self.prop2

    def get_props(self):
        return self.prop1, self.prop2, self.prop3

Design-wise, I believe this is better because:

  • storing x as an instance variable makes more sense: the point of using objects is to avoid having to pass variables around, especially those that the object itself can keep track of;
  • the attribute assignment and its corresponding calculation are bundled together in each property-decorated method: we'll never have to think whether the problem is in the init method (where you define the attribute) or in the compute method (where the logic for the attribute's calculation is laid out).

Note that the concept of "first calculate helpers, then the properties depending on them" does not really apply to this code: we only need to evaluate prop3 if/when we actually need it. If we never access it, we never need to compute it.

A "bad" side-effect of using properties, compared to your example, is that these properties are not "stored" anywhere (hence why I added the last method):

c = PropertyComputer(x=2)
c.__dict__  # outputs {'_x': 2}

Also note that, using decorators, the attributes are calculated on-the-fly whenever you access them, instead of just once in the init method. In this manner, property-decorated methods work like methods, but are accessed like attributes (it's the whole point of using them):

c = PropertyComputer(x=2)
c.prop1  # outputs 2
c._x = 10
c.prop1  # outputs 10

As a side note, you can use functools.cached_property to cache the evaluation of one of these properties, in case it's computationally expensive.

jfaccioni
  • 7,099
  • 1
  • 9
  • 25
0

I think the following would be the easiest way to avoid redundancy

class computer():
    def __init__(self, x):
        self.prop_dict = self.compute_prop_dict(x)

    def compute_prop_dict(self, x):
        prop1 = x
        prop2 = x*x
        return {'prop1': prop1, 'prop2': prop2, 'prop3': prop1 + prop2}

So anything that would come after instantiation could have access to these helpers via the prop_dict

But as said by Brian as a comment this order is just a language specification for Python 3.7

animalknox
  • 138
  • 9