1

I'm trying to make a class with 2 or more mutually dependent attributes (e.g. a and a-plus-1). The idea is to be able to initialize an object with one and only one of these 2 attributes and the other is updated simutaneously. Here is a minimal example:

class Duo:
    def __init__(self, a=None, ap1=None):
        if (a is None) == (ap1 is None): 
            raise Exception('ERROR: must initialize with either a or ap1.')
        elif a:
            self.a = a
            self.ap1 = self.a + 1
        else:
            self.ap1 = ap1
            self.a = self.ap1 - 1

This works except a and ap1 will not auto-update if one of them is modified. I checked out this anwser which talks about how to dynamically add property to class, and I came up with the following naive solution:

class Duo:
    def __init__(self, a=None, ap1=None):
        if (a is None) == (ap1 is None): 
            raise Exception('ERROR: must initialize with either a or ap1.')
        elif a:
            Duo.a = property(lambda self: a)
            Duo.ap1 = property(lambda self: self.a + 1)
        else:
            Duo.ap1 = property(lambda self: ap1)
            Duo.a = property(lambda self: self.ap1 - 1)

if __name__ == '__main__':
    duo_1 = Duo(a=1)
    print(f'a = {duo_1.a}, a + 1 = {duo_1.ap1}') # This line prints "a = 1, a + 1 = 2"

    duo_2 = Duo(ap1=100)
    print(f'a = {duo_2.a}, a + 1 = {duo_2.ap1}') # This line prints "a = 99, a + 1 = 100"
    print(f'a = {duo_1.a}, a + 1 = {duo_1.ap1}') # This line prints "a = 99, a + 1 = 100" but "a = 1, a + 1 = 2" expected

This doesn't work either since a and ap1 now become class properties and get changed whenever a new instance is created.

Is there a way to make both a and ap1 auto-updating instance properties?

Xiasu Yang
  • 69
  • 5
  • 1
    Do they have to be dynamically defined? – matszwecja Feb 28 '23 at 14:54
  • 2
    How about making them both ordinary properties, and saving just one consistent attribute that both getters read from? – Brian61354270 Feb 28 '23 at 14:54
  • The whole point of a `property` is that it's a class attribute with special behavior when accessed via an instance. The getter is a function that will be passed an instance of `Duo` when called, even though `Duo.a` itself is a class attribute. – chepner Feb 28 '23 at 15:02

2 Answers2

3

Do you really need dynamically defined attributes? Why not define a single one and then return value with static properties?

class Duo:
    def __init__(self, a=None, ap1=None):
        if (a is None) == (ap1 is None): 
            raise Exception('ERROR: must initialize with either a or ap1.')
        elif a:
            self._a = a
        else:
            self._a = ap1 - 1

    @property
    def a(self):
        return self._a
    @property
    def ap1(self):
        return self._a + 1
    
a = Duo(10)
print(a.a)
print(a.ap1)

For the sake of it working two-ways (setting as well as getting), you also need setters:

    @a.setter
    def a(self, value):
        self._a = value
    @ap1.setter
    def ap1(self, value):
        self._a = value - 1
    
a = Duo(10)
print(a.a) # 10 
print(a.ap1) # 11
a.a = 5
print(a.a) # 5
print(a.ap1) # 6
a.ap1 = 15
print(a.a) # 14
print(a.ap1) # 15
matszwecja
  • 6,357
  • 2
  • 10
  • 17
0

In addition to the properties that allow each attribute change to affect the other, I would suggest restricting the creation of an instance to the use of one of two class methods, rather than using the class directly.

class Duo:
    def __init__(self, value):
        self.a = value

    @classmethod
    def from_a(cls, value):
        return cls(value)

    @classmethod
    def from_ap1(cls, value):
        return cls(value - 1)

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

    # @a.setter
    # def a(self, v):
    #    self._a = v      

    @property
    def ap1(self):
        return self._a + 1

    @ap1.setter
    def ap1(self, v):
        self._a = v - 1


d1 = Duo.from_a(5)
assert d1.a == 5 and d1.ap1 == 6

d1.a = 9
assert d1.a == 9 and d1.ap1 == 10

d1.ap1 = 8
assert d1.a == 7 and d1.ap1 == 8

Only one of the two properties needs to be a property object; the other can be a regular instance attribute as shown above. Here's the same class with ap1 stored explicitly and a computed dynamically from its value.

class Duo:
    def __init__(self, value):
        self.ap1 = value

    @classmethod
    def from_a(cls, value):
        return cls(value+1)

    @classmethod
    def from_ap1(cls, value):
        return cls(value)

    # @property
    # def ap1(self):
    #    return self._ap1

    # @ap1.setter
    # def a(self, v):
    #    self._ap1 = v      

    @property
    def a(self):
        return self._apa - 1

    @a.setter
    def a(self, v):
        self._ap1 = v + 1
chepner
  • 497,756
  • 71
  • 530
  • 681