Is a string formatter that pulls variables from its calling scope bad practice?
I have some code that does an awful lot of string formatting, Often, I end up with code along the lines of:
"...".format(x=x, y=y, z=z, foo=foo, ...)
Where I'm trying to interpolate a large number of variables into a large string.
Is there a good reason not to write a function like this that uses the inspect
module to find variables to interpolate?
import inspect
def interpolate(s):
return s.format(**inspect.currentframe().f_back.f_locals)
def generateTheString(x):
y = foo(x)
z = x + y
# more calculations go here
return interpolate("{x}, {y}, {z}")
Update: Python 3.6 has this feature (a more powerful variant) builtin:
x, y, z = range(3)
print(f"{x} {y + z}")
# -> 0 3
See PEP 0498 -- Literal String Interpolation
It[manual solution] leads to somewhat surprising behaviour with nested functions:
from callerscope import format
def outer():
def inner():
nonlocal a
try:
print(format("{a} {b}"))
except KeyError as e:
assert e.args[0] == 'b'
else:
assert 0
def inner_read_b():
nonlocal a
print(b) # read `b` from outer()
try:
print(format("{a} {b}"))
except KeyError as e:
assert 0
a, b = "ab"
inner()
inner_read_b()
Note: the same call succeeds or fails depending on whether a variable is mentioned somewhere above or below it.
Where callerscope
is:
import inspect
from collections import ChainMap
from string import Formatter
def format(format_string, *args, _format=Formatter().vformat, **kwargs):
caller_locals = inspect.currentframe().f_back.f_locals
return _format(format_string, args, ChainMap(kwargs, caller_locals))
A simpler and safer approach would be the code below. inspect.currentframe isn't available on all implementation of python so your code would break when it isn't. Under jython, ironpython, or pypy it might not be available because it seems to be a cpython thing. This makes your code less portable.
print "{x}, {y}".format(**vars())
this technique is actually described in the Python tutorial's Input and Output chapter
This could also be done by passing the table as keyword arguments with the ‘**’ notation. This is particularly useful in combination with the new built-in vars() function, which returns a dictionary containing all local variables.
also in the python docs for inspect.currentframe
CPython implementation detail: This function relies on Python stack frame support in the interpreter, which isn’t guaranteed to exist in all implementations of Python. If running in an implementation without Python stack frame support this function returns None.
The good old mailman has a function _
that does exactly this thing:
def _(s):
if s == '':
return s
assert s
# Do translation of the given string into the current language, and do
# Ping-string interpolation into the resulting string.
#
# This lets you write something like:
#
# now = time.ctime(time.time())
# print _('The current time is: %(now)s')
#
# and have it Just Work. Note that the lookup order for keys in the
# original string is 1) locals dictionary, 2) globals dictionary.
#
# First, get the frame of the caller
frame = sys._getframe(1)
# A `safe' dictionary is used so we won't get an exception if there's a
# missing key in the dictionary.
dict = SafeDict(frame.f_globals.copy())
dict.update(frame.f_locals)
# Translate the string, then interpolate into it.
return _translation.gettext(s) % dict
So if Barry Warsaw can do that, why can't we?
In the inspect
module, currentframe
is defined like this:
if hasattr(sys, '_getframe'):
currentframe = sys._getframe
else:
currentframe = lambda _=None: None
So unless sys
has a _getframe
attribute, the interpolate
function will not work.
The docs for sys._getframe
say:
CPython implementation detail: This function should be used for internal and specialized purposes only. It is not guaranteed to exist in all implementations of Python.
Writing
"{x}, {y}, {z}".format(**vars())
in the function body is not that much longer than
interpolate("{x}, {y}, {z}")
and your code will be more portable.