【问题标题】:Python string concatenation in dictionary entries字典条目中的 Python 字符串连接
【发布时间】:2019-04-04 22:06:14
【问题描述】:

我必须将许多短字符串连接到一个字典条目中,我意识到这很慢(大致是二次的)。但是,如果预先将字符串连接起来,然后添加到字典中,那么连接时间几乎是线性的。

这是一个简单的插图。此函数基本上连接许多字符串并创建一个包含连接字符串的单个条目的字典。第一个函数直接在条目“d[key] += str(num)”上执行,第二个函数在字符串“out_str += str(num)”上执行。下面打印了时间,可以看到二次和线性行为。

我想知道开销是从哪里来的。谢谢!

def method1(loop_count):
    """ String concatenation on dictionary entry directly (slow)"""
    out_str = ''
    d={}
    key = 'key' 
    d[key] = ''
    for num in range(loop_count):
        d[key] += str(num)
    return d

def method2(loop_count):
    """ Concatenation 'on string' and then add to dictionary (fast) """
    out_str = ''
    d={}
    key = 'key' 
    for num in range(loop_count):
        out_str += str(num)

    d[key] = out_str
    return d

def method3(loop_count):
    """ Concatenation 'on string' and then add to dictionary (fast) """
    out_str = ''
    d={}
    key = 'key'

    out_str = ''.join(str(n) for n in range(loop_count))

    d[key] = out_str
    return d

from timeit import default_timer as timer
import numpy as np
for p in range(10,20):
    t0 = timer()
    method1(np.power(2,p))
    t1 = timer()
    method2(np.power(2,p))
    t2 = timer()
    method3(np.power(2,p))
    t3 = timer()
    print("2^{}:\t{:4.2g}\t{:4.2g}\t{:4.2g}".format(p, t1-t0, t2-t1, t3-t2))

        in dict   +=    join
2^10:   0.0003  0.0002  0.0002
2^11:   0.00069 0.0004  0.00038
2^12:   0.0017  0.00079 0.00076
2^13:   0.0057  0.0016  0.0015
2^14:   0.021   0.0032  0.0031
2^15:   0.095   0.0065  0.0065
2^16:   0.77    0.013   0.013
2^17:    3.2    0.026   0.027
2^18:     15    0.052   0.052
2^19:     67     0.1    0.11

注意:这不是一个严格意义上关于有效字符串连接的问题。它是关于何时在字符串连接中进行字符串优化。

注意 2:我使用“join”习语添加了第三种方法,它与 += 所用的时间完全相同


正如答案所暗示的,这似乎是一个优化问题。进一步的测试似乎证明了这一点。下面的代码显示 += 重用了许多字符串,无论字典条目中的连接是否:

a=''
for n in range(10):
    print(id(a))
    a+=str(n)

140126222965424
140126043294720
140126043294720
140126043294720
140126043294720
140126043294720
140126043294720
140126043294720
140126042796464
140126042796464

d={}
d['key']=''
for n in range(10):
    print(id(d['key']))
    d['key']+=str(n)

140126222965424
140126042643120
140126042643232
140126042643176
140126042643120
140126042643232
140126042643176
140126042643120
140126042761520
140126042761456

我仍然想知道为什么会这样。谢谢!

【问题讨论】:

  • Python 优化了一些字符串连接。我的猜测是“一些”在字典项目中不包括 +=。顺便说一句,您可能会发现将它们累积在一个列表中然后从那里加入它们会更快(您必须尝试一下)。
  • 是的,因为您应该假设与+= 的字符串连接是二次的。当您使用带有字符串的+= 进行循环时,已经做出了一些努力来添加不会在后台使用二次时间算法的优化,但是解释器很难优化除了最明显的实例之外的任何内容。在您的情况下,解释器无法对其进行优化。但是你不应该依赖解释器优化,而是使用 canonical 方式来连接许多字符串:使用 ''.join
  • 已更新加入。 += 的相同表现。
  • 所以看来问题是没有进入字典的优化。我怀疑类似的事情。谢谢!我想知道是否有任何描述此问题的 python 文档...

标签: python python-3.x


【解决方案1】:

“E.Coms”链接的文章指向一个旧的邮件列表条目: https://mail.python.org/pipermail/python-dev/2004-August/046686.html 谈论的代码如下:

s = ''
for x in y:
  s += some_string(x)

提及:

这个问题很重要,因为性能差异很大 ——我们不是在谈论快 2 倍甚至 10 倍,而是大约快 Nx 其中 N 是输入数据集的大小。

很有趣,就像我假设的那样

out_str = ''.join(str(n) for n in range(loop_count))

会比

更快
out_str = ''
for num in range(loop_count):
    out_str += str(num)

但据我所知,它们具有相同的性能。我想我之所以做出这个假设是因为str.join() 是“好东西”的重复消息

不确定这是否能回答问题,但我发现这段历史很有趣!

正如下面的 juanpa.arrivillaga 所指出的,method1 很慢,因为在别处存储对字符串的引用会使上述优化无效。进行额外的字典查找也会有 O(n) 成本,但在时间上,这个小成本主要由创建字符串的 n 个副本的 O(n^2) 工作所支配,而不仅仅是摊销的 O( n) 优化允许的版本

【讨论】:

  • 是的,因为解释器能够优化那个简单的循环。添加字典的间接性使解释器无法对其进行优化。但是你永远不应该写代码希望这些解释器优化,因为太多的人懒得去学习正确的做事方式,所以这些解释器优化真的到位了。因此,如果您要加入许多字符串,请使用 ''.join
  • @juanpa.arrivillaga 更新了我的答案,希望我解释得有用
  • “O(n) 成本也用于进行额外的字典查找”嗯,我不这么认为。你的意思是? Python 中的 dict 查找被安全地假定为 O(1)。当然,某些字典结构可能会失败,但它是语言中最优化的数据结构之一,尤其是对于像 strint 对象这样的任何东西。请注意,许多变量引用,如全局变量引用只是dict 访问
  • 我想我们说的是同一件事,只是用了不同的词!如果你读过 knuth 的 taocp,我想我可能会进入那个级别的细节——基本上不需要回答 OP
  • 同意,非正式地说,一个是线性的,另一个是二次的。
猜你喜欢
  • 1970-01-01
  • 2015-04-13
  • 2015-06-16
  • 2013-08-08
  • 1970-01-01
  • 2011-03-23
  • 1970-01-01
  • 2020-10-21
  • 2013-12-28
相关资源
最近更新 更多