为什么连接字符串比加入字符串运行更快?

3
据我了解,"".join(iterable_of_strings)是连接字符串的首选方式,因为它允许进行优化,避免不必要地将不可变对象重写到内存中多次。
对于适度大量的操作,表达式中添加字符串比使用.join()方法可靠地运行得更快。
在我的笔记本电脑上,使用Python 3.3运行此代码,我得到的联接时间约为2.9-3.2秒,而添加时间为2.3-2.7秒。我在谷歌上找不到一个好答案。能否有人解释一下可能发生了什么或者指导我到一个好资源?
import uuid
import time

class mock:
    def __init__(self):
        self.name = "foo"
        self.address = "address"
        self.age = "age"
        self.primarykey = uuid.uuid4()

data_list = [mock() for x in range(2000000)]

def added():
    my_dict_list = {}
    t = time.time()
    new_dict = { item.primarykey: item.name + item.address + item.age for item in data_list }
    print(time.time() - t)

def joined():
    my_dict_list = {}
    t = time.time()
    new_dict = { item.primarykey: ''.join([item.name, item.address, item.age]) for item in data_list }
    print(time.time() - t)

joined()
added()

1
https://dev59.com/3XA75IYBdhLWcg3w4tN7 - idanshmu
1
cPython具有非常高效的原地字符串连接功能。如果发现两个连接操作比生成列表然后执行''join更快,这并不会让我感到惊讶。 - roippi
2个回答

5
据我所知,"".join(iterable_of_strings)是连接字符串的首选方式,因为它允许通过优化避免不必要地将不可变对象重写到内存中更多次。
你的理解有些不正确。 "".join(iterable_of_strings)是连接可迭代字符串的首选方式,原因如你所解释的一样。
但是,你没有一个可迭代字符串。你只有三个字符串。连接三个字符串最快的方法是使用加号(+),或使用.format()或%。这是因为在你的情况下必须先创建可迭代对象,然后连接字符串,所有这些都是为了避免复制一些非常小的字符串。
只有当你有很多字符串时,.join()才会变得更快,当你使用其他方法编写代码变得非常愚蠢的时候。这取决于你拥有哪种Python实现、什么版本和字符串长度以及其他因素,但通常我们需要连接十个以上的字符串。
虽然并非所有实现都具有快速连接功能,但我已测试了CPython、PyPy和Jython,它们都可以用于几个字符串或者更快地连接字符串。
本质上,你应该根据代码的清晰程度来选择使用+或.join(),直到你的代码运行为止。然后,如果你关心速度:请分析和测试你的代码,不要瞎猜。
一些时序:http://slides.colliberty.com/DjangoConEU-2013/#/step-40 视频解释:http://youtu.be/50OIO9ONmks?t=18m30s

3
您看到的时间差来自于创建要传递给join的列表。虽然使用元组可能会获得一些速度提升,但当只有几个短字符串时,仍然比使用+连接慢。
如果您拥有一个字符串序列,而不是一个带有字符串作为属性的对象,则可以直接在可迭代对象上调用join,而无需为每个调用构建新的列表。
这是我使用timeit模块进行的测试:
import timeit

short_strings = ["foo", "bar", "baz"]
long_strings = [s*1000 for s in short_strings]

def concat(a, b, c):
    return a + b + c

def concat_from_list(lst):
    return lst[0] + lst[1] + lst[2]

def join(a, b, c):
    return "".join([a, b, c])

def join_tuple(a, b, c):
    return "".join((a, b, c))

def join_from_list(lst):
    return "".join(lst)

def test():
    print("Short strings")
    print("{:20}{}".format("concat:",
                           timeit.timeit(lambda: concat(*short_strings))))
    print("{:20}{}".format("concat_from_list:",
                           timeit.timeit(lambda: concat_from_list(short_strings))))
    print("{:20}{}".format("join:",
                           timeit.timeit(lambda: join(*short_strings))))
    print("{:20}{}".format("join_tuple:",
                           timeit.timeit(lambda: join_tuple(*short_strings))))
    print("{:20}{}\n".format("join_from_list:",
                             timeit.timeit(lambda: join_from_list(short_strings))))
    print("Long Strings")
    print("{:20}{}".format("concat:",
                           timeit.timeit(lambda: concat(*long_strings))))
    print("{:20}{}".format("concat_from_list:",
                           timeit.timeit(lambda: concat_from_list(long_strings))))
    print("{:20}{}".format("join:",
                           timeit.timeit(lambda: join(*long_strings))))
    print("{:20}{}".format("join_tuple:",
                           timeit.timeit(lambda: join_tuple(*long_strings))))
    print("{:20}{}".format("join_from_list:",
                           timeit.timeit(lambda: join_from_list(long_strings))))

输出:

Python 3.3.0 (v3.3.0:bd8afb90ebf2, Sep 29 2012, 10:57:17) [MSC v.1600 64 bit (AMD64)] on win32
Type "copyright", "credits" or "license()" for more information.
>>> ================================ RESTART ================================
>>> 
>>> test()
Short strings
concat:             0.5453461176251436
concat_from_list:   0.5185697357936024
join:               0.7099379456477868
join_tuple:         0.5900842397209949
join_from_list:     0.4177281794285359

Long Strings
concat:             2.002303591571888
concat_from_list:   1.8898819841869416
join:               1.5672863477837913
join_tuple:         1.4343144915087596
join_from_list:     1.231374639083505

因此,从已经存在的列表中加入元素总是最快的。如果单个项目很短,则使用+连接更快,但对于长字符串,它总是最慢的。我怀疑在测试代码中函数调用中列表的解包导致了concatconcat_from_list之间的差异。


1
没错。concat_from_list 比 concat 更快,这表明测试代码存在问题。正如你所说,可能是拆包的问题。无论如何,在这些计时中很多时间都花在了函数调用上。我做的测试表明,对于三个长字符串,"+" 也更快,尽管我手头没有相关数据。 - Lennart Regebro
很高兴看到这个,感谢你详细的回复。我一定会采用你使用timeit的方法,而不是我之前使用的笨重习语。 - Tim Wilder
2
@TimWilder 不要这样做。如果你关心代码的运行速度,就应该对其进行分析。timeit是一个有用的工具,如果使用正确的话。但这不是一件容易做到的事情,而且你无法测试真实数据下实际运行的代码,也就是说,它最多只能帮你找出哪些性能改变值得尝试,一旦你已经找出慢代码了。 - Lennart Regebro
1
@Lennart 我会记住的。我相信现在去了解一些关于Python性能分析选项的公交车阅读是值得的。 - Tim Wilder

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