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

Posted

技术标签:

【中文标题】如何推迟/推迟 f 弦的评估?【英文标题】:How to postpone/defer the evaluation of f-strings? 【发布时间】:2017-07-18 18:24:55 【问题描述】:

我正在使用模板字符串来生成一些文件,我喜欢新的 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

【问题讨论】:

你不能用f 字符串来做到这一点。 f 字符串不是数据,当然也不是字符串;这是代码。 (使用dis 模块检查它。)如果您希望稍后评估代码,请使用函数。 仅供参考,PEP 501 提出了一个接近您的第一个理想的功能,但目前“推迟了 [f-strings] 的进一步体验。” 模板是静态字符串,但 f 字符串不是字符串,而是代码对象,正如 @kindall 所说。我认为 f 字符串在实例化时(在 Python 3.6,7 中)会立即与变量绑定,而不是在最终使用时绑定。所以 f-string 可能不如你丑陋的旧 .format(**locals()) 有用,尽管在外观上更好。在 PEP-501 实施之前。 Guido 救了我们,但是PEP 498 really botched it。 PEP 501 描述的延迟评估绝对应该被纳入核心 f-string 实现。现在,我们在功能较少、速度极慢的str.format() 方法(一方面支持延迟评估)和功能更强、速度极快的 f 字符串语法 not 支持延迟评估之间进行讨价还价。所以我们仍然需要两者,Python 仍然没有标准的字符串格式化程序。 插入 xkcd 标准模因。 【参考方案1】:

这是一个完整的“理想 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

【讨论】:

我会接受这个作为答案,尽管我认为我不会在代码中真正使用它,因为它非常聪明。好吧,也许永远不会:)。也许python人可以用它来实现PEP 501。如果我的问题是“我应该如何处理这种情况”,那么答案将是“继续使用 .format() 函数并等待 PEP 501 解决”。感谢您弄清楚如何做不应该做的事情,@PaulPanzer 当模板包含比简单变量名更复杂的内容时,这不起作用。例如:template = "The beginning of the name is name[:4]" (-> TypeError: string indices must be integers) @bli 有趣,似乎是str.format 的限制。我曾经认为 f 字符串只是 str.format(**locals(), **globals()) 之类的语法糖,但显然我错了。 请不要在生产中使用它。 inspect 是一个危险信号。 我有 2 个问题,为什么要检查生产的“危险信号”,这样的情况会是一个例外,还是会有更可行的解决方法?是否有反对使用__slots__ 来减少内存使用?【参考方案2】:

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

但是你所做的只是用eval 替换str.format,这肯定不值得。只需在 format 调用中继续使用常规字符串即可。

【讨论】:

我真的认为您的代码 sn-p 没有任何优势。我的意思是,您总是可以在template.txt 文件中只写The current name is name,然后使用print(template_a.format(name=name))(或.format(**locals()))。该代码大约长了 10 个字符,但由于eval,它不会引入任何可能的安全问题。 @Bakuriu - 是的;就像我说的,虽然eval 确实允许我们编写f'name' 并将name 的评估延迟到需要时,但它不如简单地创建一个常规模板字符串然后调用format,因为OP 是已经在做。 "f-string 是一种更简洁的创建格式化字符串的方法,将 .format(**names) 替换为 f。"不完全-他们使用不同的语法。我没有足够新的 python3 来检查,但例如我相信 f'a+b' 有效,而 'a+b'.format(a=a, b=b) 引发 KeyError . .format() 在许多情况下可能都很好,但它不是替代品。 @philh 我想我刚刚遇到了一个示例,其中.format 不等于 f 字符串,可以支持您评论:DNA = "TATTCGCGGAAAATATTTTGA"; fragment = f"DNA[2:8]"; failed_fragment = "DNA[2:8]".format(**locals())。尝试创建failed_fragment 会导致TypeError: string indices must be integers【参考方案3】:

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

是的,这正是我们有替换字段和.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

【讨论】:

有趣的是,我发布了完全相同的 sn-p。但由于范围限制,我撤回了它。 (尝试将 for 循环包装在一个函数中。) @PaulPanzer 您是否想编辑问题并重新包含它?我不介意删除答案。这是 OP 案例的可行替代方案,它不是 所有 案例的可行替代方案,它是偷偷摸摸的。 不,没关系,留着吧。我对我的新解决方案更满意。但是我可以理解你的观点,如果你意识到它的局限性,这个是可行的。也许你可以在你的帖子中添加一点警告,这样就不会有人用错了脚?【参考方案4】:

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

【讨论】:

