Membangun Basic Python Iterator

569

Bagaimana cara membuat fungsi berulang (atau objek iterator) dengan python?

akdom
sumber

Jawaban:

650

Objek Iterator di python sesuai dengan protokol iterator, yang pada dasarnya berarti mereka menyediakan dua metode: __iter__() dan __next__().

  • The __iter__mengembalikan objek iterator dan secara implisit disebut di awal loop.

  • The __next__()Metode mengembalikan nilai berikutnya dan secara implisit disebut pada setiap kenaikan lingkaran. Metode ini memunculkan pengecualian StopIteration ketika tidak ada lagi nilai untuk kembali, yang secara implisit ditangkap oleh pengulangan konstruksi untuk menghentikan iterasi.

Ini contoh sederhana penghitung:

class Counter:
    def __init__(self, low, high):
        self.current = low - 1
        self.high = high

    def __iter__(self):
        return self

    def __next__(self): # Python 2: def next(self)
        self.current += 1
        if self.current < self.high:
            return self.current
        raise StopIteration


for c in Counter(3, 9):
    print(c)

Ini akan mencetak:

3
4
5
6
7
8

Ini lebih mudah untuk ditulis menggunakan generator, seperti yang tercakup dalam jawaban sebelumnya:

def counter(low, high):
    current = low
    while current < high:
        yield current
        current += 1

for c in counter(3, 9):
    print(c)

Output yang dicetak akan sama. Di bawah tenda, objek generator mendukung protokol iterator dan melakukan sesuatu yang kira-kira mirip dengan Counter kelas.

Artikel David Mertz, Iterators and Simple Generator , adalah pengantar yang cukup bagus.

ars
sumber
4
Ini sebagian besar merupakan jawaban yang baik, tetapi fakta bahwa ia mengembalikan diri sedikit kurang optimal. Misalnya, jika Anda menggunakan objek penghitung yang sama dalam loop dua bersarang untuk Anda, Anda mungkin tidak akan mendapatkan perilaku yang Anda maksudkan.
Casey Rodarmor
22
Tidak, iterator HARUS mengembalikan diri. Iterables mengembalikan iterators, tetapi iterables tidak boleh diimplementasikan __next__. counteradalah iterator, tapi itu bukan urutan. Itu tidak menyimpan nilainya. Anda tidak boleh menggunakan penghitung dalam loop berulang-kali, misalnya.
leewz
4
Dalam contoh Counter, self.current harus ditugaskan __iter__(selain ke dalam __init__). Jika tidak, objek hanya dapat diulang satu kali. Misalnya, jika Anda berkata ctr = Counters(3, 8), maka Anda tidak dapat menggunakan for c in ctrlebih dari sekali.
Curt
7
@Curt: Sama sekali tidak. Counteradalah iterator, dan iterator hanya seharusnya diulang satu kali. Jika Anda me-reset self.currentdi __iter__, maka loop bersarang di atas Counterakan benar-benar rusak, dan segala macam perilaku diasumsikan dari iterator (yang menyebut itermereka adalah idempoten) dilanggar. Jika Anda ingin dapat mengulangi ctrlebih dari satu kali, itu harus non-iterator iterable, di mana ia mengembalikan iterator baru setiap kali __iter__dipanggil. Mencoba untuk mencampur dan mencocokkan (sebuah iterator yang secara implisit direset ketika __iter__dipanggil) melanggar protokol.
ShadowRanger
2
Misalnya, jika Countermenjadi non-iterator iterable, Anda akan menghapus definisi __next__/ nextseluruhnya, dan mungkin mendefinisikan ulang __iter__sebagai fungsi generator dengan bentuk yang sama seperti generator yang dijelaskan pada akhir jawaban ini (kecuali alih-alih batas) datang dari argumen ke __iter__, mereka akan argumen untuk __init__disimpan selfdan diakses dari selfdalam __iter__).
ShadowRanger
427

Ada empat cara untuk membangun fungsi berulang:

Contoh:

# generator
def uc_gen(text):
    for char in text.upper():
        yield char

# generator expression
def uc_genexp(text):
    return (char for char in text.upper())

# iterator protocol
class uc_iter():
    def __init__(self, text):
        self.text = text.upper()
        self.index = 0
    def __iter__(self):
        return self
    def __next__(self):
        try:
            result = self.text[self.index]
        except IndexError:
            raise StopIteration
        self.index += 1
        return result

