Created
October 7, 2020 21:16
-
-
Save samwyse/d85de0b036bd3cc0095bf56d31ce6f78 to your computer and use it in GitHub Desktop.
Python decorator to emulate Go's defer statement
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /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() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
For the reference: https://stackoverflow.com/a/53069630/9456786