【问题标题】:String formatting: % vs. .format vs. f-string literal字符串格式:% vs. .format vs. f-string 文字
【发布时间】:2011-07-02 05:35:13
【问题描述】:

Python 2.6 引入了 str.format() 方法,其语法与现有的 % 运算符略有不同。哪个更好,适用于什么情况?

Python 3.6 现在通过语法f"my string" 引入了另一种字符串格式化格式的字符串文字(又名“f”字符串)。此格式选项是否比其他格式选项更好?

  1. 以下使用每种方法,结果都一样,那么有什么区别呢?

     #!/usr/bin/python
     sub1 = "python string!"
     sub2 = "an arg"
    
     sub_a = "i am a %s" % sub1
     sub_b = "i am a {0}".format(sub1)
     sub_c = f"i am a {sub1}"
    
     arg_a = "with %(kwarg)s!" % {'kwarg':sub2}
     arg_b = "with {kwarg}!".format(kwarg=sub2)
     arg_c = f"with {sub2}!"
    
     print(sub_a)    # "i am a python string!"
     print(sub_b)    # "i am a python string!"
     print(sub_c)    # "i am a python string!"
    
     print(arg_a)    # "with an arg!"
     print(arg_b)    # "with an arg!"
     print(arg_c)    # "with an arg!"
    
  2. 此外,Python 中何时会出现字符串格式化?例如,如果我的日志记录级别设置为 HIGH,我仍然会因为执行以下% 操作而受到打击吗?如果是这样,有没有办法避免这种情况?

     log.debug("some debug info: %s" % some_info)
    

【问题讨论】:

标签: python performance logging string-formatting f-string


【解决方案1】:

回答您的第一个问题....format 在许多方面似乎更加复杂。关于% 的一个恼人的事情是它可以接受一个变量或一个元组。你会认为以下方法总是有效的:

"hi there %s" % name

然而,如果name 恰好是(1, 2, 3),它会抛出一个TypeError。为保证它始终打印,您需要这样做

"hi there %s" % (name,)   # supply the single argument as a single-item tuple

这太丑了。 .format 没有这些问题。同样在您给出的第二个示例中,.format 示例看起来更简洁。

你为什么不使用它?

  • 不知道(在阅读本文之前我)
  • 必须与 Python 2.5 兼容

要回答您的第二个问题,字符串格式化与任何其他操作同时发生 - 在评估字符串格式化表达式时。而 Python 不是一种惰性语言,它会在调用函数之前评估表达式,因此在您的 log.debug 示例中,表达式 "some debug info: %s"%some_info 将首先评估为,例如"some debug info: roflcopters are active",那么该字符串将被传递给log.debug()

【讨论】:

  • "%(a)s, %(a)s" % {'a':'test'}怎么样
  • 请注意,log.debug("something: %s" % x) 会浪费时间,log.debug("something: %s", x) 则不会。字符串格式将在该方法中处理,如果不记录,则不会对性能造成影响。与往常一样,Python 会预测您的需求 =)
  • ted:与'{0}, {0}'.format('test') 做同样的事情看起来更糟糕。
  • 要点是:新语法允许对项目重新排序的一个反复出现的论点是一个有争议的观点:您可以对旧语法做同样的事情。大多数人不知道这实际上已经在 Ansi C99 Std 中定义了!查看man sprintf 的最新副本并了解% 占位符中的$ 符号
  • @cfi:如果你的意思是 printf("%2$d", 1, 3) 打印出“3”,这是在 POSIX 中指定的,而不是 C99。您引用的手册页指出,“C99 标准不包括使用 '$'... 的样式”。
【解决方案2】:

假设您使用 Python 的 logging 模块,您可以将字符串格式化参数作为参数传递给 .debug() 方法,而不是自己进行格式化:

log.debug("some debug info: %s", some_info)

除非记录器实际记录某些内容,否则会避免进行格式化。

