Call methods again in same order

Question:

For a class with an arbitrary number of methods:

class Test:
    def __init__(self, foo):
        self.foo = foo

    def test1(self, arg1=1, arg2=2):
        # do something
        print(f"test1, arg1 = {arg1}, arg2 = {arg2}")

    def test2(self, arg3=3, arg4=4):
        # do somehting else
        print(f"test2, arg3 = {arg3}, arg4 = {arg4}")

I would like to keep track of what methods that has been called, in what order, and the arguments. I would like a method that can then call the methods in the same order, with the same arguments.

My first intution was to create a decorator that sets a dictionary with function name and function, and a list with the order of the function names:

def set_stuff(func):
    def inner(self, *args, **kwargs):
        func_name = func.__name__
        self.called.append({f"{func_name}": func})
        func(self, *args, **kwargs)
    return inner


class Test:
    def __init__(self, foo):
        self.foo = foo
        self.called = list()

    @set_stuff
    def test1(self, arg1=1, arg2=2):
        # do something
        print(f"test1, arg1 = {arg1}, arg2 = {arg2}")

    @set_stuff
    def test2(self, arg3=3, arg4=4):
        # do somehting else
        print(f"test2, arg3 = {arg3}, arg4 = {arg4}")

    def call_in_order(self):
        for func_order in self.called:
            func = list(func_order.values())[0]
            func(self)

Which results in:

test = Test('yo')

test.test2()
test2, arg3 = 3, arg4 = 4

test.test1()
test1, arg1 = 1, arg2 = 2

test.call_in_order()
test2, arg3 = 3, arg4 = 4
test1, arg1 = 1, arg2 = 2

This is what i want, however it has two problems:

  1. It only works for the default argument input. If test.test1(arg1='foo', arg2='bar') is called, test.call_in_order() will still call the method with the default arguments (arg1=1, arg2=2).
  2. It should be able to call the same method multiple times. This is (obviously) not possible when using dictionaries.

Anyone with an elegant solution?

Update: as @MattDMo pointed out, it makes most sense to use a list of dictionaries. This has now been done in the example and it solves the second problem.

Asked By: DHJ

||

Answers:

How about also keeping track of the arguments?

def set_stuff(func):
    def inner(self, *args, **kwargs):
        func_name = func.__name__
        self.called.update({f"{func_name}": func})
        self.call_order.append(func_name)
        if args:
            self.args.append(args)
        else:
            self.args.append(())
        if kwargs:
            self.kwargs.append(kwargs)
        else:
            self.kwargs.append({})
        func(self, *args, **kwargs)
    return inner


class Test:
    def __init__(self, foo):
        self.foo = foo
        self.called = dict()
        self.call_order = list()
        self.args = list()
        self.kwargs = list()

    @set_stuff
    def test1(self, arg1=1, arg2=2):
        # do something
        print(f"test1, arg1 = {arg1}, arg2 = {arg2}")

    @set_stuff
    def test2(self, arg3=3, arg4=4):
        # do somehting else
        print(f"test2, arg3 = {arg3}, arg4 = {arg4}")

    def call_in_order(self):
        for func_name, args, kwargs in zip(self.call_order, self.args, self.kwargs):
            func = self.called[f'{func_name}']
            func(self, *args, **kwargs)
test = Test('yo')

test.test2()
test2, arg3 = 3, arg4 = 4

test.test1()
test1, arg1 = 1, arg2 = 2

test.test1(arg2=17,arg1=2)
test1, arg1 = 2, arg2 = 17

test.test1(4,5)
test1, arg1 = 4, arg2 = 5

test.call_in_order()
test2, arg3 = 3, arg4 = 4
test1, arg1 = 1, arg2 = 2
test1, arg1 = 2, arg2 = 17
test1, arg1 = 4, arg2 = 5
Answered By: Flow

What about using a simple list and exec()? Of course, you need to know what you are doing.

Here is a minimal implementation of the concept:

class Test:
    def __init__(self):
        self.calls = []

    def test1(self, arg1=1, arg2=2):
        # do something
        s = f"test1(arg1={arg1}, arg2={arg2})"
        print('called:', s)
        self.calls.append(s)

    def test2(self, arg1=3, arg2=4):
        # do something
        s = f"test2(arg1={arg1}, arg2={arg2})"
        print('called:', s)
        self.calls.append(s)

    def recall(self):
        _calls = self.calls[:] #copy the original call stack
        for call in _calls:
            exec(f"self.{call}")
        self.calls = _calls[:] #recover the original call stack


T = Test()
T.test1(2,3)
T.test2(4,5)
print('---')
T.recall()

Output:

called: test1(arg1=2, arg2=3)
called: test2(arg1=4, arg2=5)
---
called: test1(arg1=2, arg2=3)
called: test2(arg1=4, arg2=5)
Answered By: alec_djinn
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.