3

This appears simple, but I can't find a good solution.

It's the old 'pass by reference'/ 'pass by value' / 'pass by object reference' problem. I understand what is happening, but I can't find a good work around.

I am aware of solutions for small problems, but my state is very large and extremely expensive to save/ recalculate. Given these constraints, I can't find a solution.

Here is some simple pseudocode to illustrate what I would like to do (if Python would let me pass by reference):

class A:
    def __init__(self,x):
        self.g=x
        self.changes=[]
    def change_something(self,what,new): # I want to pass 'what' by reference
        old=what # and then de-reference it here to read the value
        self.changes.append([what,old]) # store a reference
        what=new # dereference and change the value
    def undo_changes():
        for c in self.changes:
            c[0]=c[1] # dereference and restore old value

Edit: Adding some more pseudocode to show how I would like the use the above

test=A(1) # initialise test.g as 1

print(test.g)
out: 1

test.change_something(test.g,2)
# if my imaginary code above functioned as described in its comments,
# this would change test.g to 2 and store the old value in the list of changes

print(test.g)
out: 2

test.undo_changes()

print(test.g)
out: 1

Obviously the above code doesnt work in python due to being 'pass by object reference'. Also I'd like to be able to undo a single change, not just all of them as in the code above.

The thing is... I can't seem to find a good work around. There are solutions out there like these:

Do/Undo using command pattern in Python

making undo in python

Which involve storing a stack of commands. 'Undo' then involves removing the last command and then re-building the final state by taking the initial state and re-applying everything but the last command. My state is too large for this to be feasible, the issues are:

  • The state is very large. Saving it entirely is prohibitively expensive.
  • 'Do' operations are costly (making recalculating from a saved state infeasible).
  • Do operations are also non-deterministic, relying on random input.
  • Undo operations are very frequent

I have one idea, which is to ensure that EVERYTHING is stored in lists, and writing my code so that everything is stored, read from and written to these lists. Then in the code above I can pass the list name and list index every time I want to read/write a variable.

Essentially this amounts to building my own memory architecture and C-style pointer system within Python! This works, but seems a little... ridiculous? Surely there is a better way?

Ben
  • 353
  • 1
  • 6
  • 14
  • I think you can make a copy of your what and store its copy. – Mia Dec 07 '17 at 04:44
  • The issue though is how do I store the location of 'what', so that I can later restore the old value from the copy? – Ben Dec 07 '17 at 04:46
  • Also, if I remember correctly, when you pass an argument to a function in Python, you are passing the reference to the object. When you then reassign the variable, the old object is not changed. – Mia Dec 07 '17 at 04:49
  • Can you please provide what data type is your `what`? – Mia Dec 07 '17 at 04:51
  • 'what' could be any type. For the code above to work, I need it to be a location - a reference. Because python is not a pass-by-reference language, I don't think the code above can be made to work without building my own pseudo-memory/pointer system with lists as described above (but I would love to be proven wrong!). I'm fairly sure I need a fundamentally different approach that works within Python... – Ben Dec 07 '17 at 04:59
  • Why don't you use a different data structure dict, as dict= {'what': {'old':old_val, 'new': new_val} i:e: key as 'what' and maintain/retrieve state from its value, which is again a dict having keys new and old. If you have other constraints in using dictionary, then i will suggest to maintain a custom function called undo and run as needed. – Satya Dec 07 '17 at 06:18
  • I am not sure I get the full issue. Wouldn't stack of states (objects) be an obvious solution? You could push and pop your objects from it. Basically you wouldn't pass the object as an argument, but the stack (or the stack would be implicit). At worst, you could pass a reference to your object, in the form of an absolute or relative position in the stack? – fralau Dec 07 '17 at 06:31
  • @fralau - imagine that I have 64gb of memory, and my state is 60gb in size. I need to store individual changes - I can't store entire states. – Ben Dec 07 '17 at 06:42
  • @Satya I'm not totally sure I understand: Where would I use the dict? inside the function, or to pass to the function, or for all my variables? – Ben Dec 07 '17 at 06:53
  • I've added some more pseudocode to show how I'd like to use these functions. I hope that makes things clearer! – Ben Dec 07 '17 at 06:54
  • 1
    I think this is what you want https://github.com/ActiveState/code/blob/master/recipes/Python/306866_A_basic_undo_mechanism/recipe-306866.py - with it's two companion recipes it will record a series of changes and provides undo/redo. The only thing I would like to add would be some sort of "snapshot" and the ability to revert to any snapshot. – DisappointedByUnaccountableMod Dec 07 '17 at 13:51
  • @barny fantastic, thanks again! – Ben Dec 08 '17 at 05:03

2 Answers2

1

Please check if it helps....

class A:
def __init__(self,x):
    self.g=x
    self.changes={}
    self.changes[str(x)] = {'init':x, 'old':x, 'new':x}   #or make a key by your choice(immutable)
