【问题标题】:String concatenation vs. string substitution in PythonPython中的字符串连接与字符串替换
【发布时间】:2010-09-27 10:45:59
【问题描述】:

在 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 中,字符串插值通常比串联快...
  • 你忘了 return "".join([DOMAIN, QUESTIONS, str(q_num)])
  • 我不是 Ruby 专家,但我敢打赌插值更快,因为字符串在 Ruby 中是可变的。字符串在 Python 中是不可变的序列。
  • 只是对 URI 的一点评论。 URI 并不完全像字符串。有 URI,因此在连接或比较它们时必须非常小心。示例:服务器在端口 80 上通过 http 传递其表示。example.org(最后没有 slah)example.org/(斜杠)example.org:80/(slah+port 80)是相同的 uri,但不相同字符串。

标签: python string string-concatenation


【解决方案1】:

根据我的机器,连接(显着)更快。但在风格上,如果性能不重要,我愿意付出替代的代价。好吧,如果我需要格式化,甚至不需要问这个问题......除了使用插值/模板之外别无选择。

>>> 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

【讨论】:

  • 您是否使用真正的大字符串(如 100000 个字符)进行测试?
【解决方案2】:

不要忘记命名替换:

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

【讨论】:

  • 这段代码至少有 2 个不好的编程习惯:期望全局变量(域和问题不在函数内部声明)和传递比 format() 函数需要的更多变量。投反对票,因为这个答案教导了糟糕的编码实践。
【解决方案3】:

小心在循环中连接字符串! 字符串连接的成本与结果的长度成正比。循环将您直接带到 N 平方的土地。某些语言会优化与最近分配的字符串的连接,但指望编译器将您的二次算法优化为线性是有风险的。最好使用原语 (join?),它获取整个字符串列表,进行一次分配,然后一次性将它们连接起来。

【讨论】:

  • 那不是最新的。在最新版本的 python 中,当您在循环中连接字符串时会创建一个隐藏的字符串缓冲区。
  • @Seun:是的,正如我所说,某些语言会优化,但这是一种冒险的做法。
【解决方案4】:

“由于字符串连接的性能有了很大的提升......”

如果性能很重要,很高兴知道这一点。

但是,我看到的性能问题从来都不是字符串操作。我通常遇到 I/O、排序和 O(n2) 操作成为瓶颈的问题。

在字符串操作成为性能限制器之前,我会坚持使用显而易见的东西。大多数情况下,当它是一行或更少时,它是替换,当它有意义时是连接,当它很大时是一个模板工具(如 Mako)。

【讨论】:

    【解决方案5】:

    您想要连接/插值的内容以及您希望如何格式化结果应该推动您的决定。

    • 字符串插值允许您轻松添加格式。实际上,您的字符串插值版本与您的串联版本不同;它实际上在q_num 参数之前添加了一个额外的正斜杠。要执行相同的操作,您必须在该示例中编写 return DOMAIN + QUESTIONS + "/" + str(q_num)

    • 插值可以更轻松地格式化数字; "%d of %d (%2.2f%%)" % (current, total, total/current) 在串联形式中的可读性会大大降低。

    • 当您没有固定数量的要字符串化的项目时,连接非常有用。

    另外,要知道 Python 2.6 引入了一个新版本的字符串插值,称为string templating

    def so_question_uri_template(q_num):
        return "{domain}/{questions}/{num}".format(domain=DOMAIN,
                                                   questions=QUESTIONS,
                                                   num=q_num)
    

    字符串模板最终会取代 %-interpolation,但我认为这在很长一段时间内不会发生。

    【讨论】:

    • 好吧,只要你决定迁移到 python 3.0,它就会发生。此外,请参阅 Peter 的评论,了解您无论如何都可以使用 % 运算符进行命名替换。
    • "当您没有固定数量的要字符串化的项目时,连接很有用。" -- 你的意思是一个列表/数组?在那种情况下,你不能加入()他们吗?
    • “你不能加入()他们吗?” -- 是的(假设您想要项目之间的统一分隔符)。列表和生成器推导与 string.join 配合得很好。
    • “好吧,只要你决定迁移到 python 3.0,它就会发生”——不,py3k 仍然支持 % 运算符。下一个可能的弃用点是 3.1,所以它还有一些生命力。
    • 2 年后...python 3.2 即将发布,% 样式插值仍然很好。
    【解决方案6】:

    出于好奇,我只是在测试不同字符串连接/替换方法的速度。关于这个主题的谷歌搜索把我带到了这里。我想我会发布我的测试结果,希望它可以帮助某人做出决定。

        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 的速度几乎是使用 format% 方法的 3 倍(1.1 对 2.8)。我想这取决于字符串以及您要实现的目标。如果性能真的很重要,那么尝试不同的东西并测试它们可能会更好。我倾向于选择可读性而不是速度,除非速度成为问题,但这只是我。所以不喜欢我的复制/粘贴,我必须在所有内容上放置 8 个空格以使其看起来正确。我通常使用 4。

    【讨论】:

    • 你应该认真考虑你正在分析什么。一方面,你的 concat 很慢,因为你有两个 str 强制转换。使用字符串的结果是相反的,因为当只涉及三个字符串时,字符串 concat 实际上比所有替代方法都快。
    • @JustusWingert,这已经两岁了。自从我发布这个“测试”以来,我学到了很多东西。老实说,这些天我使用str.format()str.join() 而不是正常的串联。我也在留意来自PEP 498 的“f-strings”,它最近已被接受。至于影响性能的str() 调用,我相信你是对的。那时我不知道函数调用有多昂贵。我仍然认为有任何疑问时应该进行测试。
    • join_(): return ''.join(["test ", str(1), ", with number ", str(2)]) 快速测试后,似乎join 也比百分比慢。
    【解决方案7】:

    记住,风格决定实际的决定,如果你打算维护或调试你的代码 :-) Knuth 有一句名言(可能引用 Hoare?):“我们应该忘记小的效率,大约 97% 的时间说:过早优化是万恶之源。”

    只要你小心不要(比如说)把 O(n) 任务变成 O(n2) 任务,我会选择你认为最容易理解的那个。

    【讨论】:

      【解决方案8】:

      我尽可能使用替代。如果我在 for 循环中构建一个字符串,我只使用连接。

      【讨论】:

      • “在 for 循环中构建字符串”——通常情况下,您可以使用 ''.join 和生成器表达式..
      【解决方案9】:

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

      【讨论】:

      • 对于操作系统路径(如在您的文件系统上)是正确的,但在本示例中构造 URI 时则不然。 URI 始终使用“/”作为分隔符。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-10-01
      • 2016-08-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多