0

I have a class with 3 properties that depend on each other with the following equation: c = a * b. If any 2 properties are given it should be able to set all 3.

Up to now I came up with the solution below, but I'm pretty sure it can be solved nicer. There is multiple solutions that show how to define dependent properties one way (like here), but I don't see how to extend it to my case, and be able to efficiently set any pair (a,b or b,c or a,c), and calculating the last one. Do I need to maybe look into HasTrait like here?

My idea below has a prop(**kwargs) function that always sets all values given any 2 of them. So it should only be called as one of the following three ways: prop(_a = a, _b = b), prop(_b = b, _c = c), prop(_a = a, _c = c) with a, b, c any number.

Ideally I'd want to call prop() with the keys, the name of the actual properties a, b , c instead of _a, _b, _c. In my implementation below it'll not work as the setters for a, b, c call prop() again and cause an infinite loop.

class Example:
    # a * b = c

    def __init__(self, **kwargs):
        if len(kwargs) == 2: # need to specify at least 2 properties
            self.prop(**kwargs)
        else:
            self._a = self._b = self._c = None
            print('Correctly set properties manually using prop function')

    def prop(self, **kwargs):
        # needs to always set 2 properties out of a, b, c calculate the last one
        for key, value in kwargs.items():
            setattr(self, key, value)
        if '_a' not in kwargs:
            self._a = self.c / self.b
        elif '_b' not in kwargs:
            self._b = self.c / self.a
        elif '_c' not in kwargs:
            self._c = self.b * self.a

    @property
    def a(self):
        return self._a

    @a.setter
    def a(self, value):
        self.prop(_a = value, _b = self.c / value) # set a holding c constant

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

    @b.setter
    def b(self, value):
        self.prop(_b = value, _a = self.c / value) # set b holding c constant

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

    @c.setter
    def c(self, value):
        self.prop(_c = value, _a = value / self.b) # set c holding b constant
Community
  • 1
  • 1
  • What happens if, for example, I set a and b. Then I set c to an incompatible value? – aghast Feb 05 '17 at 20:40
  • This logic is rather random. If you set `c`, how do you decide whether to adjust `a` or `b`? – user2390182 Feb 05 '17 at 21:02
  • If you set `a` and `b` then `c` should be set to `a*b`. If you set `b` and `c` then `a` should be set to `c/b`. If you set `a` and `c` then `b` should be set to `c/a`. The individual set properties keep one of the other values constant (see inline comments) so that the equation `a*b=c`, is always true. – Ron Schutjens Feb 05 '17 at 23:33

1 Answers1

0

I tried your code, and it doesn't work. Results:

$ python test.py 
Traceback (most recent call last):   
  File "test.py", line 38, in <module>
    ex.a = 3   
  File "test.py", line 19, in a
    self.prop(_a = value, _b = self.c / value) # set a holding c constant   
  File "test.py", line 31, in c
    return self._c 
AttributeError: 'Example' object has no attribute '_c'

So let's try again. First, same class name, a docblock, and a constructor that sets _mra as well as any values that the caller passes in.

class Example:
    """Define a class with three attributes, a, b, and c, such that
    c = a * b defines the relationship among them.

    In the event of a conflict, the two most recently set attributes
    determine the value of the third.
    """

    def __init__(self, **kwargs):
        self._a = self._b = self._c = None
        self._mra = []
        if 'a' in kwargs:
            self.a = kwargs['a']
        if 'b' in kwargs:
            self.b = kwargs['b']
        if 'c' in kwargs:
            self.c = kwargs['c']

And a printout method for inspecting the action:

    def __repr__(self):
        return "Example(_a={self._a!r}, _b={self._b!r}, _c={self._c!r})".format(self=self)

Next, a bunch of dummy getter properties. For some reason, @property has to be on a separate line from the def, or I'd glom them together.

    @property
    def a(self): return self._a
    @property
    def b(self): return self._b
    @property
    def c(self): return self._c

Now, the setter properties. Instead of copying and pasting code, let's just pass everything into a single _update_values method.

    @a.setter
    def a(self, value): self._update_values(_a=value)
    @b.setter
    def b(self, value): self._update_values(_b=value)
    @c.setter
    def c(self, value): self._update_values(_c=value)

And now, for the meat of the matter. I assume that properties could be set to a value, or to the value None. This is the default value for properties, so that isn't too hard to understand. But when you try to do math involving None, an exception is raised! So there has to be a try-block around the math.

The idea is that self._mra (short for "most recent attributes") is a queue of the attribute names that were most recently set. If you set an attribute, it moves to the back of the queue. When updating the object, the two most recently updated attributes are used to determine the third. Thus, if I set A,B,C, the when A,B are set C is computed, and when C is set B is used to compute A, because B,C are the most recently set attributes. It's an arbitrary rule, but consistent.

    def _update_values(self, **kwargs):
        for k,v in kwargs.items():
            try: self._mra.remove(k)
            except ValueError: pass
            self._mra.append(k)
            setattr(self, k, v)

        if len(self._mra) <= 1:
            return

        last2 = ''.join(self._mra[-2:])
        try:
            if last2 == '_a_b' or last2 == '_b_a':
                self._c = self._a * self._b
            elif last2 == '_a_c' or last2 == '_c_a':
                self._b = self._c / self._a
            elif last2 == '_b_c':
                self._a = self._c / self._b
            else:
                raise Exception("I didn't see THIS coming! Last2 = %s" % last2)
        except TypeError:
            # Something was set to None. No math for you!
            pass

Finally, some test code. Note that I'm using python3, and the division operation tends to make everything a floating point number. You can address that by using // floor division, or ... not.

ex = Example()
print(repr(ex))       # Example(_a=None, _b=None, _c=None)
ex.a = 3
print(repr(ex))       # Example(_a=3, _b=None, _c=None)
ex.b = 5
print(repr(ex))       # Example(_a=3, _b=5, _c=15)
print('c = ', ex.c)   # c =  15
ex.c = 40
print(repr(ex))       # Example(_a=8.0, _b=5, _c=40)
print('a = ', ex.a)   # a =  8.0

Output:

$ python test.py
Example(_a=None, _b=None, _c=None)
Example(_a=3, _b=None, _c=None)
Example(_a=3, _b=5, _c=15)
c =  15
Example(_a=8.0, _b=5, _c=40)
a =  8.0
aghast
  • 14,785
  • 3
  • 24
  • 56
  • He Austin, thanks for the help. I'll have to wait for tomorrow to look through it though. I'll let you know then!I will update my own example to make sure it is a working one too. The proper way to set the properties is to always set at least 2 of the 3. For individual property setters I make sure it is always consistent by keeping 1 of the other 2 constant (see inline comments). – Ron Schutjens Feb 05 '17 at 23:37
  • He Austin, your implementation does a lot more than I want to allow : ). I've updated the question and added a working example. I'm looking for a way to set the properties as described in paragraph 3 and 4. Hopefully it is more clear now. – Ron Schutjens Feb 07 '17 at 03:32