Find previous value of a variable in Python

Answer: actually, we CAN

But not in general case.

You need some magick for this.

And magick is called "custom namespaces".

Whole idea is from Armin Ronacher presentation 5 years of Bad Ideas.

Magick: custom namespace with history of values

Let's create custom namespace that saves history of values.

For demonstration purposes let's change rule for __del__ - instead of deleting values we will insert None.

from collections import MutableMapping
class HistoryNamespace(MutableMapping):
    def __init__(self):
        self.ns = {}        
    def __getitem__(self, key):
        return self.ns[key][-1]  # Rule 1. We return last value in history 
    def __delitem__(self, key):
        self.ns[key].append(None) # Rule 4. Instead of delete we will insert None in history
    def __setitem__(self, key, value): # Rule 3. Instead of update we insert value in history
        if key in self.ns:
            self.ns[key].append(value)            
        else:
            self.ns[key] = list([value,]) # Rule 2. Instead of insert we create history list
    def __len__(self):
         return len(self.ns)
    def __iter__(self):
         return iter(self.ns)    

history_locals = HistoryNamespace()        
exec('''
foo=2
foo=3
del foo  
foo=4
print(foo)
''', {}, history_locals)
print("History of foo:", history_locals.ns['foo'])

Rejoice!

Custom namespaces is very powerful technique but almost never used.

The fact I find somewhat puzzling.


Short answer No, long answer Yes, if you do it yourself.


NO

The precise handling (dict, array, ...) of local names is implementation defined, but for all intents and purposes the history of a name is not tracked. None of the major implementations provide this functionality.

As far as the Python language is concerned, an object not assigned to a name is gone. In practice, garbage collection is free to claim any ambient object not currently bound to a name. Whether this happens immediately or at an arbitrary time does not change that such objects are off limits for Python code. Otherwise, Python would have to keep a significant share of dead objects alive.

Once a name is re-asigned, its previous referent is gone from it.


Yes, but please don't do it unless your really have to

There are various ways to hook into the execution of Python code. For example, you can intercept calls/lines/returns by using sys.settrace; this is how debuggers work, and can inspect anything. If you have control over how the actual code is executed, see Alex Yu's answer for a way to hook just into the namespace.

import sys

class LocalsTracer:
    """Tracer for local assignment that prints the history of names"""
    def __init__(self, frame, event, arg):
        assert event == "call"
        # identifier for the call we are tracing - name, file, line
        self.subject = "%s (%s:%s)" % (frame.f_code.co_name, frame.f_code.co_filename, frame.f_code.co_firstlineno)
        self.names = {}

    # tracer gets *called* by the interpreter
    def __call__(self, frame, event, arg):
        if event == "line":
            self.trace_names(frame, event, arg)
        elif event in ("return", "exception"):
            self.trace_names(frame, event, arg)
            self.trace_exit(frame, event, arg)
        else:
            raise RuntimeError("Invalid event: %r" % event)

    def trace_names(self, frame, event, arg):
        """Capture modifications of names and store their history"""
        for name, value in frame.f_locals.items():
            try:
                if self.names[name][-1] != value:
                    self.names[name].append(value)
            except KeyError:
                self.names[name] = [value]
        return self

    def trace_exit(self, frame, event, arg):
        """Report the current trace on exit"""
        print("exit", self.subject, "via", event)
        print(self.names)

# trace targets can be defined regularly, anywhere
def bar(b):  # tracer captures function parameters
    b = 4

def foo():
    a = 1
    b = 2
    bar(27)  # tracer can recurse
    a = 3

sys.settrace(LocalsTracer)  # start tracing locals assignment
foo()
sys.settrace(None)          # stop tracing

Tags:

Python