Python: итерация по подсписке

Как правило, если вы хотите перебирать часть списка в Python, проще всего просто нарезать список.

# Iterate over everything except the first item in a list
#
items = [1,2,3,4]
iterrange = (x for x in items[1:])

Но оператор среза создает новый список, который даже не нужно делать во многих случаях. В идеале, мне нужна какая-то функция среза, которая создает генераторы, а не новые объекты списка. Нечто похожее на это может быть выполнено путем создания выражения генератора, которое использует range для возврата только определенных частей списка:

# Create a generator expression that returns everything except 
# the first item in the list
#
iterrange = (x for x, idx in zip(items, range(0, len(items))) if idx != 0)

Но это довольно громоздко. Мне интересно, есть ли лучший, более элегантный способ сделать это. Итак, какой самый простой способ разрезать список, чтобы вместо генерации было создано выражение генератора?

Ответы

Ответ 1

Используйте itertools.islice:

import itertools

l = range(20)

for i in itertools.islice(l,10,15):
    print i

10
11
12
13
14

Из документа:

Сделайте итератор, который возвращает выбранные элементы из итеративного

Ответ 2

Прежде чем я начну, чтобы быть ясным, правильный порядок выбора между подходами нарезки обычно:

  1. Используйте регулярные срезы (стоимость копирования всех, кроме самых длинных входных данных, как правило, не имеет смысла, а код намного проще). Если входные данные могут не иметь секвенируемый тип последовательности, преобразуйте его в один, а затем в allbutone = list(someiterable)[1:], например, allbutone = list(someiterable)[1:]. Это проще и в большинстве случаев обычно быстрее, чем любой другой подход.
  2. Если регулярное нарезание не является жизнеспособным (входные данные не обязательно являются последовательностью, и преобразование в последовательность перед нарезкой может вызвать проблемы с памятью, или оно огромно, и срез покрывает большую часть этого, например, пропуская первые 1000 и последние 1000 элементов из list элементов 10M, так что может возникнуть проблема с itertools.islice), itertools.islice обычно является правильным решением, поскольку оно достаточно простое, а затраты на производительность обычно не важны.
  3. Если и только если производительность islice неприемлемо islice (это добавляет некоторые накладные расходы на производство каждого элемента, хотя по общему признанию это довольно небольшой объем) и объем пропускаемых данных невелик, тогда как объем данных, которые нужно включить, огромен (например, OP сценарий пропуска одного элемента и сохранения остальных), продолжайте читать

Если вы окажетесь в случае № 3, вы находитесь в сценарии, в котором способность islice обойти начальные элементы (относительно) недостаточна, чтобы компенсировать дополнительные затраты на производство остальных элементов. В этом случае вы можете улучшить производительность, изменив свою проблему: от выбора всех элементов после n до отказа от всех элементов до n.

Для этого подхода вы вручную конвертируете свои входные данные в итератор, затем явно извлекаете и отбрасываете n значений, а затем повторяете то, что осталось в итераторе (но без дополнительных islice на каждый элемент islice). Например, для ввода myinput = list(range(1, 10000)) ваши варианты выбора элементов от 1 до конца:

# Approach 1, OP approach, simple slice:
for x in myinput[1:]:

# Approach 2, Sebastian approach, using itertools.islice:
for x in islice(myinput, 1, None):

# Approach 3 (my approach)
myiter = iter(myinput)  # Explicitly create iterator from input (looping does this already)
next(myiter, None) # Throw away one element, providing None default to avoid StopIteration error
for x in myiter:  # Iterate unwrapped iterator

Если количество отбрасываемых элементов больше, вероятно, лучше позаимствовать рецепт consume из документации itertools:

def consume(iterator, n=None):
    "Advance the iterator n-steps ahead. If n is None, consume entirely."
    # Use functions that consume iterators at C speed.
    if n is None:
        # feed the entire iterator into a zero-length deque
        collections.deque(iterator, maxlen=0)
    else:
        # advance to the empty slice starting at position n
        next(islice(iterator, n, n), None)

что обобщает подходы для пропуска n элементов:

# Approach 1, OP approach, simple slice:
for x in myinput[n:]:

# Approach 2, Sebastian approach, using itertools.islice:
for x in islice(myinput, n, None):

