Какой самый короткий способ подсчитать количество элементов в генераторе / итераторе?

Если я хочу, чтобы количество элементов в итерируемом элементе не заботилось о самих элементах, каким был бы питонный способ получить это? Прямо сейчас я бы определил

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)

Метод, который значительно быстрее, чем 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
Другие вопросы по тегам