Для пользовательского контейнера, обертывающего итератор, я намеревался делегировать часть своей логики itertools.islice
. Одним из соображений было избежать ненужных итераций по обернутому итератору.
При вызове itertools.islice(iterable, start, stop, step)
с помощью stop<=start
результатом, как и ожидалось, является пустой генератор.
Но даже несмотря на то, что это не является абсолютно необходимым, itertools.islice(iterable, start, stop, step)
всегда будет повторять как минимум start
число раз по итерируемому объекту.
Тестовый пример для воспроизведения:
from unittest.mock import Mock
import itertools
iterableMock = Mock()
iterableMock.__iter__ = Mock(return_value=iterableMock)
iterableMock.__next__ = Mock(side_effect=range(10))
iterable = iterableMock
start = 5
stop = 0
step = None
isliceResult = list(itertools.islice(iterable, start, stop, step))
assert isliceResult == []
assert iterableMock.__next__.call_count == 0 # <= FAILS since call_count == 5
Это поведение:
Документация может немного вводить в заблуждение/неправильно истолковывать ожидаемое поведение.
Предлагаемая «эквивалентная реализация» и исходный код явно повторяются до start-1
. Параметр stop
учитывается только впоследствии.
Мне кажется, это дизайнерское решение. Можно представить себе другой случай, когда именно это и требуется. Вы можете написать свою собственную оболочку islice, которая будет работать так, как вы хотите.
@jsbueno - Я бы не согласился с тем, что это побочный эффект. Предположим, у меня есть файловый итератор. islice гарантирует, что я использовал строки до start-1. Это не крайний случай и не проблема исключительно производительности.
Действительно, я думаю, что основной вариант использования islice — пропустить первые n элементов. Во всяком случае, я не упоминал об этом в своем ответе.
Представьте, что вы вызываете islice()
несколько раз для одного и того же итератора. Вам необходимо убедиться, что каждый из них пропускает start
элементы, чтобы следующий звонок начался в нужном месте.
Я предлагаю использовать более простой код и показать его вывод. Я был незнаком со всем этим Mock, и меня это отвлекает, и я подозреваю, что я не одинок.
Просто реализация не проверяет заранее, больше ли stop
, чем start
.
Вы можете проверить это перед вызовом islice
, в этом случае djust предоставит пустой итератор:
isliceResult = list(itertools.islice(iterable, start, stop, step) if stop >= start) else ())
(выше ()
обозначает пустой кортеж).
Обратите внимание, что по самой природе islice
аргумент step
не может быть отрицательным, поэтому условие start <= stop
достаточно для всех рабочих случаев. Если вы ожидаете получить действительные отрицательные шаги, то, вероятно, будет проще написать собственный код, чем пытаться повторно использовать islice
для этого случая.
Использование элементов базового итератора является частью контракта islice
и не может быть оптимизировано. В разделе рецептов официальной документации 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:
collections.deque(iterator, maxlen=0)
else:
next(islice(iterator, n, n), None)
Однако контракт возник скорее по исторической случайности, чем по факту замысла.
Рэймонд Хеттингер, человек, который разработал функцию и написал оригинальную документацию, не собирался islice
давать какие-либо обещания относительно конечного состояния базового итератора - даже обещания, на которые опирается consume
рецепт. Цитирую один из постов Рэймонда в разговоре об аналогичном крайнем случае с step
:
В настоящее время нет никаких обещаний или гарантий относительно конечного состояния. итератора.
И их следующий пост в этом разговоре:
Я написал инструменты, документацию и тесты. Если вы интерпретируете «обещание» в тексте, могу вас заверить, что оно не было задумано. Поведение не определено, потому что я никогда его не определял. Я рад разъяснить документацию, чтобы сделать это явным.
В результате этого разговора реализация была скорректирована, чтобы изменить поведение в этом крайнем случае, и с этого момента детали потребления элементов считались определенными.
Спустя годы кто-то другой поднял этот start>stop
случай. Ответ Раймонда включает следующую строку:
Наличие islice() всегда потребляет как минимум «начальное» количество значений кажется достаточно разумным, и это задокументированное поведение: «Если start не равно нулю, то элементы из итерации пропускаются до тех пор, пока не будет достигнут старт. ... Если stop равен нулю. Нет, то итерация продолжается до тех пор, пока итератор не будет исчерпан, если в противном случае он останавливается в указанной позиции».
указывая, что они считали потребление start
предметов в start>stop
случае частью документально подтвержденного договора.
Этот аргумент на самом деле не работает, поскольку start==остановимся на этом.
@nocomment: Любой аргумент о том, что использование элементов не является необходимым для start>stop, в равной степени применим и к start==stop.
Нет, конфликт с цитируемым «останавливается в указанной позиции» — это конфликт только тогда, когда start>stop.
@nocomment: Ах, это то, на чем ты сосредоточен. Поскольку в вопросе потребление элементов представлено как «ненужное» и рассматривается с точки зрения проблемы производительности, мой ответ был сосредоточен на демонстрации того, что потребление элементов является частью контракта islice
и не может быть оптимизировано. Полагаю, я могу немного скорректировать ответ.
Да, вопрос конкретно о том, «когда stop меньше, чем start» (заголовок), поэтому спорить с чем-то, где это не так, довольно бессмысленно. Я сам иногда использовал stop=0, когда start было более длинным выражением, чем просто «n», и я с нетерпением жду действительно убедительного аргумента. Я просмотрел тестовые примеры, поскольку я бы воспринял их как демонстрацию намерения, но я не думаю, что это какое-либо тестирование. Я вижу некоторые с start>stop и один, который потом проверяет состояние итератора, но ни один не тестирует оба.
@nocomment: сейчас я скорректировал свой ответ.
Теперь выглядит великолепно. Автор вопроса start>stop даже сказал: «Не знаю, случайное это совпадение или намеренное», что очень близко к этому вопросу. Я интерпретировал документ так, как сказал там Рэймонд, хотя я думаю, что это можно понимать по-другому (начальная часть не имеет приоритета над стоповой частью), но Рэймонд ясно дал понять, что это значит. Я просто озадачен, почему он сказал, что изменение islice на отказ от потребления чего-либо «стимулирует новую и странную идиому для «потребления» как islice(it, n, 0)», поскольку тогда это даже не сработает. В любом случае... Стоит ли удалять наши комментарии?
«Просто побочный эффект текущей реализации, который влияет на производительность только в некоторых крайних случаях». <- это