Python中的字符串拼接和字符串替换

98

在Python中,我不清楚何时以及何地使用字符串连接和字符串替换。由于字符串连接在性能方面有了大幅提升,这是否变成了更多的风格决策而非实用决策?

作为具体示例,如何处理灵活的URI构建:

DOMAIN = 'http://stackoverflow.com'
QUESTIONS = '/questions'

def so_question_uri_sub(q_num):
    return "%s%s/%d" % (DOMAIN, QUESTIONS, q_num)

def so_question_uri_cat(q_num):
    return DOMAIN + QUESTIONS + '/' + str(q_num)

编辑:还有建议将字符串列表连接起来以及使用命名替换的建议。这些都是围绕着核心主题的变体,即在何时采用哪种方式是正确的?感谢大家的回答!


有趣的是,在Ruby中,字符串插值通常比连接更快... - Keltia
你忘记了返回值 "".join([DOMAIN, QUESTIONS, str(q_num)])。 - Jimmy
我不是Ruby专家,但我敢打赌插值在Ruby中更快,因为字符串在Ruby中是可变的。而在Python中,字符串是不可变序列。 - gotgenes
1
关于URI的一点注释。URI并不完全像字符串。因为有URI存在,所以在连接或比较它们时必须非常小心。例如:服务器通过端口80上的http传递其表示。example.org(末尾没有斜杠) example.org/(斜杠) example.org:80/(斜杠+端口80)是相同的URI但不是相同的字符串。 - karlcow
9个回答

55

根据我的机器,拼接(concatenation)的速度更快。但从风格上讲,如果性能不是关键问题,我愿意支付替换(substitution)的代价。嗯,如果我需要格式化,就没有必要问了...除了使用插值/模板之外,没有其他选择。

>>> import timeit
>>> def so_q_sub(n):
...  return "%s%s/%d" % (DOMAIN, QUESTIONS, n)
...
>>> so_q_sub(1000)
'http://stackoverflow.com/questions/1000'
>>> def so_q_cat(n):
...  return DOMAIN + QUESTIONS + '/' + str(n)
...
>>> so_q_cat(1000)
'http://stackoverflow.com/questions/1000'
>>> t1 = timeit.Timer('so_q_sub(1000)','from __main__ import so_q_sub')
>>> t2 = timeit.Timer('so_q_cat(1000)','from __main__ import so_q_cat')
>>> t1.timeit(number=10000000)
12.166618871951641
>>> t2.timeit(number=10000000)
5.7813972166853773
>>> t1.timeit(number=1)
1.103492206766532e-05
>>> t2.timeit(number=1)
8.5206360154188587e-06

>>> def so_q_tmp(n):
...  return "{d}{q}/{n}".format(d=DOMAIN,q=QUESTIONS,n=n)
...
>>> so_q_tmp(1000)
'http://stackoverflow.com/questions/1000'
>>> t3= timeit.Timer('so_q_tmp(1000)','from __main__ import so_q_tmp')
>>> t3.timeit(number=10000000)
14.564135316080637

>>> def so_q_join(n):
...  return ''.join([DOMAIN,QUESTIONS,'/',str(n)])
...
>>> so_q_join(1000)
'http://stackoverflow.com/questions/1000'
>>> t4= timeit.Timer('so_q_join(1000)','from __main__ import so_q_join')
>>> t4.timeit(number=10000000)
9.4431309007150048

10
你是否进行过使用真实大字符串(例如100,000个字符)的测试? - drnk

24

别忘了命名替换:

def so_question_uri_namedsub(q_num):
    return "%(domain)s%(questions)s/%(q_num)d" % locals()

4
这段代码存在至少两个不良编程实践:期望全局变量(域和问题未在函数内声明)和向 format() 函数传递比必要变量更多的变量。因为这个答案教授了不良编码实践,所以我会投反对票。 - jperelli

12

在循环中连接字符串时要小心!字符串连接的成本与结果的长度成正比。循环会让你直接走向 N 平方的领域。有些语言会将连接优化到最近分配的字符串,但指望编译器将你的二次算法优化为线性是有风险的。最好使用原始的 (join?) 函数来接收一个完整的字符串列表,在一次分配中将它们全部连接起来。


16
在最新版本的Python里,当你在循环中连接字符串时,会创建一个隐藏的字符串缓冲区,这与当前的情况不同。 - Seun Osewa
5
@Seun:是的,正如我所说,有些语言会进行优化,但这是一种风险很高的做法。 - Norman Ramsey

11

“由于字符串拼接操作性能有了显著提升…”

如果性能很重要,这是一个好消息。

然而,我曾遇到的性能问题从来不是因为字符串操作。通常是由于 I/O、排序以及O(n2) 操作成为瓶颈。

在字符串操作不是性能限制因素之前,我会坚持使用显而易见的方法。主要是当替换不超过一行时采用替换,需要时采用字符串连接,并且使用模板工具(如Mako)进行大规模操作。


10
你想要连接/插值的内容和你想要格式化的结果应该是决定使用哪种方法的关键。
  • 字符串插值允许你轻松添加格式。实际上,你的字符串插值版本与字符串拼接版本并不相同;它在q_num参数之前实际上添加了一个额外的斜杠。要做同样的事情,你需要在那个例子中写return DOMAIN + QUESTIONS + "/" + str(q_num)

  • 插值使数字格式化更容易;"%d of %d (%2.2f%%)" % (current, total, total/current) 在连接形式下会更加难以阅读。

  • 当你没有固定的要转换成字符串的项数时,字符串拼接很有用。

