7

In Python how do I write code which shifts off the last element of a list and adds a new one to the beginning - to run as fast as possible at execution?

There are good solutions involving the use of append, rotate etc but not all may translate to fast execution.

D.Jordan
  • 89
  • 1
  • 1
  • 3
  • 2
    Maybe you could give us an example of the method you are explaining along with graphs showing how long time it takes with varying list sizes. At the moment your question seems very low effort and a bit broad – Mandera Jun 02 '20 at 06:49
  • 2
    Using a list for that is already inefficient. You should pick a data structure designed for such access, like `collections.deque`. (Also, the words "push" and "pop" are usually used for stacks, not FIFO queues like you seem to want.) – user2357112 Jun 02 '20 at 06:52
  • Thanks (user2357112) for the clarification that Lists are inherently inefficient. – D.Jordan Jun 02 '20 at 07:19
  • Lists are an inefficient choice *for this use case*. They are designed for other things. – user2357112 Jun 02 '20 at 07:23
  • Yes - I understand that there will be other situations where Lists are the right tool (and are clearer in use as well). In other languages (eg Java) the equivalent of Lists (ie arrays) are used as stacks and have stack pop and push methods specifically for this purpose. – D.Jordan Jun 02 '20 at 07:37
  • Java arrays are fixed-size. They have no push or pop methods and could not reasonably support such methods. Also, stacks push and pop from the same end, while your question asks for insertion at the opposite end from removals. If you want a stack, you can use lists and `append` and `pop`. – user2357112 Jun 02 '20 at 07:46
  • Ah yes - bad recollection on my part. – D.Jordan Jun 02 '20 at 07:51

4 Answers4

21

Don't use a list.

A list can do fast inserts and removals of items only at its end. You'd use pop(-1) and append, and you'd end up with a stack.

Instead, use collections.deque, which is designed for efficient addition and removal at both ends. Working on the "front" of a deque uses the popleft and appendleft methods. Note, "deque" means "double ended queue", and is pronounced "deck".

Blckknght
  • 100,903
  • 11
  • 120
  • 169
8
L = [1, 2, 3]
L.pop() # returns 3, L is now [1, 2]
L.append(4) # returns None, L is now [1, 2, 4]
L.insert(0, 5) # returns None, L is now [5, 1, 2, 4]
L.remove(2) # return None, L is now [5, 1, 4]
del(L[0]) # return None, L is now [1, 4]
L.pop(0) # return 1, L is now [4]
Eeshaan
  • 1,557
  • 1
  • 10
  • 22
3

I ran some benchmarks for you. Here are the results.

TL;DR You probably want to use a deque. Otherwise, insert / append, or pop / del are fine.

Adding to the end

from collections import deque
import perfplot

# Add to end

def use_append(n):
    "adds to end"
    a = [1,2,3,4,5,6,7,8,9,10]*n
    a.append(7)
    return 1

def use_insert_end(n):
    "adds to end"
    a = [1,2,3,4,5,6,7,8,9,10]*n
    a.insert(len(a),7)
    return 1

def use_add_end(n):
    "adds to end"
    a = [1,2,3,4,5,6,7,8,9,10]*n
    a = a + [7]
    return 1

perfplot.show(
    setup=lambda n: n,  # or simply setup=numpy.random.rand
    kernels=[
        lambda a: use_append(a),
        lambda a: use_insert_end(a),
        lambda a: use_add_end(a),
    ],
    labels=["use_append", "use_insert_end", "use_add_end"],
    n_range=[2 ** k for k in range(15)],
    xlabel="len(a)",
)

add_end

Remove from end

# Removing from the end

def use_pop(n):
    "removes from end"
    a = [1,2,3,4,5,6,7,8,9,10]*n
    a.pop()
    return 1

def use_del_last(n):
    "removes from end"
    a = [1,2,3,4,5,6,7,8,9,10]*n
    del(a[-1])
    return 1

