Is there a way to += and -= of an attribute call a instance's method?

Question:

Question: Is there a way to (+= and -=) call (insert_knot and remove_knot) in the class bellow?

EDIT: It’s not duplicated from this question, cause I’m changing an attribute of the class (summing and subtracting), not the instance by itself. I mean, I sum/subtract an attribute and a method of the instance should be called.

class SplineCurve:
    def __init__(self, knot_vector: Tuple[float]):
        self.knot_vector = knot_vector

    @property
    def degree(self) -> int:
        return self.__degree

    @property
    def knot_vector(self) -> Tuple[float]:
        return tuple(self.__knot_vector)

    @degree.setter
    def degree(self, new_value: int):
        if new_value == self.degree:
            return
        if new_value > self.degree:
            self.increase_degree(new_value - self.degree)
        else:
            self.decrease_degree(self.degree - new_value)
        self.__degree = new_value

    @knot_vector.setter
    def knot_vector(self, new_value: Tuple[float]):
        new_value = list(new_value)
        new_value.sort()  # Shouldn't be here, only in 'insert_knot'
        self.__knot_vector = tuple(new_value)
            
    def increase_degree(self, times: int):
        self.compute_ctrl_points()

    def decrease_degree(self, times: int):
        self.compute_ctrl_points()

    def insert_knot(self, knots: Tuple[float]):
        print(f"Inserting knots {knots}")
        new_knot_vector = list(self.knot_vector)
        for knot in knots:
            new_knot_vector += [knot]
        new_knot_vector.sort()
        self.knot_vector = new_knot_vector
        self.compute_ctrl_points()

    def remove_knot(self, knots: Tuple[float]):
        print(f"Removing knots {knots}")
        new_knot_vector = list(self.knot_vector)
        for knot in knots:
            new_knot_vector.remove(knot)
        self.knot_vector = new_knot_vector
        self.compute_ctrl_points()

    def compute_ctrl_points(self):
        print("I must be called on insertion and remotion")

Then I want to the user do it:

mycurve = SplineCurve([0, 0, 1, 1])
print(mycurve.knot_vector)         # (0, 0, 1, 1)
mycurve.knot_vector += (0.5, 0.5)  # This line should called as 'mycurve.insert_knot((0.5, 0.5))'
print(mycurve.knot_vector)         # (0, 0, 0.5, 0.5, 1, 1)
mycurve.knot_vector -= (0.5, 1)    # This line should called as 'mycurve.remove_knot((0.5, 1))'
print(mycurve.knot_vector)         # (0, 0, 0.5, 1)

For inserting knot, the printed value is correct, but the function insert_knot (and Inserting knots ... is not printed).

But for -= gives the error TypeError: unsupported operand type(s) for -=: 'tuple' and 'tuple', which is normal, cause it’s like

temp_value = mycurve.knot_vector - (0.5, 1)  # Error here
mycurve.knot_vector = temp_value  # Setter is only called here

Calling the argument on insert_knot and remove_knot should allow also:

mycurve.knot_vector += numpy.array([0.3, 0.7])
mycurve.knot_vector += [0.4, 0.9]
Asked By: Carlos Adir

||

Answers:

The solution as proposed @interjay is to create a new class object that can sum with an iterable.

Then there’s two possibilities:

  1. Call the curve.insert_knot on __iadd__ and curve.remove_knot on __isub__ using indirect reference, and then lose the curve’s reference. We can return None and curve.knot_vector setter does nothing.
class KnotVector(Tuple):
    def __new__(cls, curve, args: tuple[float]):
        self = super(KnotVector, cls).__new__(cls, tuple(args))
        self._curve = curve
        return self
    
    def __iadd__(self, knots: tuple[float]):
        if self._curve is not None:
            self._curve.insert_knot(knots)
            self._curve = None
        else:
            return tuple(self) + knots

    def __isub__(self, knots: tuple[float]):
        if self._curve is not None:
            self._curve.remove_knot(knots)
            self._curve = None
        else:
            return tuple(self) + knots

class SplineCurve:
    ...
    @property
    def knot_vector(self) -> KnotVector:
        return KnotVector(self, tuple(self._knot_vector))

    @knot_vector.setter
    def knot_vector(self, new_knot_vector: Tuple[float]):
        if new_value is None:
            return
        self._knot_vector = tuple(new_knot_vector)
    ...

A problem that can happen is assigning knot_vector into a variable myvector, and then it changes mycurve without noticing.

mycurve = SplineCurve([0, 0, 1, 1])
myvector = mycurve.knot_vector
# ... after many instructions
myvector += (0.5, 0.5)  # This will change mycurve
  1. Modify in KnotVector class to add and remove, and give the information to mycurve through knot_vector setter, the same way that happens for degree:
class KnotVector(Tuple):
    def __new__(cls, args: tuple[float]):
        return super(KnotVector, cls).__new__(cls, tuple(args))
    
    def __iadd__(self, knots: tuple[float]):
        new_knot_vector = list(self)
        for knot in knots:
            new_knot_vector += [knot]
        new_knot_vector.sort()
        return tuple(new_knot_vector)

    def __isub__(self, knots: tuple[float]):
        new_knot_vector = list(self)
        for knot in knots:
            new_knot_vector.remove(knot)
        return tuple(new_knot_vector)

class SplineCurve:
    ...

    @property
    def knot_vector(self) -> KnotVector:
        return KnotVector(self._knot_vector)

    @knot_vector.setter
    def knot_vector(self, new_knot_vector: Tuple[float]):
        if self._knot_vector == new_knot_vector:
            return
        if self.new_knots_inserted(new_knot_vector):
            self.insert_knot(self.get_new_knots(new_knot_vector))
        else:
            self.remove_knot(self.get_removed_knots(new_knot_vector))
    ...
Answered By: Carlos Adir
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.