此外,请注意,Python 2.6引入了一种新的字符串插值方法,称为字符串模板

def so_question_uri_template(q_num):
    return "{domain}/{questions}/{num}".format(domain=DOMAIN,
                                               questions=QUESTIONS,
                                               num=q_num)

字符串模板化计划最终取代%-插值,但我认为这将需要相当长的时间。


这将会在你决定迁移到Python 3.0时发生。另外,注意彼得的评论,因为你仍然可以使用%运算符进行命名替换。 - John Fouhy
“当你不确定要将多少项连接成字符串时,串联是非常有用的。”-- 你是指列表/数组吗?如果是这样,你为什么不直接使用join()函数呢? - strager
“你能不能只把它们join起来?”-可以(假设你想要在项之间使用统一的分隔符)。列表和生成器推导式与string.join功能完美匹配。 - Tim Lesher
1
“好吧,只要你决定转移到Python 3.0,这就会发生。”-- 不,Py3k仍然支持%运算符。下一个可能的弃用点是3.1,因此它仍然有一些生命力。 - Tim Lesher
2
两年后...Python 3.2 即将发布,% 样式插值仍然可用。 - Corey Goldberg

8

我只是出于好奇测试不同的字符串拼接/替换方法的速度。在谷歌上搜索这个主题,我来到了这里。我想发布我的测试结果,希望能帮助某些人做出决定。

    import timeit
    def percent_():
            return "test %s, with number %s" % (1,2)

    def format_():
            return "test {}, with number {}".format(1,2)

    def format2_():
            return "test {1}, with number {0}".format(2,1)

    def concat_():
            return "test " + str(1) + ", with number " + str(2)

    def dotimers(func_list):
            # runs a single test for all functions in the list
            for func in func_list:
                    tmr = timeit.Timer(func)
                    res = tmr.timeit()
                    print "test " + func.func_name + ": " + str(res)

    def runtests(func_list, runs=5):
            # runs multiple tests for all functions in the list
            for i in range(runs):
                    print "----------- TEST #" + str(i + 1)
                    dotimers(func_list)

在运行runtests((percent_, format_, format2_, concat_), runs=5)后,我发现在这些小字符串上,%方法的速度大约是其他方法的两倍。连接方法始终是最慢的(几乎可以忽略不计)。在format()方法中切换位置时可能会有微小的差异,但是切换位置总是比常规的format方法慢至少0.01秒。
测试结果示例:
    test concat_()  : 0.62  (0.61 to 0.63)
    test format_()  : 0.56  (consistently 0.56)
    test format2_() : 0.58  (0.57 to 0.59)
    test percent_() : 0.34  (0.33 to 0.35)

我运行了这些代码,因为我的脚本确实使用字符串拼接,我想知道它的成本是多少。我按不同的顺序运行它们,以确保没有任何干扰或者先后顺序对性能有更好的影响。顺便说一句,我在这些函数中添加了一些更长的字符串生成器,比如"%s" + ("a" * 1024),普通的连接方式几乎比使用format%方法快3倍(1.1与2.8)。我想这取决于字符串和你要实现的目标。如果性能真的很重要,最好尝试不同的方法并进行测试。除非速度成为问题,否则我倾向于选择可读性而不是速度,但这只是我的个人观点。SO不喜欢我的复制/粘贴,我必须在每个地方放置8个空格才能使其看起来正确。我通常使用4个。


1
你应该认真考虑你正在进行的性能分析。例如,你的concat操作很慢,因为其中有两个字符串转换。对于字符串而言,当只涉及三个字符串时,字符串连接实际上比所有其他替代方案都要快。 - Justus Wingert
@JustusWingert,这已经两年了。自从我发布这个“测试”以来,我学到了很多东西。老实说,现在我使用str.format()str.join()而不是普通的连接。我也在关注PEP 498中的'f-strings',它最近已被接受。至于str()调用对性能的影响,我相信你是正确的。那时我不知道函数调用有多昂贵。我仍然认为,在有任何疑问时应进行测试。 - Cj Welborn
经过使用 join_(): return ''.join(["test ", str(1), ", with number ", str(2)]) 进行快速测试,似乎 join 比百分号运算符更慢。 - gaborous

4
记住,风格决策是实际决策的一部分,如果你计划维护或调试代码 :-)。有一句出自 Knuth 的名言(可能引用 Hoare?):“我们应该忘记小优化,大约 97% 的时间:过早的优化是万恶之源。”
只要你小心不将 O(n) 的任务转换为 O(n^2) 的任务,我会选择你认为最容易理解的方式。

0

我尽可能使用替换。只有在构建字符串时,例如在for循环中,我才会使用连接。


7
在for循环中构建字符串时,通常可以使用''.join和生成器表达式。 - John Fouhy

-1

实际上,在这种情况下(构建路径),正确的做法是使用os.path.join,而不是字符串拼接或插值。


1
这对于操作系统路径(如在文件系统中)是正确的,但在构造URI时不是。URI始终使用“/”作为分隔符,就像在此示例中一样。 - Andre Blum

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接