justin
justin

Reputation: 181

attributes that modify other attributes

I would like to make a Block class that has the following behavior: if iip3 is set, then oip3 is set to iip3 + gain. If oip3 is set, then iip3 is set to oip3 - gain like the following:

b = Block(gain = 10)
Block(gain=10, iip3=inf, oip3=inf)

b.oip3 = 30
Block(gain=10, iip3=20, oip3=30)

b.iip3 = 21
Block(gain=10, iip3=21, oip3=31)

I've tried this with the attrs module:

import attr
import numpy as np

def set_oip3(instance, attribute, iip3):
    instance.oip3 = iip3 + instance.gain
    return iip3

def set_iip3(instance, attribute, oip3):
    instance.iip3 = oip3 - instance.gain
    return oip3

@attr.s
class Block:

    gain = attr.ib()
    iip3 = attr.ib(on_setattr=set_oip3, default=np.inf)
    oip3 = attr.ib(on_setattr=set_iip3, default=np.inf)

However, this doesn't work. I suspect that a call to set_oip3 results in a call to set iip3 which in turn calls set oip3.

There must be a solution using attrs.

Upvotes: 1

Views: 321

Answers (2)

hynek
hynek

Reputation: 4116

As others said, it's not a great idea to mutate attributes when on attribute change. But if you insist, you can circumvent on_setattr hooks by using object.__setattr__ – that's something attrs does internally too:

import attr
import numpy as np

def set_oip3(instance, attribute, iip3):
    object.__setattr__(instance, "oip3", iip3 + instance.gain)
    return iip3

def set_iip3(instance, attribute, oip3):
    object.__setattr__(instance, "iip3", oip3 - instance.gain)
    return oip3

@attr.define
class Block:

    gain = attr.ib()
    iip3 = attr.ib(on_setattr=set_oip3, default=np.inf)
    oip3 = attr.ib(on_setattr=set_iip3, default=np.inf)

Works as you asked for:

In [9]: b = Block(gain = 10)

In [10]: b
Out[10]: Block(gain=10, iip3=inf, oip3=inf)

In [11]: b.oip3 = 30

In [12]: b
Out[12]: Block(gain=10, iip3=20, oip3=30)

In [13]: b.iip3 = 21

In [14]: b
Out[14]: Block(gain=10, iip3=21, oip3=31)

Upvotes: 2

ddejohn
ddejohn

Reputation: 8962

You can use the @property decorator:

import numpy as np


class Block:
    def __init__(self, gain=10, iip3=np.inf, oip3=np.inf):
        self.gain = gain
        self._iip3 = iip3
        self._oip3 = oip3

    def __repr__(self):
        return f"Block(gain={self.gain}, iip3={self.iip3}, oip3={self.oip3})"

    @property
    def iip3(self):
        return self._iip3

    @iip3.setter
    def iip3(self, val):
        self._iip3 = val
        self._oip3 = self._iip3 + self.gain

    @property
    def oip3(self):
        return self._oip3

    @oip3.setter
    def oip3(self, val):
        self._oip3 = val
        self._iip3 = self._oip3 - self.gain

Usage:

In [2]: b = Block()

In [3]: b.iip3 = 21

In [4]: b
Out[4]: Block(gain=10, iip3=21, oip3=31)

In [5]: b.oip3 = 30

In [6]: b
Out[6]: Block(gain=10, iip3=20, oip3=30)

For what it's worth, it's not a great idea to have a mutator produce side effects on attributes other than the ones a given mutator is supposed to be modifying.

Upvotes: 2

Related Questions