Martin Martin - 15 days ago 6
Python Question

Keep changes to an object temporary during method chaining

I am designing an object-oriented data structure that shall be simple to handle from the user's perspective, e.g., by method chaining (aka Fluent interface). However, each change should only act temporarily on the object: within that chain, but not beyond that chain.

Here is a simplified example that does not work as intended:

class C:
def __init__(self, num):
self.sum = num

def add(self, num=0):
self.sum += num
return self

number = C(23)
number.add(7).add(12).sum
# 42
number.sum
# 42 (but I expect: 23)


In this case,
.add()
makes permanent changes to
number
. However, permanent changes should only be possible like this:

# This is the only way how a user should make permanent changes:
number = number.add(4).add(12)


In the temporary scope, I am looking for a way to get back to the old version of
number
after the chain is terminated. On the edge of despair, I can think of ugly solutions like "instance replication":

class C2:
def __init__(self, num):
self.sum = num

def add(self, num=0):
self2 = C2(self.sum) # or equivalently: self2 = copy.deepcopy(self)
self2.sum += num
return self2

number = C2(23)
number.add(7).add(12).sum
# 42
number.sum
# 23


However, the actuall classes and objects with which I am working contain a huge amount of data, attributes, methods, and even subclasses. So we should avoid copying the instance in every single method, besides the fact that it involves ugly code.

Are there ways to solve this problem, e.g. by (silently) creating a single copy only once at the first element of the chain? Or by destroying any changes made at the end of the chain? (Note that real-world "changes" involve many different, commutable methods other than just adding numbers)

An accepted solution should perform the necessary operations internally, i.e., without bothering the user interface:

# These are NOT accepted solutions:
number.copy().add(4).add(12)
number.add(4).add(12).undo()


If there no direct solution other than self-replication, the question would be: What is the most elegant way to do it that sustains code-readability and keeps memory usage low? E.g., decorating every class method by self-replicating function?

Answer

Instead of modyfing the object on which you call the method, return a modified copy:

class C:
    def __init__(self, num):
        self.sum = num

    def add(self, num=0):
        return C(self.sum + num)

number = C(23)
assert number.add(7).add(12).sum == 42
assert number.sum == 23

For details on memory handling in this solution, see comments of this posts. This solution is standard way of solving your problem.

Comments