def use_index_to_end(n):
    "removes from end"
    a = [1,2,3,4,5,6,7,8,9,10]*n
    a = a[:-1]
    return 1

perfplot.show(
    setup=lambda n: n,
    kernels=[
        lambda a: use_pop(a),
        lambda a: use_del_last(a),
        lambda a: use_index_to_end(a),
    ],
    labels=["use_pop", "use_del_last", "use_index_to_end"],
    n_range=[2 ** k for k in range(20)],
    xlabel="len(a)",
)

remove_end

Adding to the beginning

# Add to beginning

def use_insert(n):
    "adds to beginning"
    a = [1,2,3,4,5,6,7,8,9,10]*n
    a.insert(0,7)
    return 1

def use_deque_appendleft(n):
    "adds to beginning"
    a = [1,2,3,4,5,6,7,8,9,10]*n
    a = deque(a)
    a.appendleft(7)
    return 1

def use_add_start(n):
    "adds to beginning"
    a = [1,2,3,4,5,6,7,8,9,10]*n
    a = [7] + a
    return 1

perfplot.show(
    setup=lambda n: n,  # or simply setup=numpy.random.rand
    kernels=[
        lambda a: use_insert(a),
        lambda a: use_deque_appendleft(a),
        lambda a: use_add_start(a),
    ],
    labels=["use_insert", "use_deque_appendleft","use_add_start"],
    n_range=[2 ** k for k in range(15)],
    xlabel="len(a)",
)

add_start

Removing from the beginning

# Remove from beginning

def use_del_first(n):
    "removes from beginning"
    a = [1,2,3,4,5,6,7,8,9,10]*n
    del(a[0])
    return 1


def use_deque_popleft(n):
    "removes from beginning"
    a = [1,2,3,4,5,6,7,8,9,10]*n
    a = deque(a)
    a.popleft()
    return 1


def use_index_start(n):
    "removes from beginning"
    a = [1,2,3,4,5,6,7,8,9,10]*n
    a = a[1:]
    return 1


perfplot.show(
    setup=lambda n: n,  # or simply setup=numpy.random.rand
    kernels=[
        lambda a: use_del_first(a),
        lambda a: use_deque_popleft(a),
        lambda a: use_index_start(a),
    ],
    labels=["use_del_first", "use_deque_popleft", "use_index_start"],
    n_range=[2 ** k for k in range(15)],
    xlabel="len(a)",
)

remove_start


Edit

Take these results with a grain of salt. Given how perfplot works, the remove methods would get run multiple times, while setup is only run once. Hence, the list (or deque) needs to be generated locally in each function, which adds to run time.

I've modified the add methods below, and run a separate comparison for deque, to compare the effect of generating lists locally within the functions.

Deque setup difference


def gen_deque(n):
    a = [1,2,3,4,5,6,7,8,9,10]*n if n > 0 else [1,2,3]
    return deque(a)


def use_deque_appendleft(a):
    "adds to beginning"
    a.appendleft(7)
    return 1


