Какой самый короткий способ подсчитать количество элементов в генераторе / итераторе?
Если я хочу, чтобы количество элементов в итерируемом элементе не заботилось о самих элементах, каким был бы питонный способ получить это? Прямо сейчас я бы определил
def ilen(it):
return sum(itertools.imap(lambda _: 1, it)) # or just map in Python 3
но я понимаю lambda
близок к тому, чтобы считаться вредным, и lambda _: 1
конечно не красиво.
(Вариант использования этого - подсчет количества строк в текстовом файле, соответствующих регулярному выражению, т.е. grep -c
.)
8 ответов
Метод, который значительно быстрее, чем 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
магия (подставляя разные константы на 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
len(list(it))
Хотя может зависнуть, если это бесконечный генератор.
Мне нравится пакет мощности для этого, он очень легкий и пытается использовать самую быструю из возможных реализаций в зависимости от итерируемого.
Использование:
>>> 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)))
Если вы хотите использовать итерацию в другом месте и знать, сколько элементов было использовано, вы можете создать простой класс-оболочку:
from collections.abc import Iterable, Iterator
from typing import Generic, TypeVar
_T = TypeVar("_T")
class IterCounter(Generic[_T]):
"""Iterator that keeps count of the consumed elements"""
def __init__(self, iterable: Iterable[_T]) -> None:
self._iterator = iter(iterable)
self.count = 0
def __iter__(self) -> Iterator[_T]:
return self
def __next__(self) -> _T:
element = next(self._iterator)
self.count += 1
return element
counter = IterCounter(range(5))
print(counter.count) # 0
print(list(counter)) # [0, 1, 2, 3, 4]
print(counter.count) # 5