Pobieranie liczby elementów w iteratorze w Pythonie


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


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.


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.


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


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


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

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


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


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.


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)

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.


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.


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


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

-1
def count_iter(iter):
    sum = 0
    for _ in iter: sum += 1
    return sum

-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

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ę
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.