def change_something(self,what,new): # I want to pass 'what' by reference
    self.changes[what]['new'] = new #add changed value to your dict
    what=new # dereference and change the value
def undo_changes():
    what = self.changes[what]['old'] #retrieve/changed to the old value
    self.changes[what]['new'] = self.changes[what]['old'] #change latest new val to old val as you reverted your changes

for each change you can update the change_dictionary. Onlhy thing you have to figure out is "how to create entry for what as a key in self.change dictionary", I just made it str(x), just check the type(what) and how to make it a key in your case.

Satya
  • 5,470
  • 17
  • 47
  • 72
  • 1
    This gave me idea to use the name of the variable as a reference, thank you. I've come up with a solution that works, but is pretty ugly... I'll post it below now, but leave the question open in case anyone can come up with something cleaner... – Ben Dec 07 '17 at 11:12
1

Okay so I have come up with an answer... but it's ugly! I doubt it's the best solution. It uses exec() which I am told is bad practice and to be avoided if at all possible. EDIT: see below!

Old code using exec():

class A:
    def __init__(self,x):
        self.old=0
        self.g=x
        self.h=x*10
        self.changes=[]
    def change_something(self,what,new):
        whatstr='self.'+what
        exec('self.old='+whatstr)
        self.changes.append([what,self.old]) 
        exec(whatstr+'=new') 
    def undo_changes(self):
        for c in self.changes:
            exec('self.'+c[0]+'=c[1]')
    def undo_last_change(self):
        c = self.changes[-1]
        exec('self.'+c[0]+'=c[1]')
        self.changes.pop()

Thanks to barny, here's a much nicer version using getattr and setattr:

class A:
    def __init__(self,x):
        self.g=x
        self.h=x*10
        self.changes=[]
    def change_something(self,what,new):
        self.changes.append([what,getattr(self,what)])
        setattr(self,what,new) 
    def undo_changes(self):
        for c in self.changes:
            setattr(self,c[0],c[1])
    def undo_last_change(self):
        c = self.changes[-1]
        setattr(self,c[0],c[1])
        self.changes.pop()

To demonstrate, the input:

print("demonstrate changing one value")
b=A(1)
print('g=',b.g)
b.change_something('g',2)
print('g=',b.g)
b.undo_changes()
print('g=',b.g)

print("\ndemonstrate changing two values and undoing both")
b.change_something('h',3)
b.change_something('g',4)
print('g=', b.g, 'h=',b.h)
b.undo_changes()
print('g=', b.g, 'h=',b.h)

print("\ndemonstrate changing two values and undoing one")
b.change_something('h',30)
b.change_something('g',40)
print('g=', b.g, 'h=',b.h)
b.undo_last_change()
print('g=', b.g, 'h=',b.h)

returns:

demonstrate changing one value
g= 1
g= 2
g= 1

demonstrate changing two values and undoing both
g= 4 h= 3
g= 1 h= 10

demonstrate changing two values and undoing one
g= 40 h= 30
g= 1 h= 30

EDIT 2: Actually... after further testing, my initial version with exec() has some advantages over the second. If the class contains a second class, or list, or whatever, the exec() version has no trouble updating a list within a class within a class, however the second version will fail.

Ben
  • 353
  • 1
  • 6
  • 14
  • 1
    Not sure why you need the exec() calls, Nasty. Maybe part of your difficulty is that you are trying to combine the class being changed as well as the observer storing the change history. You could pickle an object to capture its state and save that in a separate 'undo stack', unpickling whichever item in the stack you need to revert to. – DisappointedByUnaccountableMod Dec 07 '17 at 11:37
  • 1
    And if state is very large, then you can always save the pickles to disk. – DisappointedByUnaccountableMod Dec 07 '17 at 11:40
  • 1
    The exec calls are the only way I have found so far to refer to a variable within the class by reference in Python. I am literally using its name to do so! It's a hack for sure, but it's the only simple method I have found that works. – Ben Dec 07 '17 at 11:43
  • 1
    I should have been clearer - storing the state is totally ruled out as a possible solution! It would definitely cause issues with memory use. Perhaps pickling could help with that... but the most significant issue with storing the state is that it will be incredibly slow to keep saving the entire state of the simulation. This is for a large Monte Carlo simulation, where I need thousands of moves per second, not one every 5 seconds, or however long it would take if I were to store the entire state before every move. – Ben Dec 07 '17 at 11:49
  • 2
    use gettar(self,name) see https://stackoverflow.com/questions/1167398/python-access-class-property-from-string – DisappointedByUnaccountableMod Dec 07 '17 at 12:25
  • @barny Thanks! That's exactly what I needed. I've updated the code above. – Ben Dec 08 '17 at 04:59