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)])
吉米

我不是Ruby专家,但是我敢打赌插值会更快,因为在Ruby中字符串是可变的。字符串是Python中的不可变序列。
gotgenes

1
关于URI的一些评论。URI并不完全像字符串。有URI,因此在连接或比较它们时必须非常小心。示例:服务器通过端口80上的http传送其表示。串。
karlcow

Answers:


55

根据我的机器,连接的速度(明显)更快。但从风格上讲,如果性能不是很关键,我愿意付出替代的代价。好吧,如果我需要格式化,就不用问这个问题了……别无选择,只能使用插值/模板化。

>>> 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
您是否使用真正的大字符串(例如100000个字符)进行测试?
drnk

24

不要忘记命名替换:

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

4
此代码至少有2种不良的编程习惯:期望全局变量(未在函数内部声明域和问题),以及将超出所需数量的变量传递给format()函数。拒绝投票,因为此答案会教您不良的编码习惯。
jperelli 2016年

12

小心将字符串串联在一起! 字符串连接的代价与结果的长度成正比。循环使您直接进入N平方的区域。某些语言会优化串联到最近分配的字符串,但是依靠编译器将二次算法优化到线性优化是有风险的。最好使用原语(join?),该原语接收整个字符串列表,进行一次分配,然后一次性将它们全部串联起来。


16
那不是当前的。在最新版本的python中,当您在循环中连接字符串时会创建一个隐藏的字符串缓冲区。
Seun Osewa 2009年

5
@Seun:是的,正如我所说,有些语言会优化,但这是冒险的做法。
Norman Ramsey

11

“由于字符串串联已经大大提高了性能……”

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

但是,我所见过的性能问题从未归结为字符串操作。我通常遇到I / O,排序和O(n 2)操作成为瓶颈的麻烦。

在字符串操作成为性能限制因素之前,我将坚持显而易见的事情。通常,当一行或更少行时,这是替换;当有意义时,则是串联;当它很大时,则是模板工具(例如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,它就会发生。另外,请参见Peter的评论,以了解您仍然可以使用%运算符进行命名替换的事实。
John Fouhy

“当您没有固定数量的项目要字符串化时,级联很有用。” -您的意思是列表/数组?在这种情况下,您不可以只加入()吗?
斯特拉格

“你不能只是加入他们吗?” -是的(假设您希望项目之间使用统一的分隔符)。列表和生成器理解可与string.join一起使用。
蒂姆·莱瑟

1
“好吧,每当您决定使用python 3.0时,它就会发生。” —不,py3k仍然支持%运算符。下一个可能的弃用点是3.1,因此它仍然具有生命。
蒂姆·莱瑟

2
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),我发现%方法的速度大约是这些小字符串上其他方法的两倍。concat方法始终是最慢的(很少)。切换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),常规concat的速度几乎是使用formatand %方法的三倍(1.1 vs 2.8)。我想这取决于字符串以及您要实现的目标。如果性能确实很重要,那么尝试不同的东西并进行测试可能会更好。除非速度成为问题,否则我倾向于选择可读性而不是速度,但这就是我。所以不喜欢我的复制/粘贴,我必须在所有内容上放置8个空格以使其看起来正确。我通常使用4。


1
您应该认真考虑您正在分析的内容。对于一个,您的concat很慢,因为其中有两个str强制转换。对于字符串,结果是相反的,因为当仅涉及三个字符串时,字符串concat实际上比所有替代方法都快。
Justus Wingert

@JustusWingert,今年两岁。自发布此“测试”以来,我学到了很多东西。说实话,这几天我用str.format()str.join()过正常的串联。我也一直在关注最近被接受的PEP 498中的 “ f弦” 。至于str()影响性能的电话,我相信您是正确的。我当时不知道函数调用是多么昂贵。我仍然认为,如有疑问,应进行测试。
Cj Welborn

用进行快速测试后join_(): return ''.join(["test ", str(1), ", with number ", str(2)]),它似乎join也慢于百分比。
令人称奇的

4

请记住,如果您打算维护或调试代码,则风格决定实际的决定:-) Knuth有句著名的名言(可能引述Hoare?):“我们应该忘记效率低下的问题,大约有97%的时间是这样:过早的优化是万恶之源。”

只要您小心谨慎,不要(例如)将O(n)任务转换为O(n 2)任务,无论您发现最容易理解的是什么,我都会选择。


0

我会尽一切可能使用替代。如果要在for循环中构建字符串,则仅使用串联。


7
“建立一个字符串一个for循环” -通常这是你可以使用“”。加入和发电机表达的情况下..
约翰·福希

-1

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


1
对于os路径(例如在文件系统上)是正确的,但在本示例中构造URI时不是这样。URI始终以'/'作为分隔符。
安德烈·布鲁姆
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.