为了账号安全,请及时绑定邮箱和手机立即绑定

Python字符串“ join”比“ +”要快(?),但这里出了什么问题?

/ 猿问

Python字符串“ join”比“ +”要快(?),但这里出了什么问题?

慕圣8478803 2019-09-24 16:19:50

我在较早的帖子中询问了用于批量动态字符串连接的最有效方法,并建议我使用join方法,这是最好,最简单和最快的方法(每个人都这么说)。但是,当我使用字符串连接时,我发现了一些奇怪的结果。我敢肯定有些事情正在发生,但我不能完全理解。这是我所做的:


我定义了以下功能:


import timeit

def x():

    s=[]

    for i in range(100):

        # Other codes here...

        s.append("abcdefg"[i%7])

    return ''.join(s)


def y():

    s=''

    for i in range(100):

        # Other codes here...

        s+="abcdefg"[i%7]

    return s


def z():

    s=''

    for i in range(100):

        # Other codes here...

        s=s+"abcdefg"[i%7]

    return s


def p():

    s=[]

    for i in range(100):

        # Other codes here...

        s+="abcdefg"[i%7]

    return ''.join(s)


def q():

    s=[]

    for i in range(100):

        # Other codes here...

        s = s + ["abcdefg"[i%7]]

    return ''.join(s)

我试图使其他功能(除了串联)在整个函数中几乎相同。然后,我对以下内容进行了测试,并在注释中给出了结果(在Windows 32位计算机上使用Python 3.1.1 IDLE):


timeit.timeit(x) # 31.54912480500002

timeit.timeit(y) # 23.533029429999942 

timeit.timeit(z) # 22.116181330000018

timeit.timeit(p) # 37.718607439999914

timeit.timeit(q) # 108.60377576499991

这意味着它表明strng = strng + dyn_strng是最快的。尽管时间差异不是很大(最后一个除外),但是我想知道为什么会这样。那是因为我使用的是Python 3.1.1,并且提供的“ +”效率最高?我应该使用“ +”代替加入吗?还是我做了一件非常愚蠢的事情?要不然是啥?请清楚解释。


查看完整描述

3 回答

?
慕的地2183247

我们中的一些Python提交者(我相信主要是Rigo和Hettinger)竭尽全力(我相信会达到2.5)优化了一些非常罕见的s += something 枯萎病的特殊情况,他们认为事实证明是初学者永远不会相信这''.join是正确的方法,而可怕的缓慢+=可能会给Python起一个坏名声。我们中的其他人并没有那么热,因为他们无法优化每次发生(甚至只是其中的大多数)来获得不错的性能。但是我们对这个问题的热情还不足以尝试积极地阻止他们。


我相信,这条线索证明了我们应该更加严厉地反对他们。到目前为止,他们+=在某些难以预测的案例子集中进行了优化,使某些愚蠢案例的速度可能比正确方法快20%(''.join)-这是诱使初学者通过使用错误的惯用语来追求那些无关紧要的20%收益的完美方法...为此付出了代价,偶尔并从其POV中突然失去了性能损失200%(或更多,因为非线性行为仍然潜伏在Hettinger和Rigo喷粉并放花的角落之外;-)-很重要的一种,会使他们痛苦的。这违背了Python的“理想地,唯一的一种明显的实现方式”,在我看来,我们集体为初学者设置了陷阱-也是最好的一种……那些不只是接受的人他们的“优胜者”会告诉他们什么,但会好奇地去问和探索。


嗯-我放弃了。OP,@ mshsayem,继续前进,在任何地方使用+ =,在平凡,细微,无关紧要的情况下享受不相关的20%加速,并且最好让他们尽情享受-因为有一天,当您看不到它时即将进行的一项重要的大型操作中,即将到来的挂车会以200%的速度减速(除非您倒霉,这是2000%的速度;-),在中速行驶时您会被打中。只要记住:记住,如果您觉得“ Python太慢了”,那很可能是您最喜欢的+=转过身并咬住喂食它的手的循环之一。


对于我们其他人-那些理解说什么意思的人来说,我们应该忘掉效率低下的问题,大约有97%的时间会说,我会一直坚持不懈地建议''.join,这样我们大家都可以安逸地入睡,知道我们会做到的。当我们最不期望并且最不能负担得起时,您就不会受到超线性减速的打击。但是对您来说,阿明·里戈(Armin Rigo)和雷蒙德·海廷格(Raymond Hettinger)(最后两个,我亲爱的私人朋友,顺便说一句,而不仅仅是共同承诺人;-)-也许您+=会很顺利,并且大O永远不会比N更糟!


