98

In Python, the where and when of using string concatenation versus string substitution eludes me. As the string concatenation has seen large boosts in performance, is this (becoming more) a stylistic decision rather than a practical one?

For a concrete example, how should one handle construction of flexible URIs:

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)

Edit: There have also been suggestions about joining a list of strings and for using named substitution. These are variants on the central theme, which is, which way is the Right Way to do it at which time? Thanks for the responses!

gotgenes
  • 38,661
  • 28
  • 100
  • 128
  • Funny, in Ruby, string interpolation is generally faster than concatenation... – Keltia Dec 17 '08 at 23:47
  • you forgot return "".join([DOMAIN, QUESTIONS, str(q_num)]) – Jimmy Dec 17 '08 at 23:59
  • I'm no Ruby expert, but I would bet that interpolation is faster because strings are mutable in Ruby. Strings are immutable sequences in Python. – gotgenes Dec 18 '08 at 05:00
  • 1
    just a little comment about URIs. URIs are not exactly like strings. There are URIs, so you have to be very careful when you concatenate or compare them. Example: a server delivering its representations over http on port 80. example.org (no slah at the end) example.org/ (slash) example.org:80/ (slah+port 80) are the same uri but not the same string. – karlcow Jul 05 '10 at 11:37

9 Answers9

55

Concatenation is (significantly) faster according to my machine. But stylistically, I'm willing to pay the price of substitution if performance is not critical. Well, and if I need formatting, there's no need to even ask the question... there's no option but to use interpolation/templating.

>>> 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
Nilesh
  • 20,521
  • 16
  • 92
  • 148
Vinko Vrsalovic
  • 330,807
  • 53
  • 334
  • 373
24

Don't forget about named substitution:

def so_question_uri_namedsub(q_num):
    return "%(domain)s%(questions)s/%(q_num)d" % locals()
too much php
  • 88,666
  • 34
  • 128
  • 138
  • 4
    This code has at least 2 bad programming practices: expectation of global variables (domain and questions are not declared inside function) and passing more variables than needed to a format() function. Downvoting because this answer teaches bad coding practices. – jperelli Dec 06 '16 at 20:29
12

Be wary of concatenating strings in a loop! The cost of string concatenation is proportional to the length of the result. Looping leads you straight to the land of N-squared. Some languages will optimize concatenation to the most recently allocated string, but it's risky to count on the compiler to optimize your quadratic algorithm down to linear. Best to use the primitive (join?) that takes an entire list of strings, does a single allocation, and concatenates them all in one go.

Norman Ramsey
  • 198,648
  • 61
  • 360
  • 533
11

"As the string concatenation has seen large boosts in performance..."

If performance matters, this is good to know.

However, performance problems I've seen have never come down to string operations. I've generally gotten in trouble with I/O, sorting and O(n2) operations being the bottlenecks.

Until string operations are the performance limiters, I'll stick with things that are obvious. Mostly, that's substitution when it's one line or less, concatenation when it makes sense, and a template tool (like Mako) when it's large.

S.Lott
  • 384,516
  • 81
  • 508
  • 779
10

What you want to concatenate/interpolate and how you want to format the result should drive your decision.

  • String interpolation allows you to easily add formatting. In fact, your string interpolation version doesn't do the same thing as your concatenation version; it actually adds an extra forward slash before the q_num parameter. To do the same thing, you would have to write return DOMAIN + QUESTIONS + "/" + str(q_num) in that example.

  • Interpolation makes it easier to format numerics; "%d of %d (%2.2f%%)" % (current, total, total/current) would be much less readable in concatenation form.

  • Concatenation is useful when you don't have a fixed number of items to string-ize.

Also, know that Python 2.6 introduces a new version of string interpolation, called string templating:

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

String templating is slated to eventually replace %-interpolation, but that won't happen for quite a while, I think.