# getitem method
class uc_getitem():
    def __init__(self, text):
        self.text = text.upper()
    def __getitem__(self, index):
        return self.text[index]

Untuk melihat keempat metode dalam aksi:

for iterator in uc_gen, uc_genexp, uc_iter, uc_getitem:
    for ch in iterator('abcde'):
        print(ch, end=' ')
    print()

Yang mengakibatkan:

A B C D E
A B C D E
A B C D E
A B C D E

Catatan :

Dua tipe generator ( uc_gendan uc_genexp) tidak bisa reversed(); iterator polos ( uc_iter) akan membutuhkan __reversed__metode ajaib (yang, menurut dokumen , harus mengembalikan iterator baru, tetapi mengembalikan selfkarya (setidaknya dalam CPython)); dan getitem iteratable ( uc_getitem) harus memiliki __len__metode ajaib:

    # for uc_iter we add __reversed__ and update __next__
    def __reversed__(self):
        self.index = -1
        return self
    def __next__(self):
        try:
            result = self.text[self.index]
        except IndexError:
            raise StopIteration
        self.index += -1 if self.index < 0 else +1
        return result

    # for uc_getitem
    def __len__(self)
        return len(self.text)

Untuk menjawab pertanyaan sekunder Kolonel Panic tentang iterator malas yang dievaluasi tanpa batas, berikut adalah contoh-contohnya, menggunakan masing-masing dari empat metode di atas:

# generator
def even_gen():
    result = 0
    while True:
        yield result
        result += 2


# generator expression
def even_genexp():
    return (num for num in even_gen())  # or even_iter or even_getitem
                                        # not much value under these circumstances

# iterator protocol
class even_iter():
    def __init__(self):
        self.value = 0
    def __iter__(self):
        return self
    def __next__(self):
        next_value = self.value
        self.value += 2
        return next_value

# getitem method
class even_getitem():
    def __getitem__(self, index):
        return index * 2

import random
for iterator in even_gen, even_genexp, even_iter, even_getitem:
    limit = random.randint(15, 30)
    count = 0
    for even in iterator():
        print even,
        count += 1
        if count >= limit:
            break
    print

Yang menghasilkan (setidaknya untuk menjalankan sampel saya):

0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50 52 54
0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38
0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30
0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32

Bagaimana memilih yang mana yang akan digunakan? Ini sebagian besar masalah selera. Dua metode yang paling sering saya lihat adalah generator dan protokol iterator, serta hibrida ( __iter__mengembalikan generator).

Ekspresi generator berguna untuk mengganti pemahaman daftar (mereka malas sehingga dapat menghemat sumber daya).

Jika seseorang membutuhkan kompatibilitas dengan versi Python 2.x sebelumnya gunakan __getitem__.

Ethan Furman
sumber
4
Saya suka ringkasan ini karena sudah lengkap. Ketiga cara (hasil, ekspresi generator dan iterator) pada dasarnya sama, meskipun beberapa lebih nyaman daripada yang lain. Operator hasil menangkap "kelanjutan" yang berisi keadaan (misalnya indeks yang kita lakukan). Informasi disimpan dalam "penutupan" kelanjutan. Cara iterator menyimpan informasi yang sama di dalam bidang iterator, yang pada dasarnya sama dengan penutupan. The getItem metode adalah sedikit berbeda karena indeks ke dalam isi dan tidak berulang di alam.
Ian
2
@metaperl: Sebenarnya, benar. Dalam keempat kasus di atas, Anda dapat menggunakan kode yang sama untuk beralih.
Ethan Furman
1
@Asterisk: Tidak, turunan uc_iterharus kedaluwarsa saat selesai (jika tidak, akan tanpa batas); jika Anda ingin melakukannya lagi, Anda harus mendapatkan iterator baru dengan menelepon uc_iter()lagi.
Ethan Furman
2
Anda dapat mengatur self.index = 0di __iter__sehingga Anda dapat iterate berkali-kali. Kalau tidak, Anda tidak bisa.
John Strood
1
Jika Anda dapat meluangkan waktu, saya akan sangat menghargai penjelasan mengapa Anda memilih salah satu metode daripada yang lain.
aaaaaa
103

Pertama-tama modul itertools sangat berguna untuk semua jenis kasus di mana iterator akan berguna, tetapi di sini adalah semua yang Anda butuhkan untuk membuat iterator dengan python:

