Ответ 1
Обычный способ
sum(1 for i in it)
Если я хочу количество элементов в итерабеле, не заботясь о самих элементах, каков будет питонический способ получить это? Прямо сейчас, я бы определил
def ilen(it):
return sum(itertools.imap(lambda _: 1, it)) # or just map in Python 3
но я понимаю, что lambda
близок к тому, чтобы считаться вредным, а lambda _: 1
, конечно, не очень.
(Пример использования этого - подсчет количества строк в текстовом файле, соответствующем регулярному выражению, т.е. grep -c
.)
Обычный способ
sum(1 for i in it)
Метод, который значительно быстрее, чем sum(1 for i in it)
, когда итерабельность может быть длинной (и не значимо медленнее, если iterable является коротким), сохраняя при этом поведение накладных расходов фиксированной памяти (в отличие от 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)
.
Короткий путь:
def ilen(it):
return len(list(it))
Обратите внимание, что если вы генерируете множество элементов (например, десятки тысяч или более), то включение их в список может стать проблемой производительности. Однако это простое выражение идеи, в которой производительность не будет иметь большого значения для большинства случаев.
more_itertools
- сторонняя библиотека, которая реализует инструмент ilen
. pip install more_itertools
import more_itertools as mit
mit.ilen(x for x in range(10))
# 10
Мне нравится cardinality пакет для этого, он очень легкий и пытается использовать максимально возможную реализацию в зависимости от итерабельности.
Использование:
>>> import cardinality
>>> cardinality.count([1, 2, 3])
3
>>> cardinality.count(i for i in range(500))
500
>>> def gen():
... yield 'hello'
... yield 'world'
>>> cardinality.count(gen())
2
Это будет мой выбор, один или другой:
print(len([*gen]))
print(len(list(gen)))