如何推迟/推迟对f弦的评估?

问题:如何推迟/推迟对f弦的评估?

我正在使用模板字符串生成一些文件,为此我喜欢新的f字符串的简洁性,以减少类似以下内容的我以前的模板代码:

template_a = "The current name is {name}"
names = ["foo", "bar"]
for name in names:
    print (template_a.format(**locals()))

现在,我可以直接替换变量:

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

但是,有时在其他地方定义模板是有意义的-在代码中较高的位置,或者从文件或其他内容中导入模板。这意味着模板是带有格式标记的静态字符串。字符串上必须发生一些事情,以告诉解释器将字符串解释为新的f字符串,但是我不知道是否有这种事情。

有什么方法可以引入字符串并将其解释为f字符串,从而避免使用.format(**locals())调用?

理想情况下,我希望能够像这样进行编码…(magic_fstring_function我不理解的部分在哪里出现):

template_a = f"The current name is {name}"
# OR [Ideal2] template_a = magic_fstring_function(open('template.txt').read())
names = ["foo", "bar"]
for name in names:
    print (template_a)

…具有所需的输出(无需两次读取文件):

The current name is foo
The current name is bar

…但是我得到的实际输出是:

The current name is {name}
The current name is {name}

I am using template strings to generate some files and I love the conciseness of the new f-strings for this purpose, for reducing my previous template code from something like this:

template_a = "The current name is {name}"
names = ["foo", "bar"]
for name in names:
    print (template_a.format(**locals()))

Now I can do this, directly replacing variables:

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

However, sometimes it makes sense to have the template defined elsewhere — higher up in the code, or imported from a file or something. This means the template is a static string with formatting tags in it. Something would have to happen to the string to tell the interpreter to interpret the string as a new f-string, but I don’t know if there is such a thing.

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

Ideally I want to be able to code like this… (where magic_fstring_function is where the part I don’t understand comes in):

template_a = f"The current name is {name}"
# OR [Ideal2] template_a = magic_fstring_function(open('template.txt').read())
names = ["foo", "bar"]
for name in names:
    print (template_a)

…with this desired output (without reading the file twice):

The current name is foo
The current name is bar

…but the actual output I get is:

The current name is {name}
The current name is {name}

回答 0

这是完整的“理想2”。

它不是f字符串,它甚至不使用f字符串,但可以按要求进行操作。语法完全符合规定。没有使用安全性,因为我们没有使用eval()

它使用了一个小类并实现了__str__由print自动调用的类。为了逃避该类的有限范围,我们使用该inspect模块向上跳一帧并查看调用者可以访问的变量。

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

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 headaches 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

回答 1

这意味着模板是带有格式标记的静态字符串

是的,这就是为什么我们要使用带有替换字段和的文字.format,因此我们可以随时调用format它来替换这些字段。

字符串上必须发生一些事情,以告知解释器将字符串解释为新的f字符串

那是前缀f/F。您可以将其包装在一个函数中,并在调用期间推迟评估,但是当然会产生额外的开销:

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

打印出:

The current name is foo
The current name is bar

但感觉不对,并受到以下事实的限制:您只能窥视替换中的全局命名空间。在需要本地名称的情况下尝试使用它会惨遭失败,除非将其作为参数传递给字符串(这完全是关键)。

有什么方法可以引入字符串并将其解释为f字符串,从而避免使用.format(**locals())调用?

除了功能(包括限制)外,不行,所以不妨坚持.format

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.


回答 2

一种将字符串评估为f字符串(具有完整功能)的简洁方法是使用以下函数:

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

然后,您可以执行以下操作:

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

并且,与许多其他建议的解决方案相比,您还可以执行以下操作:

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

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

回答 3

一架F-string是简单的创建一个格式化字符串,更换了更为简洁的方式.format(**names)f。如果您不希望以这种方式立即评估字符串,请不要将其设置为f字符串。将其另存为普通字符串文字,然后format在以后要进行插值时再调用它,就像以前一样。

当然,可以使用替代eval

template.txt

f’当前名称为{name}’

码:

>>> 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

但后来你已经设法做的是替换str.formateval,这肯定是不值得的。只需继续使用常规字符串即可format

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.


回答 4

使用.format并不是此问题的正确答案。Python f字符串与str.format()模板非常不同…它们可以包含代码或其他昂贵的操作-因此需要推迟。

这是一个延迟记录器的示例。这使用了logging.getLogger的常规序言,但是随后添加了仅在日志级别正确的情况下解释f字符串的新函数。

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)

这样做的优点是能够执行以下操作: log.fdebug("{obj.dump()}")….除非启用调试,否则不转储对象。

恕我直言:这应该是f字符串的默认操作,但是现在为时已晚。F字符串评估可能会产生大量和意想不到的副作用,以延迟的方式发生会改变程序的执行。

为了适当延迟f字符串,python需要某种方式来显式切换行为。也许使用字母“ g”?;)

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’? ;)

It has been pointed out that deferred logging shouldn’t crash if there’s a bug in the string converter. The above solution can do this as well, change the finally: to except:, and stick a log.exception in there.


回答 5

您想要的东西似乎被认为是Python增强功能

同时-从链接的讨论中-以下似乎是一个不需要使用的合理解决方法eval()

class FL:
    def __init__(self, func):
        self.func = func
    def __str__(self):
        return self.func()


template_a = FL(lambda: f"The current name, number is {name!r}, {number+1}")
names = "foo", "bar"
numbers = 40, 41
for name, number in zip(names, numbers):
    print(template_a)

输出:

The current name, number is 'foo', 41
The current name, number is 'bar', 42

What you want appears to be being considered as a Python enhancement.

Meanwhile — from the linked discussion — the following seems like it would be a reasonable workaround that doesn’t require using eval():

class FL:
    def __init__(self, func):
        self.func = func
    def __str__(self):
        return self.func()


template_a = FL(lambda: f"The current name, number is {name!r}, {number+1}")
names = "foo", "bar"
numbers = 40, 41
for name, number in zip(names, numbers):
    print(template_a)

Output:

The current name, number is 'foo', 41
The current name, number is 'bar', 42

回答 6

kadee答案的启发,以下内容可用于定义deferred-f-string类。

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

...

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

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

这正是这个问题要问的

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 __repr__(self):
        return eval(f"f'{self._s}'")

...

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


回答 7

或者也许不使用f字符串,只需格式化:

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

在没有名称的版本中:

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

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))

回答 8

怎么样:

s = 'Hi, {foo}!'

s
> 'Hi, {foo}!'

s.format(foo='Bar')
> 'Hi, Bar!'

How about:

s = 'Hi, {foo}!'

s
> 'Hi, {foo}!'

s.format(foo='Bar')
> 'Hi, Bar!'

回答 9

使用f字符串的建议。在发生模板的逻辑级别上进行评估,并将其作为生成器传递。您可以使用f弦在任意位置解开它

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  

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