Как ограничить итерации цикла в Python?

Скажем, у меня есть список элементов, и я хочу перебирать их по нескольким из них:

items = list(range(10)) # I mean this to represent any kind of iterable.
limit = 5

Наивная реализация

Python naïf, исходящий из других языков, вероятно, написал бы этот вполне исправный и эффективный (если бы унииоматический) код:

index = 0
for item in items: # Python `for` loop is a for-each.
    print(item)    # or whatever function of that item.
    index += 1
    if index == limit:
        break

Более идиоматическая реализация

Но Python имеет перечисление, которое приблизительно составляет примерно половину этого кода:

for index, item in enumerate(items):
    print(item)
    if index == limit: # There gotta be a better way.
        break

Итак, мы о сокращении дополнительного кода пополам. Но должен быть лучший способ.

Можно ли приблизить поведение псевдокода ниже?

Если enumerate взял еще один необязательный аргумент stop (например, он принимает аргумент start следующим образом: enumerate(items, start=1)), который, я думаю, был бы идеальным, но ниже не существует (см. документация по перечислению здесь):

# hypothetical code, not implemented:
for _, item in enumerate(items, start=0, stop=limit): # `stop` not implemented
    print(item)

Обратите внимание, что нет необходимости указывать index, потому что нет необходимости ссылаться на него.

Есть ли идиоматический способ написать выше? Как?

Второй вопрос: почему это не встроено в перечисление?

Ответы

Ответ 1

Как ограничить итерации цикла в Python?

for index, item in enumerate(items):
    print(item)
    if index == limit:
        break

Есть ли более короткий, идиоматический способ написать выше? Как?

Включая индекс

zip останавливается на кратчайшем итерабельном аргументе. (В отличие от поведения zip_longest, который использует самый длинный итеративный.)

range может обеспечить ограниченную итерабельность, которую мы можем передать zip вместе с нашим основным итерированием.

Итак, мы можем передать объект range (с его аргументом stop) в zip и использовать его как ограниченное перечисление.

zip(range(limit), items)

Использование Python 3, zip и range возвращает итерации, в которых конвейер данных вместо материализации данных в списках для промежуточных шагов.

for _, item in zip(range(limit), items):
    print(item)

Чтобы получить такое же поведение в Python 2, просто замените xrange на range и itertools.izip на zip.

from itertools import izip
for index, item in izip(xrange(limit), items):
    print(item)

Если не требуется индекс, itertools.islice

Вы можете использовать itertools.islice:

for item in itertools.islice(items, 0, stop):
    print(item)

который не требует присвоения индекса.

Почему это не встроено в enumerate?

Здесь перечисление реализовано в чистом Python (с возможными изменениями для получения желаемого поведения в комментариях):

def enumerate(collection, start=0):  # could add stop=None
    i = start
    it = iter(collection)
    while 1:                         # could modify to `while i != stop:`
        yield (i, next(it))
        i += 1

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

_enumerate = enumerate

def enumerate(collection, start=0, stop=None):
    if stop is not None:
        return zip(range(start, stop), collection)
    return _enumerate(collection, start)

Эта дополнительная проверка будет иметь незначительное незначительное влияние на производительность.

Что касается того, почему перечисление не имеет аргумента stop, это было первоначально предложено (см. PEP 279):

Эта функция была первоначально предложена с дополнительным запуском     и остановить аргументы. GvR [Guido van Rossum] указал, что вызов функции      enumerate(seqn, 4, 6) имел альтернативную, правдоподобную интерпретацию как     срез, который будет возвращать четвертый и пятый элементы     последовательность. Чтобы избежать двусмысленности, необязательные аргументы были     хотя это означало потеря гибкости в качестве счетчика циклов.     Эта гибкость была наиболее важной для общего случая     считая от одного, как в:

for linenum, line in enumerate(source,1):  print linenum, line

Таким образом, очевидно, что start хранился, потому что он был очень ценным, а stop был отброшен, поскольку он имел меньше случаев использования и способствовал путанице в использовании новой функции.

Избегайте нарезки с нотной записью

В другом ответе говорится:

Почему бы просто не использовать

for item in items[:limit]: # or limit+1, depends

Вот несколько минус:

  • Он работает только для итераций, которые принимают разрез, поэтому он более ограничен.
  • Если они принимают разрез, он обычно создает новую структуру данных в памяти, а не выполняет итерацию над структурой ссылочных данных, поэтому она отнимает память (все встроенные объекты делают копии при разрезе, но, например, массивы numpy создают просмотр при нарезке).
  • Unsliceable iterables потребует другого вида обработки. Если вы переключитесь на ленивую оценочную модель, вам придется также изменить код с нарезкой.

Вы должны использовать разрез только с нотной надписью, когда понимаете ограничения и делаете ли это копию или представление.

Заключение

Я бы предположил, что теперь сообщество Python знает об использовании перечисления, затраты на беспорядок будут перевешиваться значением аргумента.

До этого времени вы можете использовать:

for index, element in zip(range(limit), items):
    ...

или, если вам не нужен индекс вообще:

for element in islice(items, 0, limit):
    ...

И избегайте нарезки с нотными обозначениями, если вы не понимаете ограничений.

Ответ 2

Вы можете использовать itertools.islice. Он принимает аргументы start, stop и step, если вы передаете только один аргумент, тогда он считается stop. И он будет работать с любым итерабельным.

itertools.islice(iterable, stop)
itertools.islice(iterable, start, stop[, step])

Demo:

>>> from itertools import islice
>>> items = list(range(10))
>>> limit = 5
>>> for item in islice(items, limit):
    print item,
...
0 1 2 3 4

Пример из документов:

islice('ABCDEFG', 2) --> A B
islice('ABCDEFG', 2, 4) --> C D
islice('ABCDEFG', 2, None) --> C D E F G
islice('ABCDEFG', 0, None, 2) --> A C E G

Ответ 3

Почему бы просто не использовать

for item in items[:limit]: # or limit+1, depends
    print(item)    # or whatever function of that item.

Это будет работать только для некоторых итераций, но поскольку вы указали Списки, он работает.

Это не работает, если вы используете Sets или dicts и т.д.