Pobieranie liczby elementów w iteratorze w Pythonie

138

Czy istnieje skuteczny sposób, aby dowiedzieć się, ile elementów jest w iteratorze w Pythonie, ogólnie, bez iterowania przez każdy z nich i liczenia?

Tomasz Wysocki
źródło

Odpowiedzi:

101

Nie. To niemożliwe.

Przykład:

import random

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

iterator = gen(10)

Długość iteratorjest nieznana, dopóki nie przejdziesz przez nią.

Tomasz Wysocki
źródło
14
Alternatywnie, def gen(): yield random.randint(0, 1)jest nieskończona, więc nigdy nie będziesz w stanie znaleźć długości, iterując po niej.
tgray
1
Tak więc, aby potwierdzić oczywistość: najlepszym sposobem na uzyskanie „rozmiaru” iteratora jest po prostu policzenie, ile razy przeszedłeś przez iterację, prawda? W takim przypadku byłoby to numIters = 0 ; while iterator: numIters +=1?
Mike Williamson,
Ciekawe, więc jest to problem z zatrzymaniem
Akababa
231

Ten kod powinien działać:

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

Mimo że wykonuje iterację i zlicza każdy element, jest to najszybszy sposób.

Działa również wtedy, gdy iterator nie ma elementu:

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

Oczywiście dla nieskończonych danych wejściowych działa w nieskończoność, więc pamiętaj, że iteratory mogą być nieskończone:

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

Należy również pamiętać, że iterator zostanie w ten sposób wyczerpany , a dalsze próby jego użycia nie będą zawierać żadnych elementów . To nieunikniona konsekwencja projektu iteratora Pythona. Jeśli chcesz zachować elementy, będziesz musiał przechowywać je na liście lub w czymś takim.

John Howard
źródło
10
Wydaje mi się, że robi to dokładnie to, czego OP nie chce robić: iteruje iterator i liczy.
Adam Crossland
36
Jest to efektywny przestrzennie sposób liczenia elementów w iterowalnym
Captain Lepton,
9
Chociaż nie tego chce OP, biorąc pod uwagę, że jego pytanie nie ma odpowiedzi, ta odpowiedź pozwala uniknąć tworzenia instancji listy i jest empirycznie szybsza dzięki stałej niż metoda redukcji wymieniona powyżej.
Phillip Nordwall
5
Nic na to nie poradzę: czy jest to _odniesienie do Perla $_? :)
Alois Mahdal
17
@AloisMahdal Nie. W Pythonie zwyczajowo używa się nazwy _dla fikcyjnej zmiennej, której wartość nie jest dla nas ważna .
Taymon
67

Nie, każda metoda będzie wymagać rozwiązania każdego wyniku. Możesz to zrobić

iter_length = len(list(iterable))

ale uruchomienie tego na nieskończonym iteratorze oczywiście nigdy nie powróci. Będzie również zużywać iterator i będzie musiał zostać zresetowany, jeśli chcesz użyć zawartości.

Poinformowanie nas, jaki prawdziwy problem próbujesz rozwiązać, może pomóc nam znaleźć lepszy sposób na osiągnięcie rzeczywistego celu.

Edycja: użycie list()spowoduje natychmiastowe odczytanie całej iteracji do pamięci, co może być niepożądane. Innym sposobem jest zrobienie

sum(1 for _ in iterable)

jako inna osoba. Pozwoli to uniknąć utrzymywania go w pamięci.

Daenyth
źródło
problem polega na tym, że czytam plik z "pysam", który ma miliony wpisów. Pysam zwraca iterator. Aby obliczyć określoną ilość, muszę wiedzieć, ile odczytów znajduje się w pliku, ale nie muszę czytać każdego z nich ... to jest problem.
6
Nie jestem użytkownikiem pysam, ale prawdopodobnie czyta plik „leniwy”. Ma to sens, ponieważ nie chcesz mieć dużego pliku w pamięci. Więc jeśli musisz wiedzieć, nie. rekordów przed iteracją, jedynym sposobem jest utworzenie dwóch iteratorów i użycie pierwszego do zliczania elementów, a drugiego do odczytu pliku. BTW. Nie używaj len(list(iterable))go spowoduje załadowanie wszystkich danych do pamięci. Można użyć: reduce(lambda x, _: x+1, iterable, 0). Edycja: kod Zonda333 z sumą jest również dobry.
Tomasz Wysocki
1
@ user248237: dlaczego mówisz, że musisz wiedzieć, ile wpisów jest dostępnych, aby obliczyć określoną ilość? Możesz po prostu przeczytać określoną ich liczbę i zarządzać przypadkiem, gdy jest ich mniej niż ustalona ilość (naprawdę proste do zrobienia za pomocą iterslice). Czy jest jeszcze jeden powód, dla którego musisz czytać wszystkie wpisy?
kriss
1
@Tomasz Zwróć uwagę, że funkcja Redukcja jest przestarzała i zniknie w Pythonie 3 i nowszych.
Wilduck
7
@Wilduck: Nie ma go, właśnie przeniósł się dofunctools.reduce
Daenyth
33