menghasilkan

Bukankah itu keren? Yield dapat digunakan untuk menggantikan normal kembali dalam suatu fungsi. Ini mengembalikan objek sama saja, tetapi alih-alih menghancurkan negara dan keluar, ia menyimpan keadaan ketika Anda ingin menjalankan iterasi berikutnya. Berikut ini adalah contoh tindakan yang diambil langsung dari daftar fungsi itertools :

def count(n=0):
    while True:
        yield n
        n += 1

Seperti yang dinyatakan dalam deskripsi fungsi (ini adalah fungsi count () dari modul itertools ...), ia menghasilkan iterator yang mengembalikan bilangan bulat berurutan dimulai dengan n.

Ekspresi generator adalah kaleng cacing lainnya (cacing luar biasa!). Mereka dapat digunakan sebagai pengganti Pemahaman Daftar untuk menghemat memori (pemahaman daftar membuat daftar dalam memori yang dihancurkan setelah digunakan jika tidak ditugaskan ke variabel, tetapi ekspresi generator dapat membuat Obyek Generator ... yang merupakan cara yang bagus untuk mengatakan Iterator). Berikut adalah contoh definisi ekspresi generator:

gen = (n for n in xrange(0,11))

Ini sangat mirip dengan definisi iterator kami di atas kecuali rentang penuh telah ditentukan antara 0 dan 10.

Saya baru saja menemukan xrange () (kaget saya belum pernah melihatnya sebelumnya ...) dan menambahkannya ke contoh di atas. xrange () adalah versi rentang iterable () yang memiliki keuntungan tidak membuat ulang daftar. Akan sangat berguna jika Anda memiliki kumpulan data raksasa untuk diulangi dan hanya memiliki begitu banyak memori untuk melakukannya.

akdom
sumber
20
pada python 3.0 tidak ada lagi xrange () dan rentang baru () berperilaku seperti xrange lama ()
6
Anda tetap harus menggunakan xrange di 2._, karena 2to3 menerjemahkannya secara otomatis.
Phob
100

Aku melihat beberapa dari Anda lakukan return selfdi __iter__. Saya hanya ingin mencatat bahwa __iter__itu sendiri bisa menjadi generator (sehingga menghilangkan kebutuhan __next__dan meningkatkan StopIterationpengecualian)

class range:
  def __init__(self,a,b):
    self.a = a
    self.b = b
  def __iter__(self):
    i = self.a
    while i < self.b:
      yield i
      i+=1

Tentu saja di sini orang mungkin juga secara langsung membuat generator, tetapi untuk kelas yang lebih kompleks dapat bermanfaat.

Manux
sumber
5
Bagus! Begitu membosankan menulis hanya return selfdi __iter__. Ketika saya akan mencoba menggunakan yielddi dalamnya saya menemukan kode Anda melakukan persis apa yang ingin saya coba.
Ray
3
Tetapi dalam kasus ini, bagaimana orang akan menerapkannya next()? return iter(self).next()?
Lenna
4
@Lenna, ini sudah "diimplementasikan" karena iter (self) mengembalikan iterator, bukan instance range.
Manux
3
Ini cara termudah untuk melakukannya, dan tidak melibatkan harus melacak eg self.currentatau counter lainnya. Ini harus menjadi jawaban terpilih!
astrofrog
4
Agar jelas, pendekatan ini membuat kelas Anda dapat diubah , tetapi bukan iterator . Anda mendapatkan iterator baru setiap kali Anda memanggil iterinstance kelas, tetapi mereka sendiri bukan instance kelas.
ShadowRanger
13

Pertanyaan ini adalah tentang objek yang dapat diubah, bukan tentang iterator. Dalam Python, sekuens juga dapat diubah sehingga salah satu cara untuk membuat kelas iterable adalah membuatnya berperilaku seperti sekuens, yaitu memberikannya __getitem__dan __len__metode. Saya telah menguji ini pada Python 2 dan 3.

class CustomRange:

    def __init__(self, low, high):
        self.low = low
        self.high = high

    def __getitem__(self, item):
        if item >= len(self):
            raise IndexError("CustomRange index out of range")
        return self.low + item

    def __len__(self):
        return self.high - self.low


cr = CustomRange(0, 10)
for i in cr:
    print(i)
aq2
sumber
1
Tidak harus memiliki __len__()metode. __getitem__sendirian dengan perilaku yang diharapkan sudah cukup.
BlackJack
5

