Получение количества элементов в итераторе в Python


Ответы:


101

Нет, это невозможно.

Пример:

import random

def gen(n):
    for i in xrange(n):
        if random.randint(0, 1) == 0:
            yield i

iterator = gen(10)

Длина iteratorнеизвестна, пока вы не выполните итерацию.


14
С другой стороны, def gen(): yield random.randint(0, 1)оно бесконечно, поэтому вы никогда не сможете найти длину, перебирая ее.
tgray

1
Итак, чтобы подтвердить очевидное: лучший способ получить «размер» итератора - просто посчитать, сколько раз вы прошли итерацию, верно? В таком случае это будет numIters = 0 ; while iterator: numIters +=1?
Майк Уильямсон

Интересно, так что это проблема остановки
Акабаба

231

Этот код должен работать:

>>> iter = (i for i in range(50))
>>> sum(1 for _ in iter)
50

Хотя он выполняет итерацию по каждому элементу и считает их, это самый быстрый способ сделать это.

Это также работает, когда итератор не имеет элемента:

>>> sum(1 for _ in range(0))
0

Конечно, он работает вечно для бесконечного ввода, поэтому помните, что итераторы могут быть бесконечными:

>>> sum(1 for _ in itertools.count())
[nothing happens, forever]

Также имейте в виду, что при этом итератор будет исчерпан , и дальнейшие попытки его использования не будут видеть элементов . Это неизбежное следствие дизайна итератора Python. Если вы хотите сохранить элементы, вам придется хранить их в списке или что-то в этом роде.


10
Похоже, это именно то, что OP не хочет делать: перебирать итератор и считать.
Адам Кроссленд

36
Это эффективный способ подсчета элементов в итерируемом виде
капитан

9
Хотя это не то, что хочет OP, учитывая, что на его вопрос нет ответа, этот ответ избегает создания экземпляра списка, и он эмпирически быстрее по константе, чем метод сокращения, указанный выше.
Филипп Нордвол

5
Не могу помочь: это _ссылка на Perl $_? :)
Алоис Махдал

17
@AloisMahdal Нет. В Python принято использовать имя _для фиктивной переменной, значение которой вас не волнует.
Таймон

67

Нет, любой метод потребует от вас разрешения каждого результата. Ты можешь сделать

iter_length = len(list(iterable))

но выполнение этого на бесконечном итераторе, конечно, никогда не вернется. Он также будет использовать итератор, и его необходимо будет сбросить, если вы хотите использовать содержимое.

Если вы сообщите нам, какую реальную проблему вы пытаетесь решить, это может помочь вам найти более эффективный способ достижения вашей реальной цели.

Изменить: Использование list()будет читать все повторяемые в памяти сразу, что может быть нежелательно. Другой способ сделать

sum(1 for _ in iterable)

как написал другой человек. Это позволит избежать сохранения в памяти.


проблема в том, что я читаю файл с "pysam", который имеет миллионы записей. Пысам возвращает итератор. Чтобы вычислить определенное количество, мне нужно знать, сколько операций чтения в файле, но мне не нужно читать каждое из них ... вот в чем проблема.

6
Я не пользователь pysam, но он, вероятно, читает файл "lazy". Это имеет смысл, потому что вы не хотите иметь большой файл в памяти. Так что, если вы должны знать, нет. записей до итерации, единственный способ - создать два итератора и использовать первый для подсчета элементов, а второй для чтения файла. КСТАТИ. Не используйте len(list(iterable))это загрузит все данные в память. Вы можете использовать: reduce(lambda x, _: x+1, iterable, 0). Изменить: Zonda333 код с суммой тоже хорошо.
Томаш Высоцкий

1
@ user248237: почему вы говорите, что вам нужно знать, сколько записей доступно для вычисления определенного количества? Вы можете просто прочитать их фиксированное количество и управлять случаем, когда их меньше, чем фиксированное количество (это действительно просто сделать с помощью iterslice). Есть ли еще одна причина, по которой вы должны прочитать все записи?
Крис

1
@Tomasz Обратите внимание, что Reduce устарело и исчезнет в Python 3 и выше.
Уилдак

7
@Wilduck: это не ушло, просто переехал вfunctools.reduce
Дейнит

33

Вы не можете (кроме типа конкретного итератора реализует некоторые конкретные методы, которые делают это возможным).

Как правило, вы можете считать элементы итератора только, потребляя итератор. Один из, вероятно, самых эффективных способов:

import itertools
from collections import deque

def count_iter_items(iterable):
    """
    Consume an iterable not reading it into memory; return the number of items.
    """
    counter = itertools.count()
    deque(itertools.izip(iterable, counter), maxlen=0)  # (consume at C speed)
    return next(counter)

(Для Python 3.x заменить itertools.izipна zip).


3
+1: по сравнению со временем sum(1 for _ in iterator)это было почти в два раза быстрее.
августен

1
Точнее сказать, что он потребляет многократно, считывая каждый элемент в память и сразу выбрасывая его.
Rockallite