因此,对于我们其他人,这是一组更有意义和有趣的度量标准:


$ python -mtimeit -s'r=[str(x)*99 for x in xrange(100,1000)]' 's="".join(r)'

1000 loops, best of 3: 319 usec per loop

900个字符串,每个297个字符,直接加入列表当然是最快的,但是OP害怕在此之前必须执行附加操作。但:


$ python -mtimeit -s'r=[str(x)*99 for x in xrange(100,1000)]' 's=""' 'for x in r: s+=x'

1000 loops, best of 3: 779 usec per loop

$ python -mtimeit -s'r=[str(x)*99 for x in xrange(100,1000)]' 'z=[]' 'for x in r: z.append(x)' '"".join(z)'

1000 loops, best of 3: 538 usec per loop

...具有半重要的数据量(只有几百个KB的数据-每种方式花费毫秒的可测量分数),即使是普通的旧数据.append也已经非常出色。此外,优化显然也很容易:


$ python -mtimeit -s'r=[str(x)*99 for x in xrange(100,1000)]' 'z=[]; zap=z.append' 'for x in r: zap(x)' '"".join(z)'

1000 loops, best of 3: 438 usec per loop

在平均循环时间上又节省了十分之一毫秒。每个人(至少每个人都完全痴迷于出色的性能)显然都知道HOISTING(将内循环带走一次重复的计算,否则会反复执行)是优化中的关键技术-Python不会代表您提升,因此您必须在每个微秒都很重要的罕见情况下进行自己的吊装。


查看完整回答
反对 回复 2019-09-24
?
精慕HU

至于为什么q慢很多:当你说


l += "a"

您将字符串追加"a"到的末尾l,但是当您说


l = l + ["a"]

要创建一个使用的内容的新列表l和["a"],然后重新分配结果回l。因此,不断产生新的清单。


查看完整回答
反对 回复 2019-09-24
?
宝慕林4294392

我已经从专家在这里发布的答案中找出答案。Python字符串连接(和时序测量)取决于这些(据我所知):


串联数

平均弦长

函数调用次数

我建立了一个与此相关的新代码。感谢Peter S Magnusson,sepp2k,hughdbrown,David Wolever和其他人指出了我早些时候错过的重点。另外,在这段代码中,我可能错过了一些东西。因此,对于任何指出我们的错误,建议,批评等的答复,我深表感谢。毕竟,我是在这里学习。这是我的新代码:


from timeit import timeit


noc = 100

tocat = "a"

def f_call():

    pass


def loop_only():

    for i in range(noc):

        pass


def concat_method():

    s = ''

    for i in range(noc):

        s = s + tocat


def list_append():

    s=[]

    for i in range(noc):

        s.append(tocat)

    ''.join(s)


def list_append_opt():

    s = []

    zap = s.append

    for i in range(noc):

        zap(tocat)

    ''.join(s)


def list_comp():

    ''.join(tocat for i in range(noc))


def concat_method_buildup():

    s=''


def list_append_buildup():

    s=[]


def list_append_opt_buildup():

    s=[]

    zap = s.append


def function_time(f):

    return timeit(f,number=1000)*1000


f_callt = function_time(f_call)


def measure(ftuple,n,tc):

    global noc,tocat

    noc = n

    tocat = tc

    loopt = function_time(loop_only) - f_callt

    buildup_time = function_time(ftuple[1]) -f_callt if ftuple[1] else 0

    total_time = function_time(ftuple[0])

    return total_time, total_time - f_callt - buildup_time - loopt*ftuple[2]


functions ={'Concat Method\t\t':(concat_method,concat_method_buildup,True),

            'List append\t\t\t':(list_append,list_append_buildup,True),

            'Optimized list append':(list_append_opt,list_append_opt_buildup,True),

            'List comp\t\t\t':(list_comp,0,False)}


for i in range(5):

    print("\n\n%d concatenation\t\t\t\t10'a'\t\t\t\t 100'a'\t\t\t1000'a'"%10**i)

    print('-'*80)

    for (f,ft) in functions.items():

        print(f,"\t|",end="\t")

        for j in range(3):

            t = measure(ft,10**i,'a'*10**j)

            print("%.3f %.3f |" % t,end="\t")

        print()

