11
\$\begingroup\$

f-strings is a new feature in the upcoming python release which basically lets you do this:

>>> s = 'qwerty'
>>> f'{s} {1+2}'
'qwerty 3'

Not pleased at having to wait until f-strings are widely supported, I came up with the following function, which should work for any Python version starting with 2.6.

def f(s):
    return s.format(**inspect.currentframe().f_back.f_locals)

>>> s = 'qwerty'
>>> f('{s} abc')
'qwerty abc'

Switching to f-strings in the future should be as easy as removing the two parentheses.

Of course, my function doesn't support the more advanced features of f-strings (like evaluation of any expression).

My questions are:

  1. If I start widespread use of this function in my code, is there a chance this bites me in the future? Is there anything objectionable in my current implementation?
  2. How would you implement a more advanced f-function, supporting evaluation of arbitrary expressions? Preferably without having performance suffer too much - my benchmark shows the f-function is only slightly slower than directly calling format.

Python version 2.6.

\$\endgroup\$
  • 2
    \$\begingroup\$ I would recommend that you read the PEP that introduced new-style format strings. It has a lot of information, and it's a very interesting read. python.org/dev/peps/pep-3101 \$\endgroup\$ – zondo Aug 9 '16 at 19:25
7
\$\begingroup\$

The thing most likely, in my mind, to cause problems here is that using frames is not guaranteed to work in any other Python implementation besides CPython.

inspect.currentframe()

Return the frame object for the caller’s stack frame.

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.

This will end up throwing an error if you try to unpack None because the implementation doesn't support your function. If you were dead-set on using stack inspection, then you should give a more useful error message than AttributeError: 'NoneType' object has no attribute 'f_back'. You should test for if you get a frame or not, and then use platform to print a more helpful error message.

def f(s):
    frame = inspect.currentframe()
    if frame is not None:
        return s.format(**frame.f_back.f_locals)
    else:
        raise NotImplementedError(
            "Python implementation {} does not support stack inspection.".format(
                platform.python_implementation()
            )
        )

At this point, then, you should make sure this is a well-documented limitation of your library, and I would probably write some unit tests where it asserts that is raised for certain implementations.

The real problem, however, isn't that you're using something that isn't guaranteed (although that's certainly a problem). The real problem is that stack inspection is almost never the right answer. We don't need it here either. What we're going to want to do is something like this:

def _poor_fstring(namespace):
    def _():
        return namespace
    return _

get_namespace = _poor_fstring(locals())

def f(s):
    return s.format(**get_namespace())

This still isn't ideal - you'll need to redeclare get_namespace in any scope you want access to it - but you avoid stack inspection, which is worth it in my mind.

Really though, I don't see a huge benefit between using your function and just calling s.format(**namespace), where namespace is whatever lookup table you want. The one benefit would be if, as you said, it supported arbitrary expressions. And the only way to get arbitrary expressions is to, well, evaluate them. You'll basically have to parse the string and extract everything between curly braces. Then try a lookup in the namespace and replace that, otherwise compile it, and then eval what you find. Hopefully I don't need to point out that evaling random strings is... dangerous.

Going into actually parsing a format string is way beyond the scope of this question.

In short, my recommendation would be to just suffer through... or upgrade your Python version.

\$\endgroup\$
  • \$\begingroup\$ 1. The get_namespace solution looks very dangerous to me: if one forgets to declare it in the current scope, a previous definition might happen to hold a different variable with the same name, which could cost hours of debugging to find. 2. I can't upgrade my python version to use f-strings, 3.6 has not been released yet :-) \$\endgroup\$ – Georgios Bitzes Aug 10 '16 at 11:47
  • 1
    \$\begingroup\$ There's always prerelease versions :) and you should get off of 2.6 anyway \$\endgroup\$ – Dannnno Aug 10 '16 at 12:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.