Важно отметить (что я упустил), что порядок аргументов zipимеет значение : если вы пройдете zip(counter, iterable), вы на самом деле получите на 1 больше, чем количество итераций!
Kye W Shi

очень хороший ответ. дал бы щедрость на это.
Реут

18

Вроде. Вы можете проверить __length_hint__метод, но имейте в виду, что (по крайней мере, до Python 3.4, как подсказывает gsnedders), это недокументированная деталь реализации ( после сообщения в теме ), которая может очень легко исчезнуть или вызвать назальных демонов.

В противном случае нет. Итераторы - это просто объект, который раскрывает только next()метод. Вы можете назвать это столько раз, сколько потребуется, и они могут или не могут в конечном итоге повысить StopIteration. К счастью, такое поведение в большинстве случаев прозрачно для кодировщика. :)


5
Это больше не относится к PEP 424 и Python 3.4. __length_hint__В настоящее время задокументировано, но это подсказка и не дает никаких гарантий точности.
gsnedders

12

Мне нравится пакет мощности для этого, он очень легкий и пытается использовать самую быструю из возможных реализаций в зависимости от итерируемого.

Использование:

>>> 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

Фактическая count()реализация выглядит следующим образом:

def count(iterable):
    if hasattr(iterable, '__len__'):
        return len(iterable)

    d = collections.deque(enumerate(iterable, 1), maxlen=1)
    return d[0][0] if d else 0

Я предполагаю, что вы все еще можете перебирать итератор, если используете эту функцию, да?
Jcollum

12

Итак, для тех, кто хотел бы узнать краткое содержание этого обсуждения. Итоговые максимальные оценки для подсчета выражения генератора длиной 50 миллионов с использованием:

  • len(list(gen)),
  • len([_ for _ in gen]),
  • sum(1 for _ in gen),
  • ilen(gen)(из more_itertool ),
  • reduce(lambda c, i: c + 1, gen, 0),

отсортированный по производительности выполнения (включая потребление памяти), удивит вас:

`` `

1: test_list.py:8: 0,492 КиБ

gen = (i for i in data*1000); t0 = monotonic(); len(list(gen))

('list, sec', 1.9684218849870376)

2: test_list_compr.py:8: 0,867 КиБ

gen = (i for i in data*1000); t0 = monotonic(); len([i for i in gen])

('list_compr, sec', 2.5885991149989422)

3: test_sum.py:8: 0,859 КиБ

gen = (i for i in data*1000); t0 = monotonic(); sum(1 for i in gen); t1 = monotonic()

(«сумма, сек», 3.441088170016883)

4: more_itertools / more.py: 413: 1,266 КиБ

d = deque(enumerate(iterable, 1), maxlen=1)

test_ilen.py:10: 0.875 KiB
gen = (i for i in data*1000); t0 = monotonic(); ilen(gen)

('ilen, sec', 9.812256851990242)

5: test_reduce.py:8: 0,859 КиБ

gen = (i for i in data*1000); t0 = monotonic(); reduce(lambda counter, i: counter + 1, gen, 0)

(«уменьшить, сек», 13.436614598002052) `` `

Итак, len(list(gen))это самый частый и менее потребляемый объем памяти


Как вы измерили потребление памяти?
норманиус

1
Можете ли вы объяснить, почему len(list(gen))следует использовать меньше памяти, чем подход, основанный на методе Reduce? Первый создает новый, listкоторый включает в себя распределение памяти, в то время как последний не должен. Так что я ожидаю, что последний будет более эффективным с точки зрения памяти. Кроме того, потребление памяти будет зависеть от типа элемента.
норманиус

К сведению: я могу воспроизвести для python 3.6.8 (на MacBookPro), что метод 1 превосходит другие методы с точки зрения времени выполнения (я пропустил метод 4).
норманиус

len(tuple(iterable))может быть еще более эффективным: статья Нельсона Минара
VMAtm

9

Итератор - это просто объект, у которого есть указатель на следующий объект, который должен быть прочитан каким-либо буфером или потоком, он похож на LinkedList, где вы не знаете, сколько у вас есть вещей, пока не выполните их итерацию. Предполагается, что итераторы эффективны, потому что все, что они делают, - это сообщают вам, что дальше, по ссылкам, а не используют индексацию (но, как вы видели, вы теряете способность видеть, сколько записей дальше).


2
Итератор не похож на связанный список. Объект, возвращаемый из итератора, не указывает на следующий объект, и эти объекты (не обязательно) хранятся в памяти. Скорее, он может выдавать объект один за другим, основываясь на какой-либо внутренней логике (которая может быть, но не обязана, на основе сохраненного списка).
Том

1
@ Я использовал LinkedList в качестве примера, главным образом в том, что вы не знаете, сколько у вас есть, так как вы знаете только то, что дальше в некотором смысле (если что-то есть). Я прошу прощения, если моя формулировка кажется немного неправильной или я подразумевал, что они - одно и то же.
Иисус Рамос

8

Что касается вашего первоначального вопроса, ответ по-прежнему заключается в том, что в общем случае нет способа узнать длину итератора в Python.

