Mendapatkan jumlah elemen dalam iterator dengan Python

142

Apakah ada cara yang efisien untuk mengetahui berapa banyak elemen yang ada dalam sebuah iterator dengan Python, secara umum, tanpa melakukan iterasi melalui setiap dan penghitungan?

Tomasz Wysocki
sumber

Jawaban:

104

Tidak. Itu tidak mungkin.

Contoh:

import random

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

iterator = gen(10)

Panjangnya iteratortidak diketahui sampai Anda mengulanginya.

Tomasz Wysocki
sumber
14
Bergantian, def gen(): yield random.randint(0, 1)tidak terbatas, jadi Anda tidak akan pernah bisa menemukan panjang dengan mengulanginya.
tgray
1
Jadi, untuk memvalidasi yang sudah jelas: cara terbaik untuk mendapatkan "ukuran" dari sebuah iterator adalah dengan menghitung berapa kali Anda telah melalui iterasi, bukan? Dalam hal ini, apakah itu numIters = 0 ; while iterator: numIters +=1?
Mike Williamson
Menarik, jadi ini masalah terputus-putus
Akababa
241

Kode ini seharusnya berfungsi:

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

Meskipun melakukan iterasi melalui setiap item dan menghitungnya, ini adalah cara tercepat untuk melakukannya.

Ini juga berfungsi jika iterator tidak memiliki item:

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

Tentu saja, ini berjalan selamanya untuk input tak terbatas, jadi ingatlah bahwa iterator bisa tak terbatas:

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

Juga, ketahuilah bahwa iterator akan habis dengan melakukan ini, dan upaya lebih lanjut untuk menggunakannya tidak akan melihat elemen . Itu adalah konsekuensi yang tidak dapat dihindari dari desain iterator Python. Jika Anda ingin menyimpan elemen, Anda harus menyimpannya dalam daftar atau semacamnya.

John Howard
sumber
11
Menurut saya seperti ini melakukan persis apa yang OP tidak ingin lakukan: iterasi melalui iterator dan menghitung.
Adam Crossland
37
Ini adalah cara hemat ruang untuk menghitung elemen secara iterable
Kapten Lepton
9
Meskipun ini bukan yang diinginkan OP, mengingat pertanyaannya tidak memiliki jawaban, jawaban ini menghindari pembuatan daftar, dan secara empiris lebih cepat dengan konstanta daripada metode pengurangan yang tercantum di atas.
Phillip Nordwall
5
Tidak dapat membantu: apakah _referensi ke Perl $_? :)
Alois Mahdal
17
@AloisMahdal Tidak. Ini adalah konvensional di Python untuk menggunakan nama _untuk variabel dummy yang nilainya tidak Anda pedulikan.
Taymon
69

Tidak, metode apa pun akan mengharuskan Anda menyelesaikan setiap hasil. Anda dapat melakukan

iter_length = len(list(iterable))

tetapi menjalankannya pada iterator tak terbatas tentu saja tidak akan pernah kembali. Ini juga akan menggunakan iterator dan perlu diatur ulang jika Anda ingin menggunakan isinya.

Memberi tahu kami masalah nyata yang Anda coba selesaikan dapat membantu kami menemukan cara yang lebih baik untuk mencapai tujuan Anda yang sebenarnya.

Sunting: Menggunakan list()akan membaca seluruh iterable ke dalam memori sekaligus, yang mungkin tidak diinginkan. Cara lain adalah melakukannya

sum(1 for _ in iterable)

seperti yang diposting orang lain. Itu akan menghindari menyimpannya dalam memori.

Daenyth
sumber
masalahnya adalah saya membaca file dengan "pysam" yang memiliki jutaan entri. Pysam mengembalikan iterator. Untuk menghitung kuantitas tertentu, saya perlu tahu berapa banyak bacaan yang ada di file, tetapi saya tidak perlu membaca masing-masing ... itulah masalahnya.
6
Saya bukan pengguna pysam, tapi mungkin membaca file "malas". Masuk akal karena Anda tidak ingin memiliki file besar di memori. Jadi jika Anda pasti tahu tidak. catatan sebelum iterasi, satu-satunya cara adalah membuat dua iterator, dan menggunakan yang pertama untuk menghitung elemen dan yang kedua untuk membaca file. BTW. Jangan gunakan len(list(iterable))itu akan memuat semua data ke memori. Anda dapat menggunakan: reduce(lambda x, _: x+1, iterable, 0). Edit: Kode Zonda333 dengan jumlah juga bagus.
Tomasz Wysocki
1
@ user248237: mengapa Anda mengatakan Anda perlu mengetahui berapa banyak entri yang tersedia untuk menghitung kuantitas tertentu? Anda bisa membaca jumlah yang tetap dan mengelola kasing ketika jumlahnya kurang dari jumlah yang tetap (sangat mudah dilakukan menggunakan iterslice). Apakah ada alasan lain Anda harus membaca semua entri?
kriss
1
@Tomasz Perhatikan bahwa pengurangan tidak digunakan lagi, dan akan hilang dengan Python 3 dan yang lebih baru.
Wilduck
7
@ Wilduck: Ini belum hilang, baru saja dipindahkan kefunctools.reduce
Daenyth
34

