如何推迟/推迟 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 弦的评估?的主要内容,如果未能解决你的问题,请参考以下文章