def use_deque_appendleft_original(a):
    "adds to beginning"
    a = [1,2,3,4,5,6,7,8,9,10]*(len(a)//10)
    a = deque(a)
    a.appendleft(7)
    return 1

perfplot.show(
    setup=lambda n: gen_deque(n),  # or simply setup=numpy.random.rand
    kernels=[
        lambda a: use_deque_appendleft(a),
        lambda a: use_deque_appendleft_original(a),
    ],
    labels=["use_deque_appendleft", "use_deque_appendleft_original"],
    n_range=[2 ** k for k in range(15)],
    xlabel="len(a)",
)

deque_diff

Add to end

# Add to end

def gen_data(n):
    return [1,2,3,4,5,6,7,8,9,10]*n if n > 0 else [1,2,3]

def use_append(a):
    "adds to end"
#     a = [1,2,3,4,5,6,7,8,9,10]*n
    a.append(7)
    return 1

def use_insert_end(a):
    "adds to end"
#     a = [1,2,3,4,5,6,7,8,9,10]*n
    a.insert(len(a),7)
    return 1

def use_add_end(a):
    "adds to end"
#     a = [1,2,3,4,5,6,7,8,9,10]*n
    a = a + [7]
    return 1

perfplot.show(
    setup=lambda n: gen_data(n),  # or simply setup=numpy.random.rand
    kernels=[
        lambda a: use_append(a),
        lambda a: use_insert_end(a),
        lambda a: use_add_end(a),
    ],
    labels=["use_append", "use_insert_end", "use_add_end"],
    n_range=[2 ** k for k in range(15)],
    xlabel="len(a)",
)

add_end2

Add to start

# Add to beginning

def gen_data(n):
    return [1,2,3,4,5,6,7,8,9,10]*n if n > 0 else [1,2,3]


def use_insert(a):
    "adds to beginning"
    a.insert(0,7)
    return 1

def use_deque_appendleft(a):
    "adds to beginning"
    a = deque(a)
    a.appendleft(7)
    return 1

def use_add_start(a):
    "adds to beginning"
    a = [7] + a
    return 1

perfplot.show(
    setup=lambda n: gen_data(n),  # or simply setup=numpy.random.rand
    kernels=[
        lambda a: use_insert(a),
        lambda a: use_deque_appendleft(a),
        lambda a: use_add_start(a),
    ],
    labels=["use_insert", "use_deque_appendleft","use_add_start"],
    n_range=[2 ** k for k in range(5)],
    xlabel="len(a)",
)

add_start2

Conclusion

Insert and append have similar performance, and using a deque seems to have better performance than insert. As for del / pop / deque's popleft, it seems that del and pop have similar performance, but it's hard to tell if deque's popleft would be better, considering the overhead of generating lists / deques within each function for using perfplot.

bug_spray
  • 1,445
  • 1
  • 9
  • 23
  • 2
    Isn't what you're measuring here mostly the time spent on `[1,2,3,4,5,6,7,8,9,10]*n`? – Blckknght Jun 02 '20 at 08:01
  • The point is to measure relative performance, so all methods have that. Due to how lists work in Python, the list itself needs to be in a local scope to each function. Otherwise, the pop / delete methods will end up reducing the data to an empty list. – bug_spray Jun 02 '20 at 08:05
  • If you have a better way of running benchmarks, by all means, you're welcome to modify / reuse my code – bug_spray Jun 02 '20 at 08:07
  • @Blckknght Yeah, you're right, it's just there may be an issue with pop / delete methods. Updating everything now – bug_spray Jun 02 '20 at 08:13
  • 2
    Hmm, you'd almost want `setup=lambda n: [1,2,3,4,5,6,7,8,9,10]*n`, but that won't help the `deque` cases. For those you need to setup the deque in the setup, and just do the popping in the function, I'm not sure if that can work with perfplot. In any case, the reason you're not seeing any difference between `popleft` and `del a[0]` is that the O(1) deque popping is buried under the O(N) code building the list and converting it into a deque. – Blckknght Jun 02 '20 at 08:14
  • Is `a = deque(a)` significant? – bug_spray Jun 02 '20 at 08:16
  • Yes, that's O(N) too. – Blckknght Jun 02 '20 at 08:17
  • My issue with `setup=lambda n: [1,2,3,4,5,6,...]*n` is that I get an `IndexError: pop from empty list` for the remove methods – bug_spray Jun 02 '20 at 08:17
  • 1
    Sounds like the setup is only run once and the functions are not supposed to modify the input in place. I'm not sure that `perfplot` can easily help us with this problem, since in place modifications are what we want to performance test. – Blckknght Jun 02 '20 at 08:23
  • Thanks for the advice and benchmark tests. I'll make use of deque. – D.Jordan Jun 02 '20 at 13:31
0

You can use insert method of python list object.

l = [1, 2, 3]
#lets insert 10 at biggining, which means at index 0
l.insert(0, 10)
print(l) # this will print [10, 1, 2, 3]
Alok
  • 7,734
  • 8
  • 55
  • 100