如何postpone/defer 评估f-strings?

How to postpone/defer the evaluation of f-strings?

我正在使用模板字符串来生成一些文件,我喜欢新的 f-strings 的简洁性,用于减少我以前的模板代码,如下所示:

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}

这是完整的 "Ideal 2"。

它不是 f 弦——它甚至不使用 f 弦——但它按要求使用。完全按照指定的语法。没有安全问题,因为我们没有使用 eval().

它使用了一点class并实现了由打印自动调用的__str__。为了摆脱 class 的有限范围,我们使用 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

f 字符串只是一种创建格式化字符串的更简洁的方法,它用 f 替换了 .format(**names)。如果您不希望以这种方式立即评估字符串,请不要将其设为 f 字符串。将其保存为普通字符串文字,然后在您想要执行插值时调用 format,就像您一直在做的那样。

当然还有eval的替代方案。

template.txt:

f'The current name is {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.format 替换为 eval,这肯定不值得。只需通过 format 调用继续使用常规字符串。

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

是的,这正是我们使用带有替换字段和 .format 的文字的原因,因此我们可以随时通过调用 format 来替换字段。

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

这是前缀 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

但感觉不对,并且受到以下事实的限制:您只能查看替换中的全局命名空间。尝试在需要本地名称的情况下使用它会惨败,除非作为参数传递给字符串(这完全解决了问题)。

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

除了一个函数(包括限制),不,所以最好还是坚持使用 .format

或者不使用 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))

使用 .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-strings 的默认 操作,但是现在为时已晚。 F 字符串评估可能会产生大量意外的副作用,并且以延迟的方式发生这种情况会改变程序的执行。

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

有人指出,如果字符串转换器中存在错误,延迟日志记录不应崩溃。上面的解决方案也可以做到这一点,将 finally: 更改为 except:,并在其中粘贴一个 log.exception

一个使用f-strings的建议。做你的评价 发生模板的逻辑级别并将其作为生成器传递。 您可以使用 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  

将字符串评估为 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

的启发,下面可以用来定义一个deferred-f-stringclass.

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)

这正是问题所要求的

您想要的似乎被视为 Python enhancement

同时 - 从链接的讨论来看 - 以下似乎是一个不需要使用 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

怎么样:

s = 'Hi, {foo}!'

s
> 'Hi, {foo}!'

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

这些答案中的大多数有时会让您得到一些类似于 f 弦的东西,但在某些情况下它们都会出错。 pypi f-yeah 上有一个包可以完成这一切,只需要额外花费两个字符! (完全公开,我是作者)

from fyeah import f

print(f("""'{'"all" the quotes'}'"""))

f 字符串和格式调用之间有很多差异,这里可能不完整列表

  • f-strings 允许任意评估 python 代码
  • f-strings 不能在表达式中包含反斜杠(因为格式化字符串没有表达式,所以我想你可以说这没什么区别,但它确实与原始 eval() 不同)可以做到)
  • 不得引用格式化字符串中的字典查找。可以引用 f-strings 中的 dict 查找,因此也可以查找非字符串键
  • f-strings 具有 format() 没有的调试格式:f"The argument is {spam=}"
  • f-string 表达式不能为空

使用 eval 的建议将为您提供完整的 f 字符串格式支持,但它们并不适用于所有字符串类型。

def f_template(the_string):
    return eval(f"f'{the_string}'")

print(f_template('some "quoted" string'))
print(f_template("some 'quoted' string"))
some "quoted" string
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in f_template
  File "<string>", line 1
    f'some 'quoted' string'
            ^
SyntaxError: invalid syntax

在某些情况下,此示例也会出现变量范围错误。

有很多关于使用 str.format() 的讨论,但如前所述,它不允许使用 f-strings 中允许的大多数表达式,例如算术或切片。使用 eval() 显然也有缺点。

我建议研究一种模板语言,例如 Jinja。对于我的 use-case,它工作得很好。请参阅下面的示例,其中我使用单个花括号覆盖了变量注释语法以匹配 f-string 语法。我没有完全回顾 f-strings 和像这样调用的 Jinja 之间的区别。

from jinja2 import Environment, BaseLoader

a, b, c = 1, 2, "345"
templ = "{a or b}{c[1:]}"

env = Environment(loader=BaseLoader, variable_start_string="{", variable_end_string="}")
env.from_string(templ).render(**locals())

结果

'145'