Nie możesz (poza tym, że typ określonego iteratora implementuje określone metody, które to umożliwiają).

Ogólnie rzecz biorąc, możesz liczyć elementy iteratora tylko przez wykorzystanie iteratora. Jeden z prawdopodobnie najbardziej wydajnych sposobów:

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)

(Pythona 3.x wymienić itertools.izipz zip).

zuo
źródło
3
+1: w porównaniu z czasem sum(1 for _ in iterator)był prawie dwukrotnie szybszy.
augustomen
1
Dokładniej jest powiedzieć, że zużywa iterowalność, wczytując każdy element z pamięci i od razu go odrzucając.
Rockallite
Należy zauważyć (co przeoczyłem), że kolejność argumentów w zipsprawach : jeśli zdasz zip(counter, iterable), w rzeczywistości otrzymasz 1 więcej niż liczba iterowalna!
Kye W Shi
bardzo ładna odpowiedź. dałoby za to nagrodę.
Reut Sharabani
18

Kinda. Państwo mogli sprawdzić __length_hint__metodę, ale ostrzegam, że (przynajmniej do Python 3.4, jak gsnedders usłużnie zaznacza) jest to nieudokumentowane szczegółów wdrażania ( po wiadomości w wątku ), które mogłyby równie dobrze zniknąć lub wezwać demony zamiast nosa.

W przeciwnym razie nie. Iteratory to po prostu obiekt, który ujawnia tylko next()metodę. Możesz sprawdzać to tyle razy, ile potrzeba, a ostatecznie mogą, ale nie muszą, podbić StopIteration. Na szczęście to zachowanie jest przez większość czasu niewidoczne dla programisty. :)

badp
źródło
5
Nie ma to już miejsca, od PEP 424 i Python 3.4. __length_hint__jest teraz udokumentowana, ale jest to wskazówka i nie gwarantuje dokładności.
gsnedders,
12

Podoba mi się moc pakiet , jest bardzo lekki i stara się używać najszybszej możliwej implementacji dostępnej w zależności od iterowalnego.

Stosowanie:

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

Rzeczywista count()realizacja wygląda następująco:

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
Erwin Mayer
źródło
Zakładam, że nadal możesz iterować iterator, jeśli używasz tej funkcji, tak?
jcollum
12

A więc dla tych, którzy chcieliby poznać podsumowanie tej dyskusji. Ostateczne najwyższe wyniki za zliczanie wyrażenia generatora o długości 50 milionów przy użyciu:

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

posortowane według wydajności wykonania (w tym zużycia pamięci), sprawi, że będziesz zaskoczony:

`` ''

1: test_list.py:8: 0,492 KiB

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

(„list, sec”, 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: suma_testowa.py:8: 0,859 KiB

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

(„suma, s”, 3,441088170016883)

4: more_itertools / more.py: 413: 1,266 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)

('zmniejsz, s', 13.436614598002052) ``

Tak więc len(list(gen))jest to najczęściej i mniej zużywająca się pamięć

Alex-Bogdanov
źródło
Jak zmierzyłeś zużycie pamięci?
normanius
Czy możesz wyjaśnić, dlaczego len(list(gen))należy zużywać mniej pamięci niż podejście oparte na redukcji? Pierwsza tworzy nową, listktóra obejmuje alokację pamięci, podczas gdy druga nie powinna. Spodziewałbym się więc, że ten ostatni będzie bardziej wydajny w pamięci. Zużycie pamięci zależy również od typu elementu.
normanius
Do Twojej wiadomości: mogę odtworzyć dla Pythona 3.6.8 (na MacBookPro), że metoda 1 przewyższa inne metody pod względem czasu wykonywania (pominąłem metodę 4).
normanius
len(tuple(iterable)) może być jeszcze wydajniejsze: artykuł Nelsona
Minara
9

Iterator to po prostu obiekt, który ma wskaźnik do następnego obiektu, który ma być odczytany przez jakiś bufor lub strumień, jest jak lista LinkedList, w której nie wiesz, ile masz rzeczy, dopóki nie przejdziesz przez nie. Iteratory mają być wydajne, ponieważ jedyne, co robią, to informowanie cię o tym, co będzie dalej, zamiast korzystania z indeksowania (ale jak zobaczyłeś, tracisz możliwość sprawdzenia, ile wpisów jest następnych).