【讨论】:

  • 这是我刚刚学到的一些有用的信息。遗憾的是它没有自己的问题,因为它似乎与主要问题分开。可惜 OP 没有将他的问题分成两个单独的问题。
  • 你可以像这样使用 dict 格式:log.debug("some debug info: %(this)s and %(that)s", dict(this='Tom', that='Jerry')) 但是,你不能在这里使用新样式的.format() 语法,即使在 Python 3.3 中也不能,这是一种耻辱。
  • 这样做的主要好处不是性能(与您对日志输出所做的任何操作相比,进行字符串插值会更快,例如在终端中显示,保存到磁盘)如果你有一个日志聚合器,它可以告诉你“你有 12 个这个错误消息的实例”,即使它们都有不同的 'some_info' 值。如果在将字符串传递给 log.debug 之前完成了字符串格式化,那么这是不可能的。聚合器只能说“你有 12 条不同的日志消息”
  • 如果您关心性能,请使用文字 dict {} 语法而不是 dict() 类实例化:doughellmann.com/2012/11/…
【解决方案3】:

根据我的测试,% 的性能优于 format

测试代码:

Python 2.7.2:

import timeit
print 'format:', timeit.timeit("'{}{}{}'.format(1, 1.23, 'hello')")
print '%:', timeit.timeit("'%s%s%s' % (1, 1.23, 'hello')")

结果:

> format: 0.470329046249
> %: 0.357107877731

Python 3.5.2

import timeit
print('format:', timeit.timeit("'{}{}{}'.format(1, 1.23, 'hello')"))
print('%:', timeit.timeit("'%s%s%s' % (1, 1.23, 'hello')"))

结果

> format: 0.5864730989560485
> %: 0.013593495357781649

在 Python2 中看起来,差异很小,而在 Python3 中,%format 快得多。

感谢@Chris Cogdon 提供示例代码。

编辑 1:

2019 年 7 月在 Python 3.7.2 中再次测试。

结果:

> format: 0.86600608
> %: 0.630180146

没有太大区别。我猜 Python 正在逐步改进。

编辑 2:

在有人在评论中提到了python 3的f-string后,我在python 3.7.2下对以下代码做了测试:

import timeit
print('format:', timeit.timeit("'{}{}{}'.format(1, 1.23, 'hello')"))
print('%:', timeit.timeit("'%s%s%s' % (1, 1.23, 'hello')"))
print('f-string:', timeit.timeit("f'{1}{1.23}{\"hello\"}'"))

结果:

format: 0.8331376779999999
%: 0.6314778750000001
f-string: 0.766649943

似乎 f-string 仍然比 % 慢,但比 format 好。

【讨论】:

  • 相反,str.format 提供了更多功能(尤其是类型专用格式,例如'{0:%Y-%m-%d}'.format(datetime.datetime.utcnow()))。绩效不能是所有工作的绝对要求。为工作使用正确的工具。
  • “过早的优化是万恶之源” 或者 Donald Knuth 曾经说过...
  • 坚持使用众所周知的格式化方案(只要它适合需求,它在绝大多数情况下都会这样做),并且速度提高一倍,这不是“过早的优化”,而是简单合理。顺便说一句,% 运算符允许重用printf 知识;字典插值是原理的一个非常简单的扩展。
  • 从我的测试来看,Python3 和 Python 2.7 之间也存在巨大差异。在 Python 3 中,%format() 效率更高。我使用的代码可以在这里找到:github.com/rasbt/python_efficiency_tweaks/blob/master/test_code/…github.com/rasbt/python_efficiency_tweaks/blob/master/test_code/…
  • 我实际上在一种情况下经历了相反的情况。新型格式更快。你能提供你使用的测试代码吗?
【解决方案4】:

模运算符( % )不能做的事情,afaik:

tu = (12,45,22222,103,6)
print '{0} {2} {1} {2} {3} {2} {4} {2}'.format(*tu)

结果

12 22222 45 22222 103 22222 6 22222

非常有用。

另外一点:format(),作为一个函数,可以在其他函数中作为参数使用:

li = [12,45,78,784,2,69,1254,4785,984]
print map('the number is {}'.format,li)   

print

from datetime import datetime,timedelta

once_upon_a_time = datetime(2010, 7, 1, 12, 0, 0)
delta = timedelta(days=13, hours=8,  minutes=20)

