Skip to content

Instantly share code, notes, and snippets.

@samwyse
Created October 7, 2020 21:16
Show Gist options
  • Save samwyse/d85de0b036bd3cc0095bf56d31ce6f78 to your computer and use it in GitHub Desktop.
Save samwyse/d85de0b036bd3cc0095bf56d31ce6f78 to your computer and use it in GitHub Desktop.
Python decorator to emulate Go's defer statement
#! /usr/bin/env python
"""Emulate Go's 'defer' statement.
The defer decorator adds the 'defer' function to the wrapped opject
that will push a callable and its arguments to a stack. The list of
saved calls is executed after the surrounding function returns. Defer
is commonly used to simplify functions that perform various clean-up
actions.
A deferred function's arguments are evaluated when the defer statement
is evaluated. In this example, the expression "i" is evaluated when
the print call is deferred. The deferred call will print "0" after
the function returns.
<<< @defers
... def a():
... i = 0
... defer(print, i)
... i += 1
... return
<<< a()
0
Deferred function calls are executed in Last In First Out order after
the surrounding function returns. This function prints "3210":
<<< @defers
... def b():
... for i in range(4):
... defer(print, i, end='')
<<< b()
3210
Deferred functions may read and assign to the returning function's return
values. In this example, a deferred function increments the return value
i after the surrounding function returns. Thus, this function returns 2:
>>> def change_retval():
... global retval
... retval += 1
>>> @defers
... def c():
... defer(change_retval)
... return 1
>>> c()
2
"""
# Insure maximum compatibility between Python 2 and 3
from __future__ import absolute_import, division, print_function
try:
basestring
except NameError:
basestring = str
# Metadate...
__author__ = "Samuel T. Denton, III <sam.denton@dell.com>"
__contributors__ = []
__copyright__ = "Copyright 2020 Samuel T. Denton, III"
__version__ = '0.9'
# Declutter our namespace
__all__ = ['defers']
# Python standard libraries
import argparse, os, sys
import functools
# Python personal libraries
# n/a
puterr = functools.partial(print, file=sys.stderr)
def defers(f):
"""Creates a LIFO stack for function calls to execute when the
wrapped function returns.
The name 'defer' is visible within the wrapped function.
<<< @defers
... def z():
... defer
<<< z()
The name 'defer' is not visible outside the wrapped function.
<<< print(defer)
Traceback (most recent call last):
...
NameError: name 'defer' is not defined
The name 'defer' is not overwritten.
<<< defer = 42
<<< z()
<<< defer
42
"""
GLBL_FUNC = 'defer'
GLBL_LIST = '_deferals' # TODO: does this need to be a global variable?
f_glbls = f.__globals__
def _defer(func, *args, **keywords):
f_glbls[GLBL_LIST].append((func, args, keywords))
@functools.wraps(f)
def wrapper(*args, **kwargs):
# First, save the current state of any global variables with
# the same names as ours.
saved = {}
for name in GLBL_FUNC, GLBL_LIST:
try:
saved[name] = f_glbls[name]
except KeyError:
pass
# Insert our special variables.
f_glbls.update(((GLBL_FUNC, _defer), (GLBL_LIST, [])))
# Call the wrapped function.
try:
retval = f(*args, **kwargs)
finally:
# Get the list of defered calls.
deferals = f_glbls[GLBL_LIST]
# Remove our special varaibles.
for name in GLBL_FUNC, GLBL_LIST:
try:
f_glbls[name] = saved[name]
except KeyError:
del f_glbls[name]
# Call the deferred functions.
for func, args, keywords in reversed(deferals):
# Note that builtin functions, e.g. print, have no
# __globals__attribute, so we need to be sneaky.
func_glbls = getattr(func, '__globals__', {})
func_glbls['retval'] = retval
func(*args, **keywords)
retval = func_glbls['retval']
del deferals[:]
return retval
return wrapper
if __name__ == '__main__':
import doctest
doctest.testmod()
@navytux
Copy link

navytux commented May 26, 2022

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment