2

This question is a follow up to this one: deque.popleft() and list.pop(0). Is there performance difference?

In Python, I can pop the last item added to a list using .pop(). I can also pop the last item using dequeue and .pop().

Is there a performance difference between these two? Is there a reason or use case that one should be used over the other?

Edit: Typo.... changed .popright to .pop -- deque's "pop right" is still just .pop -- thanks ShadowRanger

Eddie
  • 378
  • 1
  • 3
  • 16

3 Answers3

3

Well, first off, it's called pop for both list and deque, there is no popright method on deques.

There is usually no meaningful performance difference between the two; every once in a while, a pop on a deque will cause a block deallocation (which has fixed overhead, it just makes that particular pop a little more costly), and on a list it can cause a realloc to shrink the underlying storage (which could end up being O(n), but only a tiny fraction of pops will cause it); asymptotically they're both O(1) operations. If your list is truly huge, then shrinks a lot you might get the occasional performance hiccup when it shrinks the underlying storage, but otherwise you're highly unlikely to notice a difference.

In answer to your question, deques are ever-so-slightly more efficient for use as stacks than lists; if you're importing collections anyway, and need a stack based structure, using a deque will get you a tiny benefit (at least on CPython, can't speak to other implementation). But it's not really worth micro-optimizing here; the cost of importing collections in the first place, and the cost of whatever useful code you execute based on this stack, likely dwarfs whatever tiny difference you'll see between list and deque for pops from the right. A simple ipython3 microbenchmark:

In [24]: %%timeit from collections import deque; s = deque([0] * 10000); onethousandnones = (None,) * 1000; pop = s.pop
    ...: ; push = s.append
    ...: for _ in onethousandnones:
    ...:     pop()
    ...: for _ in onethousandnones:
    ...:     push(0)
    ...:
    ...:
104 µs ± 7.99 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

In [25]: %%timeit s = [0] * 10000; onethousandnones = (None,) * 1000; pop = s.pop; push = s.append
    ...: for _ in onethousandnones:
    ...:     pop()
    ...: for _ in onethousandnones:
    ...:     push(0)
    ...:
    ...:
131 µs ± 8.93 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

So for a 1000 pops followed by 1000 pushes onto our stack, the deque-based stack took 30 µs less (~15 ns less per operation). Now admittedly, if I remove the call parentheses to time the base overhead, the base overhead was around 50 µs, so the overhead specifically attributable to the list is a significant fraction of the "minimum cost" of the deque, but it's still pretty small in the context of a program that is presumably doing something useful, not just pushing and popping to a stack. And it's pretty stable regardless of size; for a stack that's 10x the size, the cost remains unchanged for both deque and list. If the stack was growing and shrinking so much that list's amortized growth was kicking in, it might suffer a little more from the larger reallocations, but it's not normally something to worry about.

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
2

list.pop() is O(1). It doesn't need to copy anything, it just clears the last element and decrements the length of the list.

Deques are designed to optimize pushing and popping from either end, so both popleft() and popright() are O(1).

Barmar
  • 741,623
  • 53
  • 500
  • 612
0

A python deque is implemented as a doubly linked list . This results in pop operations more efficient than lists [O(1)]. However if you want to access elements using their index , lists are faster.

Ram K
  • 1,746
  • 2
  • 14
  • 23
  • 1
    Technically, `deque`s are a doubly-linked list of *blocks* (each block holding an implementation defined number of elements), so there is a non-uniform cost to `pop` (for block size `n`, `n - 1` pops cost `x`, where `x` is the cost to just return the item and adjust the end of `deque` marker, and `1` pop costs `x+y`, where `y` is the additional cost of freeing the block). In any event, `list.pop()` (with no argument) is amortized `O(1)`, so it's not meaningfully distinct from `deque.pop()` performance overall. – ShadowRanger Aug 10 '19 at 00:12