gen =(once_upon_a_time +x*delta for x in xrange(20))

print '\n'.join(map('{:%Y-%m-%d %H:%M:%S}'.format, gen))

结果:

['the number is 12', 'the number is 45', 'the number is 78', 'the number is 784', 'the number is 2', 'the number is 69', 'the number is 1254', 'the number is 4785', 'the number is 984']

2010-07-01 12:00:00
2010-07-14 20:20:00
2010-07-28 04:40:00
2010-08-10 13:00:00
2010-08-23 21:20:00
2010-09-06 05:40:00
2010-09-19 14:00:00
2010-10-02 22:20:00
2010-10-16 06:40:00
2010-10-29 15:00:00
2010-11-11 23:20:00
2010-11-25 07:40:00
2010-12-08 16:00:00
2010-12-22 00:20:00
2011-01-04 08:40:00
2011-01-17 17:00:00
2011-01-31 01:20:00
2011-02-13 09:40:00
2011-02-26 18:00:00
2011-03-12 02:20:00

【讨论】:

  • 您可以在map 中使用旧样式格式,就像使用格式一样容易。 map('some_format_string_%s'.__mod__, some_iterable)
  • @cfi: 请用C99重写上面的例子来证明你是对的
  • @MarcH:printf("%2$s %1$s\n", "One", "Two");gcc -std=c99 test.c -o test编译,输出为Two One。但我的立场是正确的:It is actually a POSIX extension 而不是 C。我无法在 C/C++ 标准中再次找到它,我以为我见过它。该代码甚至可以使用 'c90' std 标志。 sprintf man pageThis 没有列出它,但允许库实现超集。我原来的论点仍然有效,将C 替换为Posix
  • 我在这里的第一条评论,不适用于这个答案。我很后悔措辞。在 Python 中,我们不能使用模运算符 % 对占位符进行重新排序。为了这里的评论一致性,我仍然不想删除第一条评论。我很抱歉在这里发泄了我的愤怒。它针对的是旧语法本身不允许这样做的经常声明。我们可以引入 std Posix 扩展,而不是创建一个全新的语法。我们可以两者兼得。
  • 'modulo' 是指计算除法后余数的运算符。在这种情况下,百分号不是模运算符。
【解决方案5】:

PEP 3101 建议用 Python 3 中新的高级字符串格式替换 % 运算符,这将是默认值。

【讨论】:

  • Untrue: "通过保留现有机制可以保持向后兼容性。";当然,.format 不会替换 % 字符串格式。
  • 不,BrainStorms 的假设是正确的:“旨在替代现有的 '%'”。 Tobias 引用意味着两个系统将共存一段时间。 RTFPEP
【解决方案6】:

但是请注意,刚才我在尝试将现有代码中的所有% 替换为.format 时发现了一个问题:'{}'.format(unicode_string) 将尝试对unicode_string 进行编码,并且可能会失败。强>

看看这个 Python 交互式会话日志:

Python 2.7.2 (default, Aug 27 2012, 19:52:55) 
[GCC 4.1.2 20080704 (Red Hat 4.1.2-48)] on linux2
; s='й'
; u=u'й'
; s
'\xd0\xb9'
; u
u'\u0439'

s 只是一个字符串(在 Python3 中称为“字节数组”),u 是一个 Unicode 字符串(在 Python3 中称为“字符串”):

; '%s' % s
'\xd0\xb9'
; '%s' % u
u'\u0439'

当您将 Unicode 对象作为参数提供给 % 运算符时,即使原始字符串不是 Unicode,它也会生成 Unicode 字符串:

; '{}'.format(s)
'\xd0\xb9'
; '{}'.format(u)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'latin-1' codec can't encode character u'\u0439' in position 0: ordinal not in range(256)

.format 函数会引发“UnicodeEncodeError”:

; u'{}'.format(s)
u'\xd0\xb9'
; u'{}'.format(u)
u'\u0439'

只有当原始字符串是 Unicode 时,它​​才可以使用 Unicode 参数。

; '{}'.format(u'i')
'i'