这就是我所拥有的。[在时间列中显示了两次(标度):第一个是总函数执行时间,第二个是实际(?)串联时间。我已经扣除了函数调用时间,函数建立时间(初始化时间)和迭代时间。在这里,我正在考虑一种情况,即如果没有循环就无法做到这一点(说里面的更多声明)。


1 concatenation                 1'a'                  10'a'               100'a'

-------------------     ----------------------  -------------------  ----------------

List comp               |   2.310 2.168       |  2.298 2.156       |  2.304 2.162

Optimized list append   |   1.069 0.439       |  1.098 0.456       |  1.071 0.413

Concat Method           |   0.552 0.034       |  0.541 0.025       |  0.565 0.048

List append             |   1.099 0.557       |  1.099 0.552       |  1.094 0.552



10 concatenations                1'a'                  10'a'               100'a'

-------------------     ----------------------  -------------------  ----------------

List comp               |   3.366 3.224       |  3.473 3.331       |  4.058 3.916

Optimized list append   |   2.778 2.003       |  2.956 2.186       |  3.417 2.639

Concat Method           |   1.602 0.943       |  1.910 1.259       |  3.381 2.724

List append             |   3.290 2.612       |  3.378 2.699       |  3.959 3.282



100 concatenations               1'a'                  10'a'               100'a'

-------------------     ----------------------  -------------------  ----------------

List comp               |   15.900 15.758     |  17.086 16.944     |  20.260 20.118

Optimized list append   |   15.178 12.585     |  16.203 13.527     |  19.336 16.703

Concat Method           |   10.937 8.482      |  25.731 23.263     |  29.390 26.934

List append             |   20.515 18.031     |  21.599 19.115     |  24.487 22.003



1000 concatenations               1'a'                  10'a'               100'a'

-------------------     ----------------------  -------------------  ----------------

List comp               |   134.507 134.365   |  143.913 143.771   |  201.062 200.920

Optimized list append   |   112.018 77.525    |  121.487 87.419    |  151.063 117.059

Concat Method           |   214.329 180.093   |  290.380 256.515   |  324.572 290.720

List append             |   167.625 133.619   |  176.241 142.267   |  205.259 171.313



10000 concatenations              1'a'                  10'a'               100'a'

-------------------     ----------------------  -------------------  ----------------

List comp               |   1309.702 1309.560 |  1404.191 1404.049 |  2912.483 2912.341

Optimized list append   |   1042.271 668.696  |  1134.404 761.036  |  2628.882 2255.804

Concat Method           |   2310.204 1941.096 |  2923.805 2550.803 |  STUCK    STUCK

List append             |   1624.795 1251.589 |  1717.501 1345.137 |  3182.347 2809.233

综上所述,我为我做出了以下决定:


如果您有可用的字符串列表,则字符串“ join”方法是最好和最快的。

如果您可以使用列表理解,那也是最简单,最快的方法。

如果您需要1到10个串联(平均),长度为1到100,则列表附加,“ +”都需要相同的时间(几乎要注意,时间是按比例缩放的)。

在大多数情况下,优化的列表追加似乎非常好。

当#concatenation或字符串长度增加时,“ +”开始花费越来越多的时间。请注意,对于与100'a'的10000个串联,我的PC卡住了!

如果始终使用列表追加和“加入”,则始终可以安全(由Alex Martelli指出)。

但是在某些情况下,您需要输入用户输入并打印“ Hello user's world!”(您好,世界!),使用“ +”最简单。我认为针对这种情况建立一个列表并加入x = input(“ Enter user name:”)然后x.join([“ Hello”,“'s world!”])比“ Hello%s的世界! “%x或” Hello“ + x +”的世界“

Python 3.1改进了串联性能。但是,在像Jython这样的实现中,“ +”的效率较低。

过早的优化是万恶之源(专家的说法)。大多数时候,您不需要优化。因此,不要浪费时间进行优化(除非您正在编写一个大型或计算项目,每个微秒都在计算。)

使用这些信息并以您喜欢的任何方式写,要考虑到具体情况。

如果您确实需要优化,请使用探查器,找到瓶颈并尝试对其进行优化。

最后,我试图更深入地学习python。因此,在我的观察中会出现错误(错误)的现象并不罕见。因此,请对此发表评论,并建议我是否选择错误的路线。感谢大家的参与。


查看完整回答
反对 回复 2019-09-24

添加回答

回复

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信