Temel Python Yineleyicisi Oluşturma


569

Python'da nasıl yinelemeli bir işlev (veya yineleyici nesnesi) oluşturulur?

Yanıtlar:


650

Python'daki yineleme nesneleri yineleyici protokolüne uygundur, bu da temel olarak iki yöntem sağladıkları anlamına gelir: __iter__() ve __next__().

  • __iter__Yineleyici nesnesi döndüren ve örtük döngüler başında denir.

  • __next__()Yöntem aşağıdaki değerini verir ve dolaylı her döngü artan çağrılır. Bu yöntem, döndürülecek başka değer olmadığında bir StopIteration istisnası oluşturur; bu, yinelemeleri durdurmak için yapıları döngülerek örtülü olarak yakalanır.

Basit bir sayaç örneği:

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)

Bu yazdırılacak:

3
4
5
6
7
8

Önceki bir yanıtta anlatıldığı gibi, bir jeneratör kullanarak yazmak daha kolaydır:

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

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

Yazdırılan çıktı aynı olacaktır. Kaputun altında, jeneratör nesnesi yineleyici protokolünü destekler ve Counter sayacına kabaca benzer bir şey yapar.

David Mertz'in makalesi, Yineleyiciler ve Basit Jeneratörler , oldukça iyi bir tanıtım.


4
Bu çoğunlukla iyi bir cevaptır, ancak kendi kendine geri dönmesi biraz alt optimaldir. Örneğin, aynı sayaç nesnesini iki kez iç içe yerleştirilmiş bir döngüde kullandıysanız, muhtemelen demek istediğiniz davranışı alamazsınız.
Casey Rodarmor

22
Hayır, yineleyiciler kendilerini geri döndürmelidir. Tekrarlanabilirler yineleyicileri döndürür, ancak yinelenebilirler uygulanmamalıdır __next__. counterbir yineleyicidir, ancak bir dizi değildir. Değerlerini saklamaz. Örneğin, sayacı iki kez iç içe yerleştirilmiş bir döngü içinde kullanmamalısınız.
leewz

4
Sayaç örneğinde, self.current __iter__( in'e ek olarak __init__) atanmalıdır . Aksi takdirde, nesne yalnızca bir kez yinelenebilir. Örneğin, eğer derseniz ctr = Counters(3, 8), for c in ctrbirden fazla kullanamazsınız .
Curt

7
@Curt: Kesinlikle hayır. Counteryineleyicidir ve yineleyicilerin yalnızca bir kez yinelenmesi gerekir. Sıfırlamak Eğer self.currentiçinde __iter__, daha sonra üzerinde iç içe bir döngü Countertamamen kırılmış olur ve Yineleyicilerin kabul davranışları (sesleniyor her türlü iterüzerlerinde İdempotent olan) ihlal ediliyor. Bir ctrkereden fazla yineleme yapabilmek istiyorsanız, yinelenemeyen yinelenebilir olması gerekir, burada her __iter__çağrıldığında yepyeni bir yineleyici döndürür . Karıştırmaya ve eşleştirmeye çalışmak ( __iter__çağrıldığında dolaylı olarak sıfırlanan bir yineleyici ) protokolleri ihlal eder.
ShadowRanger

2
Örneğin, Counteryineleyici olmayan bir yinelemeli olsaydı, __next__/ nexttamamen tanımını kaldıracak ve muhtemelen __iter__bu cevabın sonunda açıklanan sınırla ( jeneratörün dışında) oluşturucu ile aynı formdaki bir jeneratör fonksiyonu olarak yeniden tanımlayacaksınız. argümanlar gelen etmek __iter__için, onlar argümanlar olurdu __init__kaydedilmiş selfgelen ve erişilen selfiçinde __iter__).
ShadowRanger

427

Yinelemeli bir işlev oluşturmanın dört yolu vardır:

Örnekler:

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

Dört yöntemin de çalıştığını görmek için:

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

Sonuç:

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

Not :