Semua jawaban pada halaman ini sangat bagus untuk objek yang kompleks. Tetapi bagi mereka yang mengandung builtin jenis iterable sebagai atribut, seperti str, list, setatau dict, atau pelaksanaan collections.Iterable, Anda dapat menghilangkan hal-hal tertentu di kelas Anda.

class Test(object):
    def __init__(self, string):
        self.string = string

    def __iter__(self):
        # since your string is already iterable
        return (ch for ch in self.string)
        # or simply
        return self.string.__iter__()
        # also
        return iter(self.string)

Dapat digunakan seperti:

for x in Test("abcde"):
    print(x)

# prints
# a
# b
# c
# d
# e
John Strood
sumber
1
Seperti yang Anda katakan, string sudah iterable jadi mengapa generator ekspresi ekstra dalam antara bukan hanya meminta string untuk iterator (yang ekspresi pembangkit melakukan internal): return iter(self.string).
BlackJack
@ BlackJack Kamu memang benar. Saya tidak tahu apa yang meyakinkan saya untuk menulis seperti itu. Mungkin saya mencoba untuk menghindari kebingungan dalam jawaban yang mencoba menjelaskan kerja sintaks iterator dalam hal sintaks iterator yang lebih banyak.
John Strood
3

Ini adalah fungsi yang dapat diubah tanpa yield. Itu menggunakan iterfungsi dan penutupan yang membuat keadaan itu bisa berubah ( list) dalam lingkup melampirkan untuk python 2.

def count(low, high):
    counter = [0]
    def tmp():
        val = low + counter[0]
        if val < high:
            counter[0] += 1
            return val
        return None
    return iter(tmp, None)

Untuk Python 3, status penutupan disimpan dalam kekekalan dalam lingkup melampirkan dan nonlocaldigunakan dalam lingkup lokal untuk memperbarui variabel status.

def count(low, high):
    counter = 0
    def tmp():
        nonlocal counter
        val = low + counter
        if val < high:
            counter += 1
            return val
        return None
    return iter(tmp, None)  

Uji;

for i in count(1,10):
    print(i)
1
2
3
4
5
6
7
8
9
Nizam Mohamed
sumber
Saya selalu menghargai penggunaan dua-arg yang cerdik iter, tetapi hanya untuk memperjelas: Ini lebih kompleks dan kurang efisien daripada hanya menggunakan yieldfungsi generator berbasis; Python memiliki banyak dukungan juru bahasa untuk yieldfungsi-fungsi generator berbasis yang tidak dapat Anda manfaatkan di sini, membuat kode ini jauh lebih lambat. Tetap terpilih.
ShadowRanger
2

Jika Anda mencari sesuatu yang pendek dan sederhana, mungkin itu sudah cukup untuk Anda:

class A(object):
    def __init__(self, l):
        self.data = l

    def __iter__(self):
        return iter(self.data)

contoh penggunaan:

In [3]: a = A([2,3,4])

In [4]: [i for i in a]
Out[4]: [2, 3, 4]
Daniil Mashkin
sumber
-1

Terinspirasi oleh jawaban Matt Gregory di sini adalah iterator yang sedikit lebih rumit yang akan mengembalikan a, b, ..., z, aa, ab, ..., zz, aaa, aab, ..., zzy, zzz

    class AlphaCounter:
    def __init__(self, low, high):
        self.current = low
        self.high = high

    def __iter__(self):
        return self

    def __next__(self): # Python 3: def __next__(self)
        alpha = ' abcdefghijklmnopqrstuvwxyz'
        n_current = sum([(alpha.find(self.current[x])* 26**(len(self.current)-x-1)) for x in range(len(self.current))])
        n_high = sum([(alpha.find(self.high[x])* 26**(len(self.high)-x-1)) for x in range(len(self.high))])
        if n_current > n_high:
            raise StopIteration
        else:
            increment = True
            ret = ''
            for x in self.current[::-1]:
                if 'z' == x:
                    if increment:
                        ret += 'a'
                    else:
                        ret += 'z'
                else:
                    if increment:
                        ret += alpha[alpha.find(x)+1]
                        increment = False
                    else:
                        ret += x
            if increment:
                ret += 'a'
            tmp = self.current
            self.current = ret[::-1]
            return tmp

for c in AlphaCounter('a', 'zzz'):
    print(c)
Ace
sumber