Anda tidak bisa (kecuali tipe iterator tertentu mengimplementasikan beberapa metode spesifik yang memungkinkannya).

Umumnya, Anda dapat menghitung item iterator hanya dengan menggunakan iterator. Salah satu cara yang mungkin paling efisien:

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)

(Untuk Python 3.x ganti itertools.izipdengan zip).

zuo
sumber
3
+1: dibandingkan dengan waktu sum(1 for _ in iterator), ini hampir dua kali lebih cepat.
augustomen
1
Lebih akurat untuk mengatakan bahwa ia menghabiskan iterable dengan membaca setiap item ke dalam memori dan segera membuangnya.
Rockallite
Penting untuk dicatat (yang saya abaikan) bahwa urutan argumen zippenting : jika Anda lulus zip(counter, iterable), Anda benar-benar akan mendapatkan 1 lebih banyak daripada jumlah yang dapat diulang!
Kye W Shi
jawaban yang sangat bagus. akan memberikan karunia di atasnya.
Reut Sharabani
18

Agak. Anda dapat memeriksa __length_hint__metodenya, tetapi diperingatkan bahwa (setidaknya hingga Python 3.4, seperti yang ditunjukkan oleh gsnedders dengan sangat membantu) ini adalah detail implementasi yang tidak terdokumentasi ( mengikuti pesan di utas ), yang dapat menghilang dengan baik atau memanggil setan hidung sebagai gantinya.

Jika tidak, tidak. Iterator hanyalah sebuah objek yang hanya mengekspos next()metode tersebut. Anda dapat memanggilnya sebanyak yang diperlukan dan mereka mungkin atau mungkin tidak akan menaikkannya StopIteration. Untungnya, perilaku ini sering kali transparan bagi pembuat kode. :)

badp
sumber
5
Ini tidak lagi menjadi masalah, pada PEP 424 dan Python 3.4. __length_hint__sekarang didokumentasikan, tetapi ini adalah petunjuk dan tidak ada jaminan keakuratan.
gsnedders
13

Nah, bagi yang ingin mengetahui ringkasan pembahasan itu. Skor teratas akhir untuk menghitung ekspresi generator berdurasi 50 juta menggunakan:

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

diurutkan berdasarkan performa eksekusi (termasuk konsumsi memori), akan membuat Anda terkejut:

``

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, detik', 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()

