ozgeneral
ozgeneral

Reputation: 6779

Is it possible to control how a function will be executed in python?

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

Upvotes: 3

Views: 356

Answers (2)

blhsing
blhsing

Reputation: 106543

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

Upvotes: 4

modesitt
modesitt

Reputation: 7210

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.

Upvotes: 2

Related Questions