İki jeneratör türü ( uc_genve uc_genexp) olamaz reversed(); düz yineleyici ( uc_iter) __reversed__sihirli yönteme ( dokümanlara göre yeni bir yineleyici döndürmeli, ancak geri dönmelidir)self işler (en azından CPython'da)); ve getitem iteratable ( uc_getitem) __len__yönteminin sihirli yöntemi olması gerekir :

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

Albay Panic'in sonsuz tembel olarak değerlendirilmiş bir yineleyici hakkındaki ikincil sorusunu cevaplamak için, yukarıdaki dört yöntemin her birini kullanan örnekler:

# 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

Hangi sonuçlar (en azından örnek çalışmam için):

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

Hangisini kullanacağımı nasıl seçebilirim? Bu çoğunlukla bir tat meselesidir. En sık gördüğüm iki yöntem jeneratörler ve yineleyici protokolünün yanı sıra bir melezdir ( __iter__bir jeneratör döndürür).

Jeneratör ifadeleri, liste kavrayışlarını değiştirmek için yararlıdır (tembeldir ve böylece kaynaklardan tasarruf edebilirler).

Daha önceki Python 2.x sürümleriyle uyumluluk gerekiyorsa kullanın __getitem__.


4
Bu özeti seviyorum çünkü tamamlandı. Bu üç yol (verim, jeneratör ifadesi ve yineleyici) esasen aynıdır, ancak bazıları diğerlerinden daha uygundur. Verim operatörü, durumu içeren "devamı" (örneğin, bulunduğumuz endeksi) yakalar. Bilgiler, devamın “kapatılması” na kaydedilir. Yineleyici yolu, yineleyicinin alanlarına aynı bilgileri kaydeder, bu da aslında bir kapatma ile aynı şeydir. GetItem içeriklerine bu endeksler ve yinelemeli doğada olmadığı için yöntem biraz farklıdır.
Ian

2
@metaperl: Aslında öyle. Yukarıdaki durumların dördünde de yinelemek için aynı kodu kullanabilirsiniz.
Ethan Furman

1
@Asterisk: Hayır, bittiğinde bir örneğinin uc_itersüresi dolmalıdır (aksi takdirde sonsuz olarak); tekrar yapmak isterseniz, tekrar arayarak yeni bir yineleyici almanız gerekir uc_iter().
Ethan Furman

2
Ayarlayabilirsiniz self.index = 0içinde __iter__size defalarca yineleme böylece. Aksi halde yapamazsınız.
John Strood

1
Eğer zaman ayırabilseydiniz, neden diğer yöntemlerden birini seçeceğinize dair bir açıklama takdir ediyorum.
aaaaaa

103

Her şeyden önce itertools modülü , bir yineleyicinin yararlı olacağı her türlü durum için inanılmaz derecede yararlıdır, ancak işte python'da bir yineleyici oluşturmanız gerekir:

Yol ver

Güzel değil mi? Verim, bir işlevdeki normal bir dönüşün yerine kullanılabilir . Nesneyi aynı şekilde döndürür, ancak durumu yok etmek ve çıkmak yerine, bir sonraki yinelemeyi yürütmek istediğinizde durumdan tasarruf sağlar. Doğrudan itertools işlev listesinden alınan bir eylem örneği :

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

İşlev açıklamasında belirtildiği gibi ( itertools modülünden count () işlevidir ...), n ile başlayan ardışık tamsayıları döndüren bir yineleyici üretir.

Jeneratör ifadeleri bir başka solucan kutusu (müthiş solucanlar!). Hafızadan tasarruf etmek için Liste Kavraması yerine kullanılabilirler (liste kavrayışları, bir değişkene atanmazsa kullanımdan sonra yok edilen bellekte bir liste oluşturur, ancak jeneratör ifadeleri, bir süslü yol olan bir Jeneratör Nesnesi oluşturabilir ... Yineleyici diyor). İşte bir jeneratör ifadesi tanımına bir örnek:

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

Bu, tam aralığın 0 ile 10 arasında önceden belirlenmiş olması dışında, yukarıdaki yineleyici tanımımıza çok benzer.

Ben sadece xrange () buldum ( daha önce görmemiştim şaşırttı ...) ve yukarıdaki örneğe ekledi. xrange () , listenin önceden oluşturulmaması avantajına sahip range () öğesinin yinelenebilir bir sürümüdür . Eğer yinelemek için dev bir veri topluluğunuz varsa ve bunu yapmak için çok fazla belleğiniz olsaydı çok yararlı olurdu.


20
python 3.0'dan itibaren artık bir xrange () yoktur ve yeni aralık () eski xrange () gibi davranır

6
2to3 otomatik olarak çevireceğinden, xrange'i 2._ içinde kullanmaya devam etmelisiniz.
Phob

100

Bazılarınızın yaptığını görüyorum return selfiçinde __iter__. Sadece __iter__kendisinin bir jeneratör olabileceğini belirtmek istedim (böylece ihtiyacı ortadan kaldırmak __next__ve StopIterationistisnaları arttırmak )

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

Tabii ki burada kişi doğrudan bir jeneratör de yapabilir, ancak daha karmaşık sınıflar için yararlı olabilir.


5
Harika! O yüzden sadece yazma sıkıcı return selfiçinde __iter__. Ben yieldbunu kullanarak denemek için gidiyordu ben tam olarak ne denemek istiyorum yapıyor kodunuzu buldum.
Ray

3
Fakat bu durumda nasıl uygulanır next()? return iter(self).next()?
Lenna

4
@Lenna, zaten "uygulandı" çünkü iter (self) bir range örneği değil yineleyici döndürür.
Manux

3
Bunu yapmanın en kolay yolu ve örneğin self.currentveya başka bir sayacı takip etmek zorunda değildir . Bu en çok oy alan cevap olmalı!
Mart'ta astrofrog

4
Açık olmak gerekirse, bu yaklaşım sınıfınızı tekrarlanabilir kılar , ancak bir yineleyici değildir . Sınıfın örneklerini her çağırdığınızda yeni yineleyiciler alırsınız iter, ancak bunlar sınıfın örnekleri değildir.
ShadowRanger

13

Bu soru yineleyicilerle değil, yinelenebilir nesnelerle ilgilidir. Python'da, sekanslar da tekrarlanabilir bir sınıftır, bu nedenle tekrarlanabilir bir sınıf oluşturmanın bir yolu, bir sekans gibi davranmasını sağlamak, yani onu __getitem__ve __len__yöntemlerini vermektir . Bunu Python 2 ve 3'te test ettim.

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)