Tim Lesher
  • 6,341
  • 2
  • 28
  • 42
  • Well, it'll happen whenever you decide to move to python 3.0. Also, see Peter's comment for the fact that you can do named substitutions with the % operator anyway. – John Fouhy Dec 18 '08 at 00:56
  • "Concatenation is useful when you don't have a fixed number of items to string-ize." -- You mean a list/array? In that case, couldn't you just join() them? – strager Dec 18 '08 at 01:04
  • "Couldn't you just join() them?" -- Yes (assuming you want uniform separators between items). List and generator comprehensions work great with string.join. – Tim Lesher Dec 18 '08 at 01:21
  • 1
    "Well, it'll happen whenever you decide to move to python 3.0" -- No, py3k still supports the % operator. The next possible deprecation point is 3.1, so it still has some life in it. – Tim Lesher Dec 18 '08 at 01:29
  • 2
    2 years later... python 3.2 is nearing release and % style interpolation is still fine. – Corey Goldberg Jan 04 '11 at 04:24
8

I was just testing the speed of different string concatenation/substitution methods out of curiosity. A google search on the subject brought me here. I thought I would post my test results in the hope that it might help someone decide.

    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)

...After running runtests((percent_, format_, format2_, concat_), runs=5), I found that the % method was about twice as fast as the others on these small strings. The concat method was always the slowest (barely). There were very tiny differences when switching the positions in the format() method, but switching positions was always at least .01 slower than the regular format method.

Sample of test results:

    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)

I ran these because I do use string concatenation in my scripts, and I was wondering what the cost was. I ran them in different orders to make sure nothing was interfering, or getting better performance being first or last. On a side note, I threw in some longer string generators into those functions like "%s" + ("a" * 1024) and regular concat was almost 3 times as fast (1.1 vs 2.8) as using the format and % methods. I guess it depends on the strings, and what you are trying to achieve. If performance really matters, it might be better to try different things and test them. I tend to choose readability over speed, unless speed becomes a problem, but thats just me. SO didn't like my copy/paste, i had to put 8 spaces on everything to make it look right. I usually use 4.

Cj Welborn
  • 119
  • 1
  • 5
  • 1
    You should seriously consider what you are profiling how. For one your concat is slow because you have two str casts in it. With strings the result is the opposite, since string concat is actually faster than all the alternatives when only three strings are concerned. – Justus Wingert Sep 16 '15 at 10:02
  • @JustusWingert, this is two years old now. I've learned a lot since I posted this 'test'. Honestly, these days I use `str.format()` and `str.join()` over normal concatenation. I'm also keeping an eye out for 'f-strings' from [PEP 498](https://www.python.org/dev/peps/pep-0498/), which has recently been accepted. As for the `str()` calls affecting performance, I'm sure you're right about that. I had no idea how expensive function calls were at that time. I still think that tests should be done when there is any doubt. – Cj Welborn Sep 17 '15 at 23:55
  • After a quick test with `join_(): return ''.join(["test ", str(1), ", with number ", str(2)])`, it seems `join` is also slower than percentage. – gaborous Aug 08 '17 at 14:56
4

Remember, stylistic decisions are practical decisions, if you ever plan on maintaining or debugging your code :-) There's a famous quote from Knuth (possibly quoting Hoare?): "We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil."

As long as you're careful not to (say) turn a O(n) task into an O(n2) task, I would go with whichever you find easiest to understand..

John Fouhy
  • 41,203
  • 19
  • 62
  • 77
0

I use substitution wherever I can. I only use concatenation if I'm building a string up in say a for-loop.

Draemon
  • 33,955
  • 16
  • 77
  • 104
  • 7
    "building a string in a for-loop" – often this is a case where you can use ''.join and a generator expression.. – John Fouhy Dec 18 '08 at 00:55
-1

Actually the correct thing to do, in this case (building paths) is to use os.path.join. Not string concatenation or interpolation

hoskeri
  • 349
  • 2
  • 4
  • 1
    that is true for os paths (like on your filesystem) but not when constructing a URI as in this example. URI's always have '/' as separator. – Andre Blum Sep 27 '12 at 19:37