Can I postpone/defer the evaluation of f-strings?

南笙酒味 提交于 2019-11-28 08:51:58
Paul Panzer

Here's a complete "Ideal 2".

It's not an f-string it doesn't even use f-strings. But it does as requested. Syntax exactly as specified. No security head aches since we are not using eval.

It uses a little class and implements __str__ which is automatically called by print. To escape the limited scope of the class we use the inspect module to hop one frame up and see the variables the caller has access to.

import inspect

class magic_fstring_function:
    def __init__(self, payload):
        self.payload = payload
    def __str__(self):
        vars = inspect.currentframe().f_back.f_globals.copy()
        vars.update(inspect.currentframe().f_back.f_locals)
        return self.payload.format(**vars)

template = "The current name is {name}"

template_a = magic_fstring_function(template)

# use it inside a function to demonstrate it gets the scoping right
def new_scope():
    names = ["foo", "bar"]
    for name in names:
        print(template_a)

new_scope()
# The current name is foo
# The current name is bar

An f-string is simply a more concise way of creating a formatted string, replacing .format(**names) with f. If you don't want a string to be immediately evaluated in such a manner, don't make it an f-string. Save it as an ordinary string literal, and then call format on it later when you want to perform the interpolation, as you have been doing.

Of course, there is an alternative with eval.

template.txt:

f'The current name is {name}'

Code:

>>> template_a = open('template.txt').read()
>>> names = 'foo', 'bar'
>>> for name in names:
...     print(eval(template_a))
...
The current name is foo
The current name is bar

But then all you've managed to do is replace str.format with eval, which is surely not worth it. Just keep using regular strings with a format call.

A concise way to have a string evaluated as an f-string (with its full capabilities) is using following function:

def fstr(template):
    return eval(f"f'{template}'")

Then you can do:

template_a = "The current name is {name}"
names = ["foo", "bar"]
for name in names:
    print(fstr(template_a))
# The current name is foo
# The current name is bar

And, in contrast to many other proposed solutions, you can also do:

template_b = "The current name is {name.upper() * 2}"
for name in names:
    print(fstr(template_b))
# The current name is FOOFOO
# The current name is BARBAR

This means the template is a static string with formatting tags in it

Yes, that's exactly why we have literals with replacement fields and .format, so we can replace the fields whenever we like by calling format on it.

Something would have to happen to the string to tell the interpreter to interpret the string as a new f-string

That's the prefix f/F. You could wrap it in a function and postpone the evaluation during call time but of course that incurs extra overhead:

template_a = lambda: f"The current name is {name}"
names = ["foo", "bar"]
for name in names:
    print (template_a())

Which prints out:

The current name is foo
The current name is bar

but feels wrong and is limited by the fact that you can only peek at the global namespace in your replacements. Trying to use it in a situation which requires local names will fail miserably unless passed to the string as arguments (which totally beats the point).

Is there any way to bring in a string and have it interpreted as an f-string to avoid using the .format(**locals()) call?

Other than a function (limitations included), nope, so might as well stick with .format.

Or maybe do not use f-strings, just format:

fun = "The curent name is {name}".format
names = ["foo", "bar"]
for name in names:
    print(fun(name=name))

In version without names:

fun = "The curent name is {}".format
names = ["foo", "bar"]
for name in names:
    print(fun(name))

inspired by the answer by kadee, the following can be used to define a deferred-f-string class.

class FStr:
    def __init__(self, s):
        self._s = s
    def __str__(self):
        return eval(f"f'{self._s}'")
    def __repr__(self):
        return self.__str__()

...

template_a = FStr('The current name is {name}')

names = ["foo", "bar"]
for name in names:
    print (template_a)

which is exactly what the question asked for

Using .format is not a correct answer to this question. Python f-strings are very different from str.format() templates ... they can contain code or other expensive operations - hence the need for deferral.

Here's an example of a deferred logger. This uses the normal preamble of logging.getLogger, but then adds new functions that interpret the f-string only if the log level is correct.

log = logging.getLogger(__name__)

def __deferred_flog(log, fstr, level, *args):
    if log.isEnabledFor(level):
        import inspect
        frame = inspect.currentframe().f_back.f_back
        try:
            fstr = 'f"' + fstr + '"'
            log.log(level, eval(fstr, frame.f_globals, frame.f_locals))
        finally:
            del frame
log.fdebug = lambda fstr, *args: __deferred_flog(log, fstr, logging.DEBUG, *args)
log.finfo = lambda fstr, *args: __deferred_flog(log, fstr, logging.INFO, *args)

This has the advantage of being able to do things like: log.fdebug("{obj.dump()}") .... without dumping the object unless debugging is enabled.

IMHO: This should have been the default operation of f-strings, however now it's too late. F-string evaluation can have massive and unintended side-effects, and having that happen in a deferred manner will change program execution.

In order to make f-strings properly deferred, python would need some way of explicitly switching behavior. Maybe use the letter 'g'? ;)

A suggestion that uses f-strings. Do your evaluation on the logical level where the templating is occurring and pass it as a generator. You can unwind it at whatever point you choose, using f-strings

In [46]: names = (i for i in ('The CIO, Reed', 'The homeless guy, Arnot', 'The security guard Spencer'))

In [47]: po = (f'Strangely, {next(names)} has a nice {i}' for i in (" nice house", " fast car", " big boat"))

In [48]: while True:  
...:     try:  
...:         print(next(po))  
...:     except StopIteration:  
...:         break  
...:       
Strangely, The CIO, Reed has a nice  nice house  
Strangely, The homeless guy, Arnot has a nice  fast car  
Strangely, The security guard Spencer has a nice  big boat  
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!