或者如果参数字符串可以转换为字符串(所谓的“字节数组”)

【讨论】:

  • 除非确实需要新的format 方法的附加功能,否则根本没有理由更改工作代码...
  • 完全同意你的观点,Tobias,但有时在升级到较新版本的 Python 时需要
  • 例如? AFAIK,它从未被需要;我认为% 字符串插值不太可能消失。
  • 我认为 .format() 函数比字符串的 % 更安全。我经常看到像"p1=%s p2=%d" % "abc", 2"p1=%s p2=%s" % (tuple_p1_p2,) 这样的初学者错误。您可能认为这是编码人员的错,但我认为这只是奇怪的错误语法,对于快速脚本来说看起来不错,但对生产代码不利。
  • 但是我不喜欢 .format() 的语法,我会更喜欢旧的 %s%02d"p1=%s p2=%02d".format("abc", 2)。我责怪那些发明和批准花括号格式的人,这些格式需要你像{{}} 一样逃避它们,而且看起来很丑恕我直言。
【解决方案7】:

正如我今天发现的那样,通过 % 格式化字符串的旧方法不支持 Decimal,这是 Python 用于十进制定点和浮点运算的模块,开箱即用。

示例(使用 Python 3.3.5):

#!/usr/bin/env python3

from decimal import *

getcontext().prec = 50
d = Decimal('3.12375239e-24') # no magic number, I rather produced it by banging my head on my keyboard

print('%.50f' % d)
print('{0:.50f}'.format(d))

输出:

0.000000000000000000000000312375239000000009907464850 0.000000000000000000000000312375239000000000000000000

肯定有一些变通方法,但您仍然可以考虑立即使用format() 方法。

【讨论】:

  • 这可能是因为新格式在扩展参数之前调用str(d),而旧格式可能首先调用float(d)
  • 你会这么认为,但 str(d) 返回 "3.12375239e-24",而不是 "0.00000000000000000000000312375239000000000000000000"
【解决方案8】:

附带说明一下,您不必对性能造成影响就可以使用带有日志记录的新样式格式。您可以将任何对象传递给实现__str__ 魔术方法的logging.debuglogging.info 等。当日志记录模块决定它必须发出您的消息对象(无论它是什么)时,它会在此之前调用str(message_object)。所以你可以这样做:

import logging


class NewStyleLogMessage(object):
    def __init__(self, message, *args, **kwargs):
        self.message = message
        self.args = args
        self.kwargs = kwargs

    def __str__(self):
        args = (i() if callable(i) else i for i in self.args)
        kwargs = dict((k, v() if callable(v) else v) for k, v in self.kwargs.items())

        return self.message.format(*args, **kwargs)

N = NewStyleLogMessage

# Neither one of these messages are formatted (or calculated) until they're
# needed

# Emits "Lazily formatted log entry: 123 foo" in log
logging.debug(N('Lazily formatted log entry: {0} {keyword}', 123, keyword='foo'))


def expensive_func():
    # Do something that takes a long time...
    return 'foo'

# Emits "Expensive log entry: foo" in log
logging.debug(N('Expensive log entry: {keyword}', keyword=expensive_func))