1
Bunun bir __len__()yöntemi olması gerekmez . __getitem__yalnız beklenen davranış ile yeterlidir.
BlackJack

5

Bu sayfadaki tüm cevaplar karmaşık bir nesne için gerçekten harika. Fakat nitelikleri olarak iterable türleri yerleşik içerenler için, gibi str, list, setveya dict, ya herhangi uygulanması collections.Iterable, kendi sınıfında bazı şeyleri atlayabilirsiniz.

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)

Gibi kullanılabilir:

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

# prints
# a
# b
# c
# d
# e

1
Dediğin gibi, dize iterable yüzden zaten neden yerine Yineleyici için dize (jeneratör ifade içten yapar) sorma arasında ekstra jeneratör ifadesi: return iter(self.string).
BlackJack

@BlackJack Gerçekten haklısın. Beni bu şekilde yazmaya niçin ikna etti bilmiyorum. Belki de yineleyici sözdiziminin çalışmasını daha yineleyici sözdizimi açısından açıklamaya çalışan bir cevapta karışıklıktan kaçınmaya çalışıyordum.
John Strood

3

Bu, yinelenmeyen bir işlevdir yield. Bu iterişlevi ve listpython 2 için kapalı kapsamda bir mutable ( ) durumunu tutan bir kapatma kullanır .

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)

Python 3 için, kapatma durumu çevreleme kapsamında değişmez bir şekilde tutulur nonlocalve durum değişkenini güncellemek için yerel kapsamda kullanılır.

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)  

Ölçek;

for i in count(1,10):
    print(i)
1
2
3
4
5
6
7
8
9

İki argün akıllıca kullanılmasını her zaman takdir ediyorum iter, ama sadece açık olmak gerekirse: Bu, sadece yieldtemel bir jeneratör işlevi kullanmaktan daha karmaşık ve daha az verimlidir ; Python, yieldburada yararlanamayacağınız temel jeneratör işlevleri için tonlarca tercüman desteğine sahiptir ve bu kodu önemli ölçüde yavaşlatır. Yine de yukarı oy kullandı.
ShadowRanger

2

Kısa ve basit bir şey arıyorsanız, belki de sizin için yeterli olacaktır:

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

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

kullanım örneği:

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

In [4]: [i for i in a]
Out[4]: [2, 3, 4]

-1

Matt Gregory'nin cevabından esinlenerek, a, b, ..., z, aa, ab, ..., zz, aaa, aab, ..., zzy, zzz döndürecek biraz daha karmaşık yineleyici

    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)
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.