【问题标题】:How to postpone/defer the evaluation of f-strings?如何推迟/推迟 f 弦的评估?
【发布时间】:2018-05-31 20:53:10
【问题描述】:

我正在使用模板字符串来生成一些文件,我喜欢新的 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 itPEP 501 描述的延迟评估绝对应该被纳入核心 f-string 实现。现在,我们在功能较少、速度极慢的str.format() 方法(一方面支持延迟评估)和功能更强大、速度极快的 f 字符串语法 not 支持延迟评估之间犹豫不决。所以我们仍然需要两者,Python 仍然没有标准的字符串格式化程序。 插入 xkcd 标准模因。

标签: python python-3.x string-interpolation python-3.6 f-string


【解决方案1】:

这些答案中的大多数会在某些时候为您提供一些类似于 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}'''""") 会解决一些问题
【解决方案2】:

使用 .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() 函数吗?这也适用于多个模块的全球范围内吗?
【解决方案3】:

怎么样:

s = 'Hi, {foo}!'

s
> 'Hi, {foo}!'

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

【讨论】:

    【解决方案4】:

    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)
    

    这正是问题所要求的

    【讨论】:

      【解决方案5】:

      这是一个完整的“理想 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__ 来减少内存使用?
      【解决方案6】:

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

      【讨论】:

        【解决方案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】:

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

        【讨论】:

          【解决方案9】:

          或者也许不使用 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
          • 但是正常使用也不行,请看答案stackoverflow.com/questions/14072810/…
          【解决方案10】:

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

          是的,这正是我们有替换字段和.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 案例的可行替代方案,它不是 所有 案例的可行替代方案,它是偷偷摸摸的。
          • 不,没关系,留着吧。我对我的新解决方案更满意。但是我可以理解你的观点,如果你意识到它的局限性,这个是可行的。也许你可以在你的帖子中添加一点警告,这样就不会有人用错了脚?
          【解决方案11】:

          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.format 替换为eval,这肯定不值得。只需在 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
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-02-01
          • 1970-01-01
          • 2012-01-27
          • 2021-05-17
          • 1970-01-01
          相关资源
          最近更新 更多