Jesus Ramos
źródło
2
Iterator w niczym nie przypomina listy połączonej. Obiekt zwrócony z iteratora nie wskazuje na następny obiekt, a obiekty te nie są (koniecznie) przechowywane w pamięci. Raczej może dostarczać obiekty jeden po drugim, w oparciu o jakąkolwiek wewnętrzną logikę (która może, ale nie musi, opierać się na przechowywanej liście).
Tom
1
@Tom Użyłem LinkedList jako przykładu głównie dlatego, że nie wiesz, ile masz, ponieważ wiesz tylko, co jest dalej w pewnym sensie (jeśli jest coś). Przepraszam, jeśli moje sformułowanie wydaje się trochę niewłaściwe lub jeśli zasugerowałem, że są takie same.
Jesus Ramos
8

Jeśli chodzi o twoje pierwotne pytanie, nadal odpowiedź brzmi, że ogólnie nie ma sposobu, aby poznać długość iteratora w Pythonie.

Biorąc pod uwagę, że Twoje pytanie jest motywowane aplikacją biblioteki pysam, mogę udzielić bardziej szczegółowej odpowiedzi: jestem współtwórcą PySAM i ostateczna odpowiedź jest taka, że ​​pliki SAM / BAM nie zapewniają dokładnej liczby wyrównanych odczytów. Informacje te nie są również łatwo dostępne w pliku indeksu BAM. Najlepsze, co można zrobić, to oszacować przybliżoną liczbę wyrównań, używając położenia wskaźnika pliku po odczytaniu liczby dopasowań i ekstrapolacji na podstawie całkowitego rozmiaru pliku. To wystarczy, aby zaimplementować pasek postępu, ale nie metodę zliczania wyrównań w stałym czasie.

Kevin Jacobs
źródło
6

Szybki test porównawczy:

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

Wyniki:

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

Tzn. Proste count_iter_items jest drogą do zrobienia.

Dostosowywanie tego dla 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)
Michael
źródło
Uwaga: ten test jest oparty na pythonie2
normanius
3

Istnieją dwa sposoby uzyskania długości „czegoś” na komputerze.

Pierwszym sposobem jest przechowywanie liczby - wymaga to wszystkiego, co dotyka pliku / danych, aby go zmodyfikować (lub klasy, która ujawnia tylko interfejsy - ale sprowadza się do tego samego).

Innym sposobem jest powtórzenie tego i policzenie, jak duże jest.

Wayne Werner
źródło
0

Powszechną praktyką jest umieszczanie tego typu informacji w nagłówku pliku, a pysam zapewnia do nich dostęp. Nie znam formatu, ale czy sprawdziłeś API?

Jak powiedzieli inni, nie możesz poznać długości z iteratora.

tom10
źródło
0

Jest to sprzeczne z samą definicją iteratora, który jest wskaźnikiem do obiektu oraz informacją o tym, jak dostać się do następnego obiektu.

Iterator nie wie, ile razy będzie w stanie wykonać iterację aż do zakończenia. To może być nieskończone, więc nieskończoność może być twoją odpowiedzią.

FCAlive
źródło
Nie narusza niczego i nie ma nic złego w stosowaniu wcześniejszej wiedzy podczas korzystania z iteratora. Wokół jest mnóstwo iteratorów, o których wiadomo, że liczba elementów jest ograniczona. Pomyśl o zwykłym przefiltrowaniu listy, możesz łatwo podać maksymalną długość, po prostu tak naprawdę nie wiesz, ile elementów faktycznie pasuje do warunku filtra. Chęć poznania liczby pasujących do siebie elementów jest poprawną aplikacją, nie naruszającą żadnej mistycznej idei iteratora.
Michael
0

Chociaż generalnie nie jest możliwe zrobienie tego, o co zostało poproszone, nadal często warto policzyć, ile elementów zostało powtórzonych po wykonaniu iteracji. W tym celu możesz użyć jaraco.itertools.Counter lub podobnego. Oto przykład użycia Pythona 3 i rwt do załadowania pakietu.

$ 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
Jason R. Coombs
źródło
-1
def count_iter(iter):
    sum = 0
    for _ in iter: sum += 1
    return sum
hasen
źródło
-1

Prawdopodobnie chcesz policzyć liczbę elementów bez iteracji, aby iterator nie został wyczerpany, i użyjesz go ponownie później. Jest to możliwe dzięki copylubdeepcopy

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

Wynik to „Finding the length did not exhaust the iterator!

Opcjonalnie (i niezalecane) możesz zasłonić wbudowaną lenfunkcję w następujący sposób:

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
Wykałaczka Anemone
źródło
1
Zakresy nie są iteratorami. Istnieje kilka typów iteratorów, które można skopiować, ale inne spowodują niepowodzenie tego kodu z błędem TypeError (np. Generatory), a iteracja przez skopiowany iterator może spowodować dwukrotne wystąpienie efektów ubocznych lub arbitralne uszkodzenie kodu, które, powiedzmy, zwrócił mapiterator oczekujący, że wynikowe wywołania funkcji wystąpią tylko raz.
user2357112 obsługuje Monikę