Отримання кількості елементів в ітераторі в 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
Мені здається, що це робить саме те, що ОП не хоче робити: повторіть ітератор і порахуйте.
Адам Кросленд

36
Це просторний спосіб підрахунку елементів в ітерабелі
Капітан Лептон

9
Хоча це не те, чого хоче ОП, враховуючи, що на його запитання немає відповіді, ця відповідь дозволяє уникнути подання списку, і це емпірично швидше константа, ніж метод зменшення, перерахований вище.
Філіп Нордволл

5
Не можу допомогти: це _посилання на Perl $_? :)
Alois Mahdal

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

67

Ні, будь-який метод не вимагатиме від вас вирішення кожного результату. Ви можете зробити

iter_length = len(list(iterable))

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

Розповісти нам, яку реальну проблему ви намагаєтеся вирішити, може допомогти нам знайти кращий спосіб досягти вашої реальної мети.

Редагувати: Використання list()дозволить прочитати весь ітерабельний запис в пам'яті відразу, що може бути небажаним. Інший спосіб - це зробити

sum(1 for _ in iterable)

як опублікувала інша людина. Це дозволить уникнути збереження його в пам'яті.


проблема полягає в тому, що я читаю файл із "pysam", який містить мільйони записів. Pysam повертає ітератор. Щоб обчислити певну кількість, мені потрібно знати, скільки прочитаних є у файлі, але мені не потрібно читати кожне ... ось у чому проблема.

6
Я не користувач pysam, але, мабуть, читає файл "ледачий". Це має сенс, тому що ви не хочете мати великий файл в пам'яті. Тож якщо ви мусите знати, що ні. записів перед ітерацією, єдиний спосіб - створити два ітератори, і використовувати перший для підрахунку елементів, а другий - для читання файлів. До речі. Не використовуйте len(list(iterable))це завантажує всі дані в пам'ять. Ви можете використовувати: reduce(lambda x, _: x+1, iterable, 0). Редагувати: Код Zonda333 із сумою також хороший.
Tomasz Wysocki

1
@ user248237: чому ти кажеш, що потрібно знати, скільки записів доступно для обчислення певної кількості? Ви можете просто прочитати фіксовану їх кількість та керувати випадком, коли фіксовано менше, ніж це (це дуже просто зробити за допомогою iterslice). Чи є ще одна причина, що вам доведеться прочитати всі записи?
kriss

1
@Tomasz Зауважте, що зменшення застаріло, і воно піде в Python 3 і вище.
Wilduck

7
@Wilduck: Він не пішов, просто переїхав доfunctools.reduce
Daenyth

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

дуже приємна відповідь. дав би щедрість за це.
Reut

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

('список, сек', 1.9684218849870376)

2: test_list_compr.py:8: 0.867 KiB

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 KiB

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,226 KiB

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 KiB

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

('зменшити, сек', 13.436614598002052) `` `

Отже, len(list(gen))є найчастішим і менш витраченим у пам'яті


Як ви вимірювали споживання пам’яті?
Норманій

Чи можете ви пояснити, чому len(list(gen))слід споживати менше пам'яті, ніж підхід, заснований на зменшенні? Перший створює новий, listякий передбачає розподіл пам'яті, а другий не повинен. Тож я б очікував, що останній буде більш ефективним у пам’яті. Також витрата пам'яті буде залежати від типу елемента.
Норманій

FYI: Я можу відтворити для python 3.6.8 (на MacBookPro), що метод 1 перевершує інші методи з точки зору виконання (я пропустив метод 4).
норманій

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

9

Ітератор - це просто об’єкт, який має вказівник на наступний об'єкт, який слід прочитати якимось буфером або потоком, це як LinkedList, де ви не знаєте, скільки у вас є речей, поки ви не повторите їх. Ітератори повинні бути ефективними, тому що все, що вони роблять, - це сказати вам, що далі за посиланнями, а не використовувати індексацію (але, як ви побачили, ви втрачаєте можливість бачити, скільки наступних записів).


2
Ітератор - це не що інше, як пов'язаний список. Об'єкт, повернутий з ітератора, не вказує на наступний об'єкт, і ці об'єкти не (обов'язково) зберігаються в пам'яті. Швидше, він може давати об'єкт один за одним, виходячи з будь-якої внутрішньої логіки (яка може бути, але не повинна бути, спираючись на збережений список).
Том

1
@Tom я використовував 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 підтримує Моніку
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.