Метод, который значительно быстрее, чем sum(1 for i in it)
, когда итерация может быть длинной (и не значительно медленнее, когда итерация короткая), при этом поддерживается фиксированное поведение памяти (в отличие от len(list(it))
), чтобы избежать перебора и перераспределения для большихвходные данные:
# On Python 2 only, get zip that lazily generates results instead of returning list
from future_builtins import zip
from collections import deque
from itertools import count
def ilen(it):
# Make a stateful counting iterator
cnt = count()
# zip it with the input iterator, then drain until input exhausted at C level
deque(zip(it, cnt), 0) # cnt must be second zip arg to avoid advancing too far
# Since count 0 based, the next value is the count
return next(cnt)
Как и len(list(it))
, он выполняет цикл в C-коде на CPython (deque
, count
и zip
все реализованы на C);избегание выполнения байтового кода в цикле обычно является ключом к производительности в CPython.
Удивительно сложно придумать честные тестовые примеры для сравнения производительности (list
читы с использованием __length_hint__
, что вряд лидоступные для произвольных входных итераций, функции itertools
, которые не предоставляют __length_hint__
, часто имеют специальные режимы работы, которые работают быстрее, когда значение, возвращаемое в каждом цикле, освобождается до того, как запрошено следующее значение, что deque
с maxlen=0
Сделаю).Тестовый пример, который я использовал, был для создания функции генератора, которая будет принимать входные данные и возвращать генератор уровня C, в котором не было специальных itertools
возвращаемых оптимизаций контейнера или __length_hint__
, используя Python 3.3 yield from
:
def no_opt_iter(it):
yield from it
Затем с помощью ipython
%timeit
magic (с заменой различных констант на 100):
>>> %%timeit -r5 fakeinput = (0,) * 100
... ilen(no_opt_iter(fakeinput))
Когда ввод недостаточно велик, чтобы len(list(it))
мог вызвать проблемы с памятью, в LinuxПри работе на Python 3.5 x64 мое решение занимает примерно на 50% больше, чем def ilen(it): return len(list(it))
, независимо от длины ввода.
Для наименьшего из входов настройка стоит вызвать deque
/ zip
/ count
/ next
означает, что этот путь занимает бесконечно больше времени, чем def ilen(it): sum(1 for x in it)
(примерно на 200 нс на моей машине для ввода длины 0, что на 33% больше, чем при простом подходе sum
), но для более длинных входов,он работает примерно вдвое меньше на каждый дополнительный элемент;для входов длины 5 стоимость эквивалентна, а где-то в диапазоне длин 50-100 начальные издержки незаметны по сравнению с реальной работой;Подход sum
занимает примерно вдвое больше времени.
В основном, если использование памяти имеет значение, или входы не имеют ограниченного размера и вам важна скорость, а не краткость, используйте это решение.Если входные данные ограничены и маловаты, len(list(it))
, вероятно, лучше, и если они не ограничены, но простота / краткость имеет значение, вы должны использовать sum(1 for x in it)
.