1

I stumbled into something that surprised me while writing some Python code. I was considering two ways of duplicating a list, then adding one more element to the duplicate:

# I thought this would be clean-looking but slow since it creates an extra one element list, ['foo'] mylist = range(4) newlist_0 = mylist + ['foo'] print newlist_0 # [0, 1, 2, 3, 'foo'] # I thought this would be faster newlist_1 = list(mylist) newlist_1.append('foo') print newlist_1 # [0, 1, 2, 3, 'foo'] 

Surprisingly the first way is not only nice to look at but also faster. I ran:

import timeit for stmt in ['newlist_0 = mylist + ["foo"]', 'newlist_1 = list(mylist); newlist_1.append("foo")']: print "For statement {:50} timeit results are {}".format(stmt, timeit.repeat(setup='mylist = range(4)', stmt=stmt)) 

and got this output:

For statement newlist_0 = mylist + ["foo"] timeit results are [0.29012012481689453, 0.3021109104156494, 0.32175779342651367] For statement newlist_1 = list(mylist); newlist_1.append("foo") timeit results are [0.39945101737976074, 0.39692091941833496, 0.38529205322265625] 

Momentarily I stumbled onto this question discussing the fact that list(lst) is slower than lst[:] for copying a list, but switching to using [:] to copy mylist doesn't change anything.

2
  • 2
    function call overhead Commented Sep 1, 2014 at 18:06
  • @wim, would I be right to infer, then, that neither + nor ["foo"] involve function calls under the hood, but append does? Commented Sep 1, 2014 at 18:18

1 Answer 1

2

Let's take a look at the disassembly. Your first method looks like this in Python 2.7 bytecode:

 0 LOAD_FAST 0 (mylist) 3 LOAD_CONST 1 ('foo') 6 BUILD_LIST 1 9 BINARY_ADD 

The second method looks like this:

 0 LOAD_GLOBAL 0 (list) 3 LOAD_FAST 0 (mylist) 6 CALL_FUNCTION 1 9 STORE_FAST 1 (newlist_1) 12 LOAD_FAST 1 (newlist_1) 15 LOAD_ATTR 1 (append) 18 LOAD_CONST 1 ('foo') 21 CALL_FUNCTION 1 

A few things that would make the latter slower based on a comparison of the disassembly:

  1. list must be loaded from the globals namespace.

  2. append must be loaded from the list object.

  3. You pay for two call overheads rather than one.

The short answer is that Python byte code has very concise and efficient ways of storing short lists like ['foo'] and doing binary operations.

Sign up to request clarification or add additional context in comments.

2 Comments

What are you using to get your disassembly? If I run def f1(): newlist_0 = mylist + ['foo'] and then dis.dis(f1) I get seven lines of output and can't get it down to four by messing with the function.
I used the dis module and wrapped things in functions. My f1 differs in that I return mylist + ['foo'].

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.