('jumlah, detik', 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, detik', 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)

('kurangi, detik', 13.436614598002052) ``

Jadi, len(list(gen))adalah yang paling sering dan lebih sedikit memori yang dikonsumsi

Alex-Bogdanov
sumber
Bagaimana Anda mengukur konsumsi memori?
normanius
2
Bisakah Anda menjelaskan mengapa len(list(gen))harus mengonsumsi lebih sedikit memori daripada pendekatan berdasarkan pengurangan? Yang pertama membuat yang baru listyang melibatkan alokasi memori sedangkan yang terakhir seharusnya tidak. Jadi saya berharap yang terakhir lebih hemat memori. Selain itu, konsumsi memori akan bergantung pada jenis elemen.
normanius
FYI: Saya dapat mereproduksi untuk python 3.6.8 (pada MacBookPro) bahwa metode 1 mengungguli metode lain dalam hal runtime (saya melewatkan metode 4).
normanius
len(tuple(iterable))bahkan bisa lebih efisien: artikel oleh Nelson Minar
VMAtm
12

Saya suka paket kardinalitas untuk ini, ini sangat ringan dan mencoba menggunakan implementasi tercepat yang tersedia tergantung pada iterable.

Pemakaian:

>>> 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()Implementasi sebenarnya adalah sebagai berikut:

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
sumber
Saya berasumsi Anda masih bisa mengulang iterator jika Anda menggunakan fungsi itu, ya?
jcollum
9

Sebuah iterator hanyalah sebuah objek yang memiliki penunjuk ke objek berikutnya untuk dibaca oleh semacam buffer atau aliran, ini seperti LinkedList di mana Anda tidak tahu berapa banyak hal yang Anda miliki sampai Anda mengulanginya. Iterator dimaksudkan agar efisien karena yang mereka lakukan hanyalah memberi tahu Anda apa yang berikutnya dengan referensi alih-alih menggunakan pengindeksan (tetapi seperti yang Anda lihat, Anda kehilangan kemampuan untuk melihat berapa banyak entri berikutnya).

Yesus Ramos
sumber
2
Sebuah iterator tidak seperti daftar tertaut. Objek yang dikembalikan dari iterator tidak mengarah ke objek berikutnya, dan objek ini tidak (harus) disimpan dalam memori. Sebaliknya, itu dapat menghasilkan objek satu demi satu, berdasarkan logika batin apa pun (yang bisa, tetapi tidak harus, berdasarkan daftar yang disimpan).
Tom
1
@ Tom Saya menggunakan LinkedList sebagai contoh sebagian besar karena Anda tidak tahu berapa banyak yang Anda miliki karena Anda hanya tahu apa yang selanjutnya dalam arti (jika ada sesuatu). Saya minta maaf jika kata-kata saya tampak sedikit salah atau jika saya menyiratkan bahwa kata-kata itu sama.
Jesus Ramos
8

Mengenai pertanyaan awal Anda, jawabannya tetaplah tidak ada cara secara umum untuk mengetahui panjang sebuah iterator dengan Python.

Mengingat pertanyaan Anda dimotivasi oleh aplikasi pysam library, saya dapat memberikan jawaban yang lebih spesifik: Saya seorang kontributor PySAM dan jawaban definitifnya adalah bahwa file SAM / BAM tidak memberikan jumlah persis pembacaan yang selaras. Informasi ini juga tidak tersedia dengan mudah dari file indeks BAM. Yang terbaik yang bisa dilakukan adalah memperkirakan perkiraan jumlah perataan dengan menggunakan lokasi penunjuk file setelah membaca sejumlah perataan dan mengekstrapolasi berdasarkan ukuran total file. Ini cukup untuk menerapkan bilah kemajuan, tetapi bukan metode penghitungan penyelarasan dalam waktu yang konstan.

Kevin Jacobs
sumber
6

Tolok ukur cepat:

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

Hasil:

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

Yaitu count_iter_items sederhana adalah cara untuk pergi.

Menyesuaikan ini untuk 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
sumber
Catatan: tes ini didasarkan pada python2
normanius
3

Ada dua cara untuk mendapatkan panjang "sesuatu" di komputer.

Cara pertama adalah menyimpan hitungan - ini memerlukan apa pun yang menyentuh file / data untuk memodifikasinya (atau kelas yang hanya mengekspos antarmuka - tetapi intinya sama).

Cara lain adalah mengulanginya dan menghitung seberapa besar ukurannya.

Wayne Werner
sumber
0

Merupakan praktik umum untuk meletakkan jenis informasi ini di header file, dan agar pysam memberi Anda akses ke ini. Saya tidak tahu formatnya, tetapi apakah Anda sudah memeriksa API?

Seperti yang dikatakan orang lain, Anda tidak dapat mengetahui panjangnya dari iterator.

tom10
sumber
0

Ini bertentangan dengan definisi iterator, yang merupakan penunjuk ke objek, ditambah informasi tentang cara menuju ke objek berikutnya.

Sebuah iterator tidak tahu berapa kali lagi iterator dapat melakukan iterasi hingga dihentikan. Ini bisa jadi tidak terbatas, jadi ketidakterbatasan mungkin jawaban Anda.

FCAlive
sumber
Tidak melanggar apa pun, dan tidak ada salahnya menerapkan pengetahuan sebelumnya saat menggunakan iterator. Ada miliaran iterator di sekitar, di mana Anda tahu, bahwa jumlah elemennya terbatas. Pikirkan tentang memfilter daftar, Anda dapat dengan mudah memberikan panjang maksimum, Anda tidak terlalu mengetahui berapa banyak elemen yang benar-benar sesuai dengan kondisi filter Anda. Ingin mengetahui jumlah elemen yang cocok adalah aplikasi yang valid, tidak melanggar ide misterius iterator.
Michael
0

Meskipun secara umum tidak mungkin untuk melakukan apa yang diminta, sering kali berguna untuk menghitung berapa banyak item yang diiterasi setelah diiterasi. Untuk itu, Anda bisa menggunakan jaraco.itertools.Counter atau sejenisnya. Berikut adalah contoh menggunakan Python 3 dan rwt untuk memuat paket.

$ 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
sumber
-1
def count_iter(iter):
    sum = 0
    for _ in iter: sum += 1
    return sum
hasen
sumber
-1

Agaknya, Anda ingin menghitung jumlah item tanpa melakukan iterasi, sehingga iterator tidak habis, dan Anda menggunakannya lagi nanti. Ini dimungkinkan dengan copyataudeepcopy

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

Outputnya adalah " Finding the length did not exhaust the iterator!"

Secara opsional (dan tanpa pertimbangan), Anda bisa membayangi lenfungsi bawaan sebagai berikut:

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
Anemon Tusuk Gigi
sumber
1
Rentang bukanlah iterator. Ada beberapa jenis iterator yang dapat disalin, tetapi yang lain akan menyebabkan kode ini gagal dengan TypeError (misalnya generator), dan iterasi melalui iterator yang disalin dapat menyebabkan efek samping terjadi dua kali, atau menyebabkan kerusakan sewenang-wenang dalam kode yang, katakanlah, mengembalikan mapiterator yang mengharapkan panggilan fungsi yang dihasilkan hanya terjadi sekali.
user2357112 mendukung Monica