这并不适用于所有情况。示例:fun = "DNA[2:8]".format; DNA = "TATTCGCGGAAAATATTTTGA"; fun(DNA=DNA)。 -> TypeError: string indices must be integers 但是正常使用也不行,请看答案***.com/questions/14072810/…【参考方案5】:

使用 .format 不是此问题的正确答案。 Python f-strings 与 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-string 求值可能会产生大量和意想不到的副作用,并且以延迟的方式发生这种情况会改变程序的执行。

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

已经指出,如果字符串转换器中存在错误,则延迟日志记录不应该崩溃。上面的解决方案也可以做到这一点,把finally:改成except:,在里面贴上log.exception

【讨论】:

完全同意这个答案。这个用例是我在搜索这个问题时所考虑的。 这是正确答案。一些时间:%timeit log.finfo(f"bar=") 91.9 µs ± 7.45 µs per loop %timeit log.info(f"bar=") 56.2 µs ± 630 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each) log.setLevel(logging.CRITICAL) %timeit log.finfo("bar=") 575 ns ± 2.9 ns per loop %timeit log.info(f"bar=") 480 ns ± 9.37 ns per loop %timeit log.finfo("") 571 ns ± 2.66 ns per loop %timeit log.info(f"") 380 ns ± 0.92 ns per loop %timeit log.info("") 367 ns ± 1.65 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each) "如果字符串转换器中存在错误..." -- 错误是它不接受字符串中的双引号。 f_string.replace('"', '\\"') 适用于转义引号,但不适用于已转义的引号(例如,如果您正在记录输出)。 无法编辑我的评论:使用'f"""' + fstr + '"""' 会有所帮助。 有趣的方法,args 在哪里使用 __deferred_flog() ?顺便说一句,它可以嵌入到一个代理类中,取代原来的.debug().critical() 函数吗?这也可以跨多个模块在全球范围内工作?【参考方案6】:

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

【讨论】:

【参考方案7】:

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

【讨论】:

迄今为止最好的答案!当他们引入 f-strings 时,他们怎么没有将这个简单的实现作为内置功能包含在内? 不,这失去了作用域。唯一有效的原因是name 是全球性的。 f-strings 应该在评估中被推迟,但是类 FString 需要通过查看调用者本地和全局来创建对范围参数的引用列表......然后在使用时评估字符串。 @user3204459:因为能够执行任意字符串本质上是一种安全隐患——这就是为什么通常不鼓励使用eval() @martineau 它应该是 python 的一个特性,所以你不需要使用 eval ......另外,f-string 与 eval() 具有相同的风险,因为你可以放入任何东西大括号包括恶意代码,所以如果这是一个问题,那么不要使用 f-strings 这正是我正在寻找的,躲避“fstr 推迟”。Eval 似乎并不比使用 fstrings 差,因为我猜它们都具有相同的权力:f" eval('print(42)')"【参考方案8】:

受answer by 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)

这正是问题所要求的

【讨论】:

【参考方案9】:

您想要的似乎被视为 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

【讨论】:

【参考方案10】:

怎么样:

s = 'Hi, foo!'

s
> 'Hi, foo!'

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

【讨论】:

【参考方案11】:

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

from fyeah import f

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

f-strings 和 format 调用有很多不同,这里有一个可能不完整的列表

f-strings 允许对 python 代码进行任意评估 f-strings 不能在表达式中包含反斜杠(因为格式化的字符串没有表达式,所以我想你可以说这没有区别,但它确实与原始 eval() 可以做的不同) 格式化字符串中的dict 查找不能被引用。可以引用 f 字符串中的 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

此示例在某些情况下也会导致变量范围错误。

【讨论】:

哇,超级棒。开箱即用。向那个 11 次代表的人致敬!就像您列出的差异一样,可以激发信心。你遇到过什么陷阱吗?我看到您使用(小型)测试套件开发。老实说,我不知道你在那里的 c 文件 (_cfyeah.c) 中做了什么……但看起来你知道自己在做什么。 嘿,谢谢!绝对试图让它易于使用,所以很高兴听到。 _cfyeah.c 公开了本机 CPython fstring eval,它不是公共 Python API 的一部分。包不是必需的,但与每次编译字符串相比,如果使用它可以提供很大的加速。 return eval(f"""f'''the_string'''""") 会解决一些问题

以上是关于如何推迟/推迟 f 弦的评估?的主要内容,如果未能解决你的问题,请参考以下文章

如何推迟流读取调用

Django推迟外键查找

如何推迟 Tableau 中的计算?

如何推迟加载 UpdatePanel 内容,直到页面呈现后?

JS怎么把时间推迟

如何推迟将 Ajax 数据传递给子组件?