69

How would you extract items 3..6 efficiently, elegantly and pythonically from the following deque without altering it:

from collections import deque
q = deque('',maxlen=10)
for i in range(10,20):
    q.append(i)

the slice notation doesn't seem to work with deque...

Community
  • 1
  • 1
Jonathan Livni
  • 101,334
  • 104
  • 266
  • 359

6 Answers6

98
import itertools
output = list(itertools.islice(q, 3, 7))

For example:

>>> import collections, itertools
>>> q = collections.deque(xrange(10, 20))
>>> q
deque([10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
>>> list(itertools.islice(q, 3, 7))
[13, 14, 15, 16]

This should be more efficient the the other solutions posted so far. Proof?

[me@home]$ SETUP="import itertools,collections; q=collections.deque(xrange(1000000))"

[me@home]$ python -m timeit  "$SETUP" "list(itertools.islice(q, 10000, 20000))"
10 loops, best of 3: 68 msec per loop

[me@home]$ python -m timeit "$SETUP" "[q[i] for i in  xrange(10000, 20000)]"
10 loops, best of 3: 98.4 msec per loop

[me@home]$ python -m timeit "$SETUP" "list(q)[10000:20000]"
10 loops, best of 3: 107 msec per loop
Shawn Chin
  • 84,080
  • 19
  • 162
  • 191
  • Very nice. Since you have a full example, perhaps `q.extend(range(10, 20))` might be better. – Roshan Mathews Aug 15 '11 at 12:29
  • @rm yes, `q.extend(..)` would be good. In fact, `q = deque(xrange(10,20))` should be used. I simply copied OP's example as is. Updated answer. – Shawn Chin Aug 15 '11 at 12:31
9

This is an old question, but for any future travelers, the Python docs explicitly recommend using rotate for this:

The rotate() method provides a way to implement deque slicing and deletion.

https://docs.python.org/2/library/collections.html

An implementation is relatively simple:

def slice_deque(d, start, stop, step):
    d.rotate(-start)
    slice = list(itertools.islice(d, 0, stop-start, step))
    d.rotate(start)
    return slice

Effectively the same as using islice directly, except that rotate is more efficient for skipping through to the starting point. On the other hand, it also temporarily modifies the deque, which could be a threadsafety concern.

MattyV
  • 170
  • 1
  • 10
  • 1
    Here's the link to the v3 documentation (the last paragraph in the section): https://docs.python.org/3/library/collections.html#deque-recipes – natka_m Nov 23 '20 at 23:44
9

I would prefer this, it's shorter so easier to read:

output = list(q)[3:6+1]
Roshan Mathews
  • 5,788
  • 2
  • 26
  • 36
  • 5
    But it makes two copies of the `deque` (one full, one partial) instead of one partial copy. Not a problem if the `deque` is short, but it might be if it's long. – agf Aug 15 '11 at 12:04
  • 2
    True, that. But then, this isn't really a use-case for a `deque`. The other solution creates a list for the `range`. I'd think of this like the `list(set(list))` hack to find unique elements of a list - elegant, and pretty, but maybe not the most efficient. – Roshan Mathews Aug 15 '11 at 12:13
  • You could just substitute `xrange` for range though and it wouldn't create a list -- also, consider if you want a short slice of a long `deque` -- the partial list and / or range would be tiny, but the full copy would a problem. – agf Aug 15 '11 at 12:16
  • Agreed. But like I said, a `deque` won't be the best choice of data-structure if you want to do something like that, with those constraints. – Roshan Mathews Aug 15 '11 at 12:24
6

I'd add this as a new answer, to provide better formatting.

For simplicity, Shawn's answer is perfect, but if you often need to get a slice from dequeue, you might prefer to subclass it and add a __getslice__ method.

from collections import deque
from itertools import islice
class deque_slice(deque):
    def __new__(cls, *args):
        return deque.__new__(cls, *args)
    def __getslice__(self, start, end):
        return list(islice(self, start, end))

This won't support setting a new slice, but you can implement your own custom __setslice__ method using the same concept.

NOTE: this is valid for Python <=2.* only. It's also worth noticing that, while __getslice__ is deprecated since python 2.0, the documentation still reports this for the latest 2.7 release:

(However, built-in types in CPython currently still implement __getslice__(). Therefore, you have to override it in derived classes when implementing slicing.)

musicamante
  • 41,230
  • 6
  • 33
  • 58
  • 2
    @Zeromatiker You're right, thanks for pointing that out. However, the note in the docs is still valid for python 2 (and we all know that it's still alive even now in mid-2020, no matter what): "built-in types in CPython currently still implement __getslice__(). Therefore, you have to override it in derived classes when implementing slicing." – musicamante Jul 02 '20 at 19:14
5

you can override the __getitem__ method and create a SliceableDeque using islice.

There are edge cases, you should consider (for example using negative slices doesn't work with islice).

here is what I have been using:

import itertools
from collections import deque

class SliceableDeque(deque):
    def __getitem__(self, s):
        try:
            start, stop, step = s.start or 0, s.stop or sys.maxsize, s.step or 1
        except AttributeError:  # not a slice but an int
            return super().__getitem__(s)
        try:  # normal slicing
            return list(itertools.islice(self, start, stop, step))
        except ValueError:  # incase of a negative slice object
            length = len(self)
            start, stop = length + start if start < 0 else start, length + stop if stop < 0 else stop
            return list(itertools.islice(self, start, stop, step))
moshevi
  • 4,999
  • 5
  • 33
  • 50
3
output = [q[i] for i in range(3,6+1)]
Jonathan Livni
  • 101,334
  • 104
  • 266
  • 359
  • The slice properly also works if the slice length exceeds the container length (it will return less elements). This is not the case here – galinette May 28 '18 at 21:30