# Approach 3 (my approach)
myiter = iter(myinput)  # Explicitly create iterator from input (looping does this already)
consume(myiter, n)      # Throw away n elements
# Or inlined consume as next(islice(myiter, n, n), None)
for x in myiter:        # Iterate unwrapped iterator

С точки зрения производительности, это выигрывает на значительную величину для большинства больших входных данных (исключение: сам range на Python 3 уже оптимизирован для простой нарезки; простая нарезка не может быть разбита на реальных объектах range). ipython3 (в CPython 3.6, 64-битная сборка Linux) иллюстрируют это (определение slurp в настройке - это просто способ сделать подход с минимальными издержками на выполнение итеративным, поэтому мы минимизируем влияние того, что нам не интересно в):

>>> from itertools import islice
>>> from collections import deque
>>> %%timeit -r5 slurp = deque(maxlen=0).extend; r = list(range(10000))
... slurp(r[1:])
...
65.8 μs ± 109 ns per loop (mean ± std. dev. of 5 runs, 10000 loops each)

>>> %%timeit -r5 slurp = deque(maxlen=0).extend; r = list(range(10000))
... slurp(islice(r, 1, None))
...
70.7 μs ± 104 ns per loop (mean ± std. dev. of 5 runs, 10000 loops each)

>>> %%timeit -r5 slurp = deque(maxlen=0).extend; r = list(range(10000))
... ir = iter(r)
... next(islice(ir, 1, 1), None)  # Inlined consume for simplicity, but with islice wrapping to show generalized usage
... slurp(ir)
...
30.3 μs ± 64.1 ns per loop (mean ± std. dev. of 5 runs, 10000 loops each)

Очевидно, что дополнительная сложность моего решения, как правило, не стоит того, но для входных данных среднего размера (в данном случае 10 тыс. Элементов) выигрыш в производительности очевиден; islice был худшим исполнителем (по небольшому количеству), обычная нарезка была немного лучше (что подтверждает мою точку зрения на то, что простая нарезка почти всегда является лучшим решением, когда у вас есть фактическая последовательность), а также "преобразовать в итератор, отбросить начальную, использовать Подход "покоя" выиграл в огромной сумме, условно говоря (значительно меньше, чем вдвое меньшее из решений).

Это преимущество не islice для крошечных вводов, потому что фиксированные накладные расходы на загрузку/вызов iter/next, и особенно islice, перевесят экономию:

>>> %%timeit -r5 slurp = deque(maxlen=0).extend; r = list(range(10))
... slurp(r[1:])
...
207 ns ± 1.86 ns per loop (mean ± std. dev. of 5 runs, 1000000 loops each)

>>> %%timeit -r5 slurp = deque(maxlen=0).extend; r = list(range(10))
... slurp(islice(r, 1, None))
...
307 ns ± 1.71 ns per loop (mean ± std. dev. of 5 runs, 1000000 loops each)

>>> %%timeit -r5 slurp = deque(maxlen=0).extend; r = list(range(10))
... ir = iter(r)
... next(islice(ir, 1, 1), None)  # Inlined consume for simplicity, but with islice wrapping to show generalized usage
... slurp(ir)
...
518 ns ± 4.5 ns per loop (mean ± std. dev. of 5 runs, 1000000 loops each)

>>> %%timeit -r5 slurp = deque(maxlen=0).extend; r = list(range(10))
... ir = iter(r)
... next(ir, None)  # To show fixed overhead of islice, use next without it
... slurp(ir)
...
341 ns ± 0.947 ns per loop (mean ± std. dev. of 5 runs, 1000000 loops each)

но, как вы можете видеть, даже для 10 элементов islice -free не намного хуже; на 100 элементов islice -free быстрее, чем у всех конкурентов, а на 200 элементов обобщенный next + islice превосходит всех конкурентов (очевидно, он не islice -free, учитывая 180 нс накладных расходов на islice, но это компенсируется пропуском n элементов как одного шага, вместо необходимости повторного вызова next для пропуска более одного элемента). Обычный islice редко побеждает в деле "пропусти несколько, сохраняй много" из-за накладных расходов на каждый элемент, требуемых оболочкой (он явно не побеждает стремительного нарезания микробенчмарков до тех пор, пока не наберется около 100 КБ элементов; это неэффективно для памяти, но неэффективно для ЦП) и это будет еще хуже (по сравнению с рвением нарезать) в случае "пропустить много, сохранить несколько".