ely
ely

Reputation: 77424

Python: shorter syntax for slices with gaps?

Suppose I want the first element, the 3rd through 200th elements, and the 201st element through the last element by step-size 3, from a list in Python.

One way to do it is with distinct indexing and concatenation:

new_list = old_list[0:1] + old_list[3:201] + old_list[201::3]

Is there a way to do this with just one index on old_list? I would like something like the following (I know this doesn't syntactically work since list indices cannot be lists and since Python unfortunately doesn't have slice literals; I'm just looking for something close):

new_list = old_list[[0, 3:201, 201::3]]

I can achieve some of this by switching to NumPy arrays, but I'm more interested in how to do it for native Python lists. I could also create a slice maker or something like that, and possibly strong arm that into giving me an equivalent slice object to represent the composition of all my desired slices.

But I'm looking for something that doesn't involve creating a new class to manage the slices. I want to just sort of concatenate the slice syntax and feed that to my list and have the list understand that it means to separately get the slices and concatenate their respective results in the end.

Upvotes: 17

Views: 17966

Answers (6)

mon
mon

Reputation: 22254

import numpy as np
a = list(range(15, 50, 3))

# %%timeit -n 10000 -> 41.1 µs ± 1.71 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
[a[index] for index in np.r_[1:3, 5:7, 9:11]]
---
[18, 21, 30, 33, 42, 45]
import numpy as np
a = np.arange(15, 50, 3).astype(np.int32)

# %%timeit -n 10000 -> 31.9 µs ± 5.68 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
a[np.r_[1:3, 5:7, 9:11]]
---
array([18, 21, 30, 33, 42, 45], dtype=int32)
import numpy as np
a = np.arange(15, 50, 3).astype(np.int32)

# %%timeit -n 10000 -> 7.17 µs ± 1.17 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
slices = np.s_[1:3, 5:7, 9:11]
np.concatenate([a[_slice] for _slice in slices])
---
array([18, 21, 30, 33, 42, 45], dtype=int32)

Seems using numpy is a faster way.

Adding numpy part to the answer from ecatmur.

import numpy as np
def xslice(x, slices):
    """Extract slices from array-like
    Args:
        x: array-like
        slices: slice or tuple of slice objects
    """
    if isinstance(slices, tuple):
        if isinstance(x, np.ndarray):
            return np.concatenate([x[_slice] for _slice in slices])
        else:
            return sum((x[s] if isinstance(s, slice) else [x[s]] for s in slices), [])        
    elif isinstance(slices, slice):
        return x[slices]
    else:
        return [x[slices]]

Upvotes: 2

Stuart
Stuart

Reputation: 9858

You could extend list to allow multiple slices and indices:

class MultindexList(list):
    def __getitem__(self, key):
        if type(key) is tuple or type(key) is list:
            r = []
            for index in key:
                item = super().__getitem__(index)
                if type(index) is slice:
                    r += item
                else:
                    r.append(item)
            return r
        else:
            return super().__getitem__(key)


a = MultindexList(range(10))
print(a[1:3])             # [1, 2]
print(a[[1, 2]])          # [1, 2]
print(a[1, 1:3, 4:6])     # [1, 1, 2, 4, 5]

Upvotes: 0

ecatmur
ecatmur

Reputation: 157364

A slice maker object (e.g. SliceMaker from your other question, or np.s_) can accept multiple comma-separated slices; they are received as a tuple of slices or other objects:

from numpy import s_
s_[0, 3:5, 6::3]
Out[1]: (0, slice(3, 5, None), slice(6, None, 3))

NumPy uses this for multidimensional arrays, but you can use it for slice concatenation:

def xslice(arr, slices):
    if isinstance(slices, tuple):
        return sum((arr[s] if isinstance(s, slice) else [arr[s]] for s in slices), [])
    elif isinstance(slices, slice):
        return arr[slices]
    else:
        return [arr[slices]]
xslice(list(range(10)), s_[0, 3:5, 6::3])
Out[1]: [0, 3, 4, 6, 9]
xslice(list(range(10)), s_[1])
Out[2]: [1]
xslice(list(range(10)), s_[:])
Out[3]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Upvotes: 11

Matthew Purdon
Matthew Purdon

Reputation: 773

Not sure if this is "better", but it works so why not...

[y for x in [old_list[slice(*a)] for a in ((0,1),(3,201),(201,None,3))] for y in x]

It's probably slow (especially compared to chain) but it's basic python (3.5.2 used for testing)

Upvotes: 1

Abhijit
Abhijit

Reputation: 63737

Why don;t you create a custom slice for your purpose

>>> from itertools import chain, islice
>>> it = range(50)
>>> def cslice(iterable, *selectors):
    return chain(*(islice(iterable,*s) for s in selectors))

>>> list(cslice(it,(1,5),(10,15),(25,None,3)))
[1, 2, 3, 4, 10, 11, 12, 13, 14, 25, 28, 31, 34, 37, 40, 43, 46, 49]

Upvotes: 0

Ignacio Vazquez-Abrams
Ignacio Vazquez-Abrams

Reputation: 798706

You're probably better off writing your own sequence type.

>>> L = range(20)
>>> L
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
>>> operator.itemgetter(*(range(1, 5) + range(10, 18, 3)))(L)
(1, 2, 3, 4, 10, 13, 16)

And to get you started on that:

>>> operator.itemgetter(*(range(*slice(1, 5).indices(len(L))) + range(*slice(10, 18, 3).indices(len(L)))))(L)
(1, 2, 3, 4, 10, 13, 16)

Upvotes: 1

Related Questions