Python字符串连接循环

Python字符串连接循环,python,Python,我对%timeit为这两种实现提供的结果感到惊讶: def f1(): s = '' for i in range(len(values)):

我对%timeit为这两种实现提供的结果感到惊讶:

def f1():                                                                       
    s = ''                                                                    
    for i in range(len(values)):                                                
        s += str(values[i][0])                                                
        s += '\t'                                                             
        s += str(values[i][1])                                    
        s += '\r\n'                                                           
    return s  

知道
是一个大约2400个元组的列表
f1()
是我在一位同事编写的脚本中发现的原始代码,他在编写脚本时更习惯于C/C++而不是Python,而
f2
是我为同样的处理编写的更具Python风格的代码

我本以为
f2
f1
快得多,主要是因为
f1
使用了大量的串联和字符串重新分配,但发生的情况是
%timeit
为这两种情况提供了相同的数量级(约18ns),更令人惊讶的是,有时会使
f2
快1ns

这一结果的原因是什么


[7月14日编辑]修复了由同名局部变量重写的
str
的f1错误。但是,分析代码中不存在此错误。

由于以下原因,f2代码仍然由字符串连接绑定:

str(ts)+'\t'+str(v)+'\r\n'

它比同样基于字符串concat的原始版本更差的事实可能是由于中提到的实现细节

如果将内部连接更改为同时使用join,则会获得更好的性能

def f2(values):                                                                       
    return '\r\n'.join(
        ('\t'.join([str(ts), str(v)])
      for ts, v in values))

由于观察到的结果有点令人惊讶,我使用以下脚本以不同的方式进行了相同的分析:

import random
import timeit

data = [(random.randint(0, 100000), random.randint(0, 1000)) for _ in range(0, 2500)]

def f1():
    return ''.join(('{}\t{}\r\n'.format(ts, v) for ts, v in data))

def f2():
    s = ''
    for i in range(len(data)):
        s += str(data[i][0])
        s += '\t'
        s += str(data[i][1])
        s += '\r\n'

    return s


if __name__ == '__main__':
    repeat = 10000

    for f in ['f1', 'f2']:
        t = timeit.timeit(
            '%s()' % f, number=repeat, setup="from __main__ import %s" % f
        )
        print(
            "%s : avg time per loop = %f ms" % (f, t * 1000 / repeat)
        )
现在输出为:

    f1 : avg time per loop = 0.779966 ms
    f2 : avg time per loop = 1.144340 ms
这更符合预期结果


我将进行更多调查,以了解两种测试之间的行为差异。

我可以相当确信您的测试方法是无效的,如和所示。代码相同,如下所示,但结果不同:

f1是您的f1函数
f2是您的f2函数
f3是使用c样式字符串格式的f2函数
%s”%str

f4是使用
.format()

结果:

Python 2.7.10 (default, Jul 14 2015, 19:46:27)
[GCC 4.8.2] on linux

1.67547893524
1.33767485619
0.72606086731
1.32540607452
有一些不同,但在任何情况下,f1都不优于以下任何一种方法

Python 3.6.1 (default, Dec 2015, 13:05:11)
[GCC 4.8.2] on linux

3.0050943629757967
2.016791722999187
0.9476796620001551
1.9396837950043846
在这两种情况下,c样式字符串的格式化速度都是原来的两倍多

使用的功能包括:

def f1():
    s = ''
    for i in range(len(values)):
        s += str(values[i][0])
        s += '\t'
        s += str(values[i][1])
        s += '\r\n'
    return s

def f2():    
    return ''.join((                        
        str(ts) + '\t' + str(v) + '\r\n'                
        for ts, v in values))

def f3():           
    return ''.join((
        "%s\t%s\r\n" % (ts, v)  
        for ts, v in values))

def f4():
    return ''.join((
        "{}\t{}\r\n".format(ts, v)
        for ts, v in values))
有趣的是,通过对f1函数进行一个小的更改,我们可以通过danny的引用获得一个不错的加速:

def f1opt():
    s = ''
    for i in range(len(values)):
        s += str(values[i][0]) + '\t' + str(values[i][1]) + '\r\n'
    return s
屈服

Python 2.7.10 (default, Jul 14 2015, 19:46:27)
[GCC 4.8.2] on linux

f1()         1.68486714363
f1bytecode() 0.999644994736

@ayhan我在发布之前做了一些搜索,但是找不到与这个例子直接相关的答案。你能告诉我这道题是哪道题的副本吗?这样我就可以研究他们的答案了。我相信这道题解释了这个问题。如果你认为还有什么原因没有解释,我可以重新开始这个问题。谢谢你的参考,但是那里的结论似乎支持
+=
不如
加入
,这不是我测试报告的行为。这个矛盾(而不是“+=vs.join”)是我问题的重点。因此,我不确定我的问题是否与您提到的问题重复。这一“实施细节”是否解释了矛盾?它通常是O(n^2),但是在适当的位置调整字符串的大小可能会使其成为O(n),这就是join的复杂性?@TemporalWolf感谢您为我指出这一点。我对原始代码的糟糕错误编辑,为了简单起见,我在将其包含在文章中之前删除了额外的内容。无论如何,我用%timeit分析的函数不包含此错误。对于如此短的列表,对
join
的内部调用是过分的;只需使用
'{}\t{}.format(ts,v)
,或整体
返回'\r\n'.join([{}\t{}.format(*p)表示值中的p])
。就我个人而言,我可能会使用
返回“\r\n”。join(“%s\t%s”%表示值中的(a,b)
,因为它对我来说可读性更强一些,但它的形式与“@chepner和应该可以与big-O POV相比。如果使用了大小不正确的元组,则也会出现错误(
ValueError:太多的值无法解包
),而值中p的格式(*p)将忽略其他术语。使用字符串插值器会比暴力串联结果长5%。格式化方法版本的速度或多或少与连接相同。然而,我的问题不是关于内部项目的构建,而是关于整体结果的构建,连接方法在性能方面没有给“累积”连接带来预期的好处。我肯定错过了一些明显的东西。感谢临时狼花时间在你这边调查。关于你的评论有两个问题:1/为什么你的代码中的后斜杠加倍了?这是否会导致将文本'\t'(例如)添加到字符串中,而不是按预期添加到表格中?2/我不理解f1opt的逻辑:它似乎附加元组的一部分或另一部分,这取决于我们是在奇数项上还是在偶数项上,但不是想要的两个。也许这就是性能提高的原因。理解双“\\”:这是因为代码摘录是从函数源的字符串版本中提取的,而不是从脚本中提取的。@EricPASCUAL是的,我会把它们取出来:)@EricPASCUAL我刚刚在replit on&中再次测试了它。两种方法都能如期工作。作为
if/else
,您不需要任何括号来解释它。“我可以相当确信您的测试方法是无效的”:您当然是对的。我现在觉得有点愚蠢,因为问题是在iPython中工作时,我没有对各种实现进行计时,而是。。。由于未正确生成测试数据而导致异常引发。我真的很抱歉问了这么多没用的问题。只是希望贡献者给出的答案能够给其他读者带来一些关于各种代码模式性能的信息。感谢大家的耐心。如前一个答案的评论中所详细说明的,这个问题存在于使用algo进行实验时测试方法中的一个bug中
Python 2.7.10 (default, Jul 14 2015, 19:46:27)
[GCC 4.8.2] on linux

f1()         1.68486714363
f1bytecode() 0.999644994736