Учитывая, что ваш вопрос мотивирован приложением библиотеки pysam, я могу дать более конкретный ответ: я участвую в PySAM, и однозначный ответ заключается в том, что файлы SAM / BAM не обеспечивают точного количества выровненных чтений. Также эта информация не легко доступна из индексного файла BAM. Лучшее, что можно сделать, - это оценить приблизительное количество выравниваний, используя расположение указателя файла после считывания ряда выравниваний и экстраполяции на основе общего размера файла. Этого достаточно, чтобы реализовать индикатор выполнения, но не метод подсчета выравниваний за постоянное время.


6

Быстрый тест:

import collections
import itertools

def count_iter_items(iterable):
    counter = itertools.count()
    collections.deque(itertools.izip(iterable, counter), maxlen=0)
    return next(counter)

def count_lencheck(iterable):
    if hasattr(iterable, '__len__'):
        return len(iterable)

    d = collections.deque(enumerate(iterable, 1), maxlen=1)
    return d[0][0] if d else 0

def count_sum(iterable):           
    return sum(1 for _ in iterable)

iter = lambda y: (x for x in xrange(y))

%timeit count_iter_items(iter(1000))
%timeit count_lencheck(iter(1000))
%timeit count_sum(iter(1000))

Результаты:

10000 loops, best of 3: 37.2 µs per loop
10000 loops, best of 3: 47.6 µs per loop
10000 loops, best of 3: 61 µs per loop

Т.е. простой count_iter_items - это путь.

Настраиваем это для python3:

61.9 µs ± 275 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
74.4 µs ± 190 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
82.6 µs ± 164 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

Примечание: этот тест основан на python2
normanius

3

Есть два способа получить длину «чего-то» на компьютере.

Первый способ - сохранить счетчик - для его изменения требуется все, что касается файла / данных (или класс, который предоставляет только интерфейсы, но сводится к одному и тому же).

Другой способ - перебрать его и посчитать, насколько он велик.


0

Обычно такая информация помещается в заголовок файла, а pysam предоставляет вам доступ к этому. Я не знаю формат, но вы проверили API?

Как уже говорили другие, вы не можете узнать длину от итератора.


0

Это противоречит самому определению итератора, который является указателем на объект, плюс информация о том, как добраться до следующего объекта.

Итератор не знает, сколько еще раз он сможет выполнить итерацию до завершения. Это может быть бесконечно, поэтому бесконечность может быть вашим ответом.


Это ничего не нарушает, и нет ничего плохого в применении предыдущих знаний при использовании итератора. Есть миллионы итераторов вокруг, где вы знаете, что количество элементов ограничено. Подумайте о простой фильтрации списка, вы можете легко указать максимальную длину, вы просто не знаете, сколько элементов на самом деле соответствуют условиям вашего фильтра. Желание узнать количество совпадающих элементов является допустимым приложением, не нарушающим ни одной загадочной идеи итератора.
Майкл

0

Хотя в общем и целом невозможно выполнить то, что было задано, все равно часто полезно подсчитывать, сколько элементов было повторено после их повторения. Для этого вы можете использовать jaraco.itertools.Counter или аналогичный. Вот пример использования Python 3 и rwt для загрузки пакета.

$ rwt -q jaraco.itertools -- -q
>>> import jaraco.itertools
>>> items = jaraco.itertools.Counter(range(100))
>>> _ = list(counted)
>>> items.count
100
>>> import random
>>> def gen(n):
...     for i in range(n):
...         if random.randint(0, 1) == 0:
...             yield i
... 
>>> items = jaraco.itertools.Counter(gen(100))
>>> _ = list(counted)
>>> items.count
48


-1

Предположительно, вы хотите посчитать количество элементов без итераций, чтобы итератор не был исчерпан, и вы будете использовать его позже. Это возможно с copyилиdeepcopy

import copy

def get_iter_len(iterator):
    return sum(1 for _ in copy.copy(iterator))

###############################################

iterator = range(0, 10)
print(get_iter_len(iterator))

if len(tuple(iterator)) > 1:
    print("Finding the length did not exhaust the iterator!")
else:
    print("oh no! it's all gone")

Выход " Finding the length did not exhaust the iterator!"

По желанию (и неосознанно) вы можете скрыть встроенную lenфункцию следующим образом:

import copy

def len(obj, *, len=len):
    try:
        if hasattr(obj, "__len__"):
            r = len(obj)
        elif hasattr(obj, "__next__"):
            r = sum(1 for _ in copy.copy(obj))
        else:
            r = len(obj)
    finally:
        pass
    return r

1
Диапазоны не являются итераторами. Есть некоторые типы итераторов, которые могут быть скопированы, но другие приведут к сбою этого кода с помощью TypeError (например, генераторов), а итерация через скопированный итератор может вызвать побочные эффекты дважды или вызвать произвольный сбой в коде, который, скажем, вернул mapитератор, ожидающий, что вызовы функций будут происходить только один раз.
user2357112 поддерживает Monica
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.