3

I know python is extremely flexible allowing -almost- anything user wants. However I have never seen nor heard of such a feature, and could not find anything related online: is it possible to execute a variable that is a function step by step?

def example_function():
  print("line 1")
  # stuff
  print("line 2")
  # stuff
  return(3)

def step_by_step_executor(fn):
  while fn.has_next_step():
    print(fn.current_step)
    fn.execute_step()
  return fn.return

step_by_step_executor(example_function) 
# print("line 1")
# line 1
# stuff
# print("line 2")
# line 2
# stuff
# return(3)
# returns 3

I think I can implement something like this using a combination of inspect, exec and maybe __call__, but I am interested to see if there is an already existing name and implementation for this.

Example use cases:

@do_y_instead_of_x
def some_function():
  do stuff
  do x
  do more
some_function()
# does stuff
# does y
# does more

@update_progress_bar_on_loops
def some_other_function():
  do stuff
  for x in range...:
     ...
  do more
some_other_function()
# does stuff
# initializes a progress bar, reports whats going on, does the loop
# does more
10
  • Not sure if python has anything liket his built-in, but I think 'executor' is the common name for this mechanism. It's probably hard to find a good reference because this question is actually very general and different applications solve it in different ways. The theoretical answer to this question is probably a sequence/control/list monad. Commented Aug 21, 2019 at 22:19
  • 1
    The debugger maybe? With a Tcl/expect script driving it😉 Commented Aug 21, 2019 at 22:19
  • 1
    You're describing pdb.runcall. Commented Aug 21, 2019 at 22:22
  • 1
    Make your function a generator and yield values when you want a "stop". Commented Aug 21, 2019 at 22:28
  • @blhsing pdb.runcall looks like it is specifically intended for debugging. I'm not sure if this would impose any performance drawbacks. (I will check and consider this as a possible solution when I start coding this) Commented Aug 21, 2019 at 22:34

2 Answers 2

4

You can create a Python debugger pdb.Pdb instance and pass to it a custom file-like object that implements the write method to selectively output the code portions of the debugger output, and the readline method to always send to the debugger the n (short for next) command. Since the debugger always outputs the line that returns from a function twice, the second time of which is preceded by a --Return-- line, you can use a flag to avoid outputting the redundant line of return:

import pdb

class PdbHandler:
    def __init__(self):
        self.returning = False

    def write(self, buffer):
        if buffer == '--Return--':
            self.returning = True

        # each line of code is prefixed with a '-> '
        _, *code = buffer.split('\n-> ', 1)
        if code:
            if self.returning:
                self.returning = False
            else:
                print(code[0])

    def readline(self):
        return 'n\n'

    def flush(self):
        pass

def example_function():
    print("line 1")
    print("line 2")
    return (3)

handler = PdbHandler()
print('returns', pdb.Pdb(stdin=handler, stdout=handler).runcall(example_function))

This outputs:

print("line 1")
line 1
print("line 2")
line 2
return (3)
returns 3
Sign up to request clarification or add additional context in comments.

Comments

2

How about yield - it does more than people think and can be used to fundamentally represent coroutines and ordering - a feature perhaps more fundamental than it's use to build iterators. Your example:

def example_function():
    print("line 1")
    yield
    print("line 2")
    yield
    print("line 3")
    return 3

def step_by_step_executor(fn):
    res = fn()
    while True:
        try:
            print(next(res))
        except StopIteration as e:
            retval = e.value
            break
    return retval

print(step_by_step_executor(example_function))

Which results in

line 1
None
line 2
None
line 3
3

As the return value of the generator is given as the value of the stop iteration that is raised. If you choose to intermittently yield values (instead of blank yield), you will see those printed at each iteration as well.

One common place where this kind of code comes up is the contextlib contextmanager decorator which is used to turn a generator into a context manager though the series of enter and exit steps.

6 Comments

that would cause repetition of yield, and I wouldn't be able to pass any function (function specifically needs to have a yield in every second line). this would limit it's use for functions that come from third party libraries for example (see example use cases)
What about using inspect.getsource(f) and adding said yields, executing the result then with the above step_by_step_executor. A bit hacky. Using a debugger may make sense.
If it gets to implementing myself, I was thinking getting the source and using exec for each line, but that would work too
as long as you can adequately control the scope of the arguments
This simply isn't a safe solution. Unless you fully parse the code it is going to be hard for you to identify the end of a "line" to insert a yield statement into when the line is part of a multi-line statement.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.