这在 Python 3 文档 (https://docs.python.org/3/howto/logging-cookbook.html#formatting-styles) 中都有描述。但是,它也适用于 Python 2.6 (https://docs.python.org/2.6/library/logging.html#using-arbitrary-objects-as-messages)。

使用这种技术的一个优点,除了它与格式风格无关之外,是它允许惰性值,例如上面的函数expensive_func。这为此处 Python 文档中给出的建议提供了一个更优雅的替代方案:https://docs.python.org/2.6/library/logging.html#optimization

【讨论】:

  • 我希望我能更多地支持这个。它允许在不影响性能的情况下使用format 进行日志记录——通过完全按照logging 的设计目的覆盖__str__ 来实现——将函数调用缩短为单个字母(N),感觉非常类似于一些定义字符串的标准方法——AND 允许延迟函数调用。谢谢! +1
  • 这与使用logging.Formatter(style='{') 参数的结果有什么不同吗?
【解决方案9】:

.format 的另一个优点(我在答案中没有看到):它可以获取对象属性。

In [12]: class A(object):
   ....:     def __init__(self, x, y):
   ....:         self.x = x
   ....:         self.y = y
   ....:         

In [13]: a = A(2,3)

In [14]: 'x is {0.x}, y is {0.y}'.format(a)
Out[14]: 'x is 2, y is 3'

或者,作为关键字参数:

In [15]: 'x is {a.x}, y is {a.y}'.format(a=a)
Out[15]: 'x is 2, y is 3'

据我所知,% 无法做到这一点。

【讨论】:

  • 与等效的 'x is {0}, y is {1}'.format(a.x, a.y) 相比,这看起来比必要的更难以阅读。仅应在 a.x 操作成本非常高时使用。
  • @dtheodor 通过调整使用关键字参数而不是位置参数...'x is {a.x}, y is {a.y}'.format(a=a)。比这两个例子更具可读性。
  • @CivFan 或者,如果您有多个对象,'x is {a.x}, y is {a.y}'.format(**vars())
  • 同样注意这一点:'{foo[bar]}'.format(foo={'bar': 'baz'})
  • 这对于面向客户的应用程序非常有用,在这些应用程序中,您的应用程序提供一组标准的格式化选项和用户提供的格式字符串。我经常用这个。例如,配置文件将具有一些“messagestring”属性,用户可以提供Your order, number {order[number]} was processed at {now:%Y-%m-%d %H:%M:%S}, will be ready at about {order[eta]:%H:%M:%S} 或任何他们想要的东西。这比尝试使用旧格式化程序提供相同的功能要干净得多。它使用户提供的格式字符串更加强大。
【解决方案10】:

% 可能有帮助的一种情况是您正在格式化正则表达式。例如,

'{type_names} [a-z]{2}'.format(type_names='triangle|square')

引发IndexError。在这种情况下,您可以使用:

'%(type_names)s [a-z]{2}' % {'type_names': 'triangle|square'}

这避免了将正则表达式写为'{type_names} [a-z]{{2}}'。当您有两个正则表达式时,这可能很有用,其中一个单独使用而没有格式,但两者的连接都已格式化。

【讨论】:

  • 或者只使用'{type_names} [a-z]{{2}}'.format(type_names='triangle|square')。这就像说.format() 在使用已经包含百分比字符的字符串时可以提供帮助。当然。那么你必须逃脱他们。
  • @Alfe 你是对的,这就是答案以"One situation where % may help is when you are formatting regex expressions." 开头的原因具体来说,假设a=r"[a-z]{2}" 是一个正则表达式块,您将在两个不同的最终表达式中使用它(例如c1 = b + ac2 = a)。假设c1 需要是formated(例如b 需要在运行时进行格式化),但c2 不需要。然后你需要a=r"[a-z]{2}" 来代替c2a=r"[a-z]{{2}}" 来代替c1.format(...)
【解决方案11】:

从 Python 3.6 (2016) 开始,您可以使用 f-strings 替换变量:

>>> origin = "London"
>>> destination = "Paris"
>>> f"from {origin} to {destination}"
'from London to Paris'

注意f" 前缀。如果您在 Python 3.5 或更早版本中尝试此操作,您将获得 SyntaxError

https://docs.python.org/3.6/reference/lexical_analysis.html#f-strings

【讨论】:

【解决方案12】:

对于 python 版本 >= 3.6(参见PEP 498

s1='albha'
s2='beta'

f'{s1}{s2:>10}'

#output
'albha      beta'

【讨论】:

    【解决方案13】:

    我要补充一点,从 3.6 版开始,我们可以像下面这样使用 fstrings

    foo = "john"
    bar = "smith"
    print(f"My name is {foo} {bar}")
    

    哪个给

    我叫约翰·史密斯

    所有内容都转换为字符串

    mylist = ["foo", "bar"]
    print(f"mylist = {mylist}")
    

    结果:

    mylist = ['foo', 'bar']

    你可以像其他格式的方法一样传递函数

    print(f'Hello, here is the date : {time.strftime("%d/%m/%Y")}')
    

    举个例子

    您好,这里是日期:16/04/2018

    【讨论】:

      【解决方案14】:

      如果您的 python >= 3.6,F 字符串格式的文字是您的新朋友。

      它更简单、干净、性能更好。

      In [1]: params=['Hello', 'adam', 42]
      
      In [2]: %timeit "%s %s, the answer to everything is %d."%(params[0],params[1],params[2])
      448 ns ± 1.48 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
      
      In [3]: %timeit "{} {}, the answer to everything is {}.".format(*params)
      449 ns ± 1.42 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
      
      In [4]: %timeit f"{params[0]} {params[1]}, the answer to everything is {params[2]}."
      12.7 ns ± 0.0129 ns per loop (mean ± std. dev. of 7 runs, 100000000 loops each)
      

      【讨论】:

        【解决方案15】:

        但有一件事是,如果您有嵌套的花括号,则不适用于格式,但 % 会起作用。

        例子:

        >>> '{{0}, {1}}'.format(1,2)
        Traceback (most recent call last):
          File "<pyshell#3>", line 1, in <module>
            '{{0}, {1}}'.format(1,2)
        ValueError: Single '}' encountered in format string
        >>> '{%s, %s}'%(1,2)
        '{1, 2}'
        >>> 
        

        【讨论】:

        • 你可以这样做,但我同意这太棒了 '{{ {0}, {1} }}'.format(1, 2)
        【解决方案16】:

        Python 3.6.7 对比:

        #!/usr/bin/env python
        import timeit
        
        def time_it(fn):
            """
            Measure time of execution of a function
            """
            def wrapper(*args, **kwargs):
                t0 = timeit.default_timer()
                fn(*args, **kwargs)
                t1 = timeit.default_timer()
                print("{0:.10f} seconds".format(t1 - t0))
            return wrapper
        
        
        @time_it
        def new_new_format(s):
            print("new_new_format:", f"{s[0]} {s[1]} {s[2]} {s[3]} {s[4]}")
        
        
        @time_it
        def new_format(s):
            print("new_format:", "{0} {1} {2} {3} {4}".format(*s))
        
        
        @time_it
        def old_format(s):
            print("old_format:", "%s %s %s %s %s" % s)
        
        
        def main():
            samples = (("uno", "dos", "tres", "cuatro", "cinco"), (1,2,3,4,5), (1.1, 2.1, 3.1, 4.1, 5.1), ("uno", 2, 3.14, "cuatro", 5.5),) 
            for s in samples:
                new_new_format(s)
                new_format(s)
                old_format(s)
                print("-----")
        
        
        if __name__ == '__main__':
            main()
        

        输出:

        new_new_format: uno dos tres cuatro cinco
        0.0000170280 seconds
        new_format: uno dos tres cuatro cinco
        0.0000046750 seconds
        old_format: uno dos tres cuatro cinco
        0.0000034820 seconds
        -----
        new_new_format: 1 2 3 4 5
        0.0000043980 seconds
        new_format: 1 2 3 4 5
        0.0000062590 seconds
        old_format: 1 2 3 4 5
        0.0000041730 seconds
        -----
        new_new_format: 1.1 2.1 3.1 4.1 5.1
        0.0000092650 seconds
        new_format: 1.1 2.1 3.1 4.1 5.1
        0.0000055340 seconds
        old_format: 1.1 2.1 3.1 4.1 5.1
        0.0000052130 seconds
        -----
        new_new_format: uno 2 3.14 cuatro 5.5
        0.0000053380 seconds
        new_format: uno 2 3.14 cuatro 5.5
        0.0000047570 seconds
        old_format: uno 2 3.14 cuatro 5.5
        0.0000045320 seconds
        -----
        

        【讨论】:

        • 您应该多次运行每个示例,一次运行可能会产生误导,例如操作系统可能通常很忙,因此代码的执行会延迟。请参阅文档:docs.python.org/3/library/timeit.html。 (好头像,Guybrush!)
        猜你喜欢
        • 2023-03-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-04-06
        • 2023-03-08
        • 1970-01-01
        相关资源
        最近更新 更多