Python'daki jeneratörleri anlama


218

Şu anda Python yemek kitabını okuyorum ve şu anda jeneratörlere bakıyorum. Kafamı döndürmek için zorlanıyorum.

Java geçmişinden geldiğim için Java eşdeğeri var mı? Kitap 'Yapımcı / Tüketici' hakkında konuşuyordu, ancak duyduğumda diş açmayı düşündüğümü.

Jeneratör nedir ve neden kullanasınız? Hiçbir kitabı alıntılamadan, açıkçası (doğrudan bir kitaptan iyi ve basit bir cevap bulamazsanız). Belki örneklerle, cömert hissediyorsanız!

Yanıtlar:


402

Not: Bu yazı Python 3.x sözdizimini varsayar.

Bir jeneratör basitçe Arayabileceğin hangi bir nesneyi döndüren bir fonksiyondur nexto yükseltir kadar her çağrı için, bazı değer döndürmesi gibi StopIterationtüm değerler üretildiğini fark sinyalizasyon, istisna. Böyle bir nesneye yineleyici denir .

Normal işlevler return, tıpkı Java'da olduğu gibi kullanarak tek bir değer döndürür . Ancak Python'da, denilen bir alternatif var yield. yieldBir fonksiyonda herhangi bir yerde kullanmak onu jeneratör yapar. Bu kodu inceleyin:

>>> def myGen(n):
...     yield n
...     yield n + 1
... 
>>> g = myGen(6)
>>> next(g)
6
>>> next(g)
7
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Gördüğün gibi, myGen(n)n ve veren bir işlevdir n + 1. Her değer çağrısı next, tüm değerler verilinceye kadar tek bir değer verir. fordöngüler nextarka planda çağırır , böylece:

>>> for n in myGen(6):
...     print(n)
... 
6
7

Benzer şekilde , bazı yaygın jeneratör türlerini özlü bir şekilde tanımlamak için bir araç sağlayan jeneratör ifadeleri vardır :

>>> g = (n for n in range(3, 5))
>>> next(g)
3
>>> next(g)
4
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Üretici ifadelerinin liste kavrayışlarına çok benzediğini unutmayın :

>>> lc = [n for n in range(3, 5)]
>>> lc
[3, 4]

Bir jeneratör nesnesinin bir kez oluşturulduğunu ancak kodunun değil bir defada tüm çalıştırın. Sadece nextkodu gerçekten (bir kısmını) yürütmeye yönelik çağrılar . Bir üreteçte kodun yieldyürütülmesi, üzerine bir değer döndürdüğü bir ifadeye ulaşıldığında durur . Bir sonraki çağrı ise next, jeneratörün sondan sonra bırakıldığı durumda yürütmenin devam etmesine neden olur yield. Bu, düzenli işlevlerle temel bir farktır: her zaman "üstte" yürütmeye başlarlar ve bir değer döndürüldükten sonra durumlarını atarlar.

Bu konuda söylenecek daha çok şey var. Örneğin sendbir jeneratöre ( referans ) veri geri vermek mümkündür . Ama bu bir jeneratörün temel kavramını anlayana kadar bakmamanızı önerdiğim bir şey.

Şimdi sorabilirsiniz: neden jeneratörleri kullanıyorsunuz? Birkaç iyi neden var:

  • Bazı kavramlar, jeneratörler kullanılarak çok daha özlü bir şekilde tarif edilebilir.
  • Değerler listesi döndüren bir işlev oluşturmak yerine, değerleri anında üreten bir jeneratör yazılabilir. Bu, hiçbir listenin oluşturulmasına gerek olmadığı anlamına gelir, yani sonuçta elde edilen kod daha fazla bellek verimlidir. Bu şekilde, hafızaya sığmayacak kadar büyük veri akışları bile tanımlanabilir.
  • Jeneratörler sonsuz akışları tanımlamak için doğal bir yol sağlar . Örneğin Fibonacci sayılarını düşünün :

    >>> def fib():
    ...     a, b = 0, 1
    ...     while True:
    ...         yield a
    ...         a, b = b, a + b
    ... 
    >>> import itertools
    >>> list(itertools.islice(fib(), 10))
    [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

    Bu kod, itertools.islicesonsuz bir akıştan sınırlı sayıda öğe almak için kullanılır. itertoolsGelişmiş jeneratörleri kolaylıkla yazabilmeniz için gerekli araçlar olduklarından , modülün fonksiyonlarına iyi bir şekilde bakmanız tavsiye edilir .


   Hakkında Python <= 2.6: yukarıdaki örneklerde next, __next__verilen nesne üzerindeki yöntemi çağıran bir işlevdir . Python <= 2.6'da, kişi o.next()yerine biraz farklı bir teknik kullanır next(o). Python 2.7'de next()çağrı var, .nextbu yüzden 2.7'de aşağıdakileri kullanmanıza gerek yok:

>>> g = (n for n in range(3, 5))
>>> g.next()
3

9
sendBir jeneratöre veri vermenin mümkün olduğunu belirtiyorsunuz . Bunu yaptıktan sonra bir 'coroutine' var. Bahsedilen Tüketici / Üretici gibi kalıpları koroutinlerle uygulamak çok basittir çünkü Locks'ye ihtiyaç duymazlar ve bu nedenle kilitlenemezler. Koroutinleri dişleri dayamadan tanımlamak zor, bu yüzden koroutinlerin diş açmaya çok zarif bir alternatif olduğunu söyleyeceğim.
Jochen Ritzel

Python jeneratörleri temelde makineleri nasıl çalıştıkları açısından Turing mi?
Ateşli Phoenix

48

Bir jeneratör etkin olarak, bitmeden önce (veri) döndüren bir işlevdir, ancak o noktada duraklar ve bu noktada işlevi devam ettirebilirsiniz.

>>> def myGenerator():
...     yield 'These'
...     yield 'words'
...     yield 'come'
...     yield 'one'
...     yield 'at'
...     yield 'a'
...     yield 'time'

>>> myGeneratorInstance = myGenerator()
>>> next(myGeneratorInstance)
These
>>> next(myGeneratorInstance)
words

ve bunun gibi. Üreticilerin (veya bir) yararı, verilerle bir seferde tek parça işlemesi nedeniyle, büyük miktarda veriyle başa çıkabilmenizdir; listelerde aşırı bellek gereksinimleri bir sorun haline gelebilir. Jeneratörler, tıpkı listeler gibi, tekrarlanabilirler, böylece aynı şekilde kullanılabilirler:

>>> for word in myGeneratorInstance:
...     print word
These
words
come
one
at 
a 
time

Jeneratörlerin sonsuzlukla başa çıkmanın başka bir yolunu sağladığını unutmayın, örneğin

>>> from time import gmtime, strftime
>>> def myGen():
...     while True:
...         yield strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())    
>>> myGeneratorInstance = myGen()
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:17:15 +0000
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:18:02 +0000   

Jeneratör sonsuz bir döngüyü içine alır, ancak bu bir sorun değildir, çünkü her soruyu her istediğinizde alırsınız.


30

Her şeyden önce, jeneratör terimi aslında Python'da bir şekilde kötü tanımlanmıştı ve çok fazla karışıklığa neden oldu. Muhtemelen yineleyiciler ve yinelenebilirler anlamına gelirsiniz ( buraya bakın ). Sonra Python'da da jeneratör fonksiyonları (bir jeneratör nesnesini döndüren) vardır, jeneratör nesneleri (yineleyiciler olan) ve jeneratör ifadeleri vardır. (bunlar bir jeneratör nesnesine göre değerlendirilir) vardır.

Jeneratör sözlüğüne göre terimler , resmi terminoloji şu anda bu jeneratör "jeneratör fonksiyonu" kısaltması olduğu görülmektedir. Geçmişte, belgeler terimleri tutarsız bir şekilde tanımlamıştı, ancak neyse ki bu düzeltildi.

Kesin olmak ve daha fazla spesifikasyon olmadan "jeneratör" teriminden kaçınmak iyi bir fikir olabilir.


2
Hmm bence haklısın, en azından Python 2.6'daki birkaç çizginin testine göre. Bir üreteç ifadesi bir üreteci değil bir yineleyici (diğer bir deyişle 'üreteç nesnesi') döndürür.
Craig McQueen

22

Jeneratörler bir yineleyici oluşturmak için kestirme yol olarak düşünülebilir. Java Yineleyicisi gibi davranıyorlar. Misal:

>>> g = (x for x in range(10))
>>> g
<generator object <genexpr> at 0x7fac1c1e6aa0>
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> list(g)   # force iterating the rest
[3, 4, 5, 6, 7, 8, 9]
>>> g.next()  # iterator is at the end; calling next again will throw
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Umarım bu yardımcı olur / aradığınız şeydir.

Güncelleme:

Diğer birçok yanıtın gösterdiği gibi, bir jeneratör oluşturmanın farklı yolları vardır. Yukarıdaki örneğimde olduğu gibi parantez sözdizimini veya verimi kullanabilirsiniz. Bir başka ilginç özellik, jeneratörlerin "sonsuz" olabilmesidir - durmayan yineleyiciler:

>>> def infinite_gen():
...     n = 0
...     while True:
...         yield n
...         n = n + 1
... 
>>> g = infinite_gen()
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> g.next()
3
...

1
Şimdi, Java'nın Streamjeneratörlere çok daha benzer olanları var, ancak görünüşe göre şaşırtıcı bir güçlük çekmeden bir sonraki öğeyi alamıyorsunuz.
Monica'nın Davası

12

Java eşdeğeri yoktur.

İşte biraz örnek:

#! /usr/bin/python
def  mygen(n):
    x = 0
    while x < n:
        x = x + 1
        if x % 3 == 0:
            yield x

for a in mygen(100):
    print a

Jeneratörde 0 ila n arasında çalışan bir döngü vardır ve döngü değişkeni 3'ün katı ise değişkeni verir.

forDöngünün her yinelemesi sırasında jeneratör yürütülür. Jeneratör ilk kez çalıştırıyorsa, başlangıçta başlar, aksi takdirde önceki veriminden devam eder.


2
Son paragraf çok önemlidir: Jeneratör fonksiyonunun durumu, her sth verdiğinde 'dondurulur' ve bir dahaki sefere çağrıldığında tamamen aynı durumda devam eder.
Johannes Charra

Java'da bir "jeneratör ifadesi" ile sözdizimsel eşdeğeri yoktur, ancak jeneratörler - bir kez elde ettiğinizde - aslında sadece bir yineleyicidir (Java yineleyicisi ile aynı temel özellikler).
overthink

@overthink: Üreticilerin Java yineleyicilerinin sahip olamayacağı başka yan etkileri olabilir. Örneğimden print "hello"sonra koyarsam x=x+1, "merhaba" 100 kez yazdırılırken, for döngüsünün gövdesi hala sadece 33 kez çalıştırılır.
Wernsey

@iWerner: Java'da aynı etkinin olabileceğinden eminiz. Eşdeğer Java yineleyicisinde next () uygulamasının yine de 0 ile 99 arasında arama yapması gerekir (mygen (100) örneğinizi kullanarak), böylece isterseniz her seferinde System.out.println () yapabilirsiniz. Sonraki () sadece 33 kez dönecekti. Java'nın eksikliği, okunması (ve yazılması) çok daha kolay olan çok kullanışlı verim sözdizimidir.
overthink

Bu tek satırlı def okumayı ve hatırlamayı sevdim: Jeneratör ilk kez çalıştırıyorsa, başlangıçta başlar, aksi takdirde önceki veriminden devam eder.
Iqra.

8

Jeneratörleri, programlama dilleri ve bilgi işlem konusunda iyi bir altyapıya sahip olanlara yığın çerçeveleri açısından tanımlamayı seviyorum.

Birçok dilde, üstte mevcut yığın "çerçeve" olan bir yığın vardır. Yığın çerçevesi, o işleve iletilen bağımsız değişkenler de dahil olmak üzere işleve yerel değişkenler için ayrılan alanı içerir.

Bir işlevi çağırdığınızda, geçerli yürütme noktası ("program sayacı" veya eşdeğeri) yığına itilir ve yeni bir yığın çerçevesi oluşturulur. Ardından yürütme çağrılan işlevin başlangıcına aktarılır.

Normal işlevlerde, bir noktada işlev bir değer döndürür ve yığın "patlatılır". İşlevin yığın çerçevesi atılır ve yürütme önceki konumda devam eder.

Bir işlev bir jeneratör olduğunda, bir değer döndürebilir olmadan verim deyimi ile, yığın çerçevesi atılır edilir. Yerel değişkenlerin değerleri ve fonksiyon içindeki program sayacı korunur. Bu, jeneratörün verim ifadesinden devam ederek jeneratörün daha sonra yeniden başlatılmasına izin verir ve daha fazla kod yürütebilir ve başka bir değer döndürebilir.

Python 2.5'ten önce tüm jeneratörler bunu yaptı. Python 2.5 sırt değerlerini iletme yeteneğine eklendi içinde jeneratöre de. Bunu yaparken, geçirilen değer, üreteçten geçici olarak kontrolü (ve bir değeri) döndüren verim ifadesinden kaynaklanan bir ifade olarak kullanılabilir.

Jeneratörlerin temel avantajı, yığın çerçevesinin her atıldığı normal fonksiyonlardan farklı olarak, fonksiyonun "durumunun" korunmuş olmasıdır. İkincil bir avantaj, fonksiyon çağrısı yükünün (yığın kareleri oluşturma ve silme) bazılarından kaçınılmasıdır, ancak bu genellikle küçük bir avantajdır.


6

Stephan202'nin cevabına ekleyebileceğim tek şey, David Beazley'nin PyCon '08 "Sistem Programcıları için Jeneratör Hileleri" sunumuna bir göz atmanız önerisi. herhangi bir yer. Bu beni "Python biraz eğlenceli görünüyor" dan "Aradığım şey" e götürdü. Hiç de var http://www.dabeaz.com/generators/ .


6

Foo fonksiyonu ile jeneratör foo (n) arasında net bir ayrım yapmaya yardımcı olur:

def foo(n):
    yield n
    yield n+1

foo bir işlevdir. foo (6) bir jeneratör nesnesidir.

Bir jeneratör nesnesini kullanmanın tipik yolu bir döngüdedir:

for n in foo(6):
    print(n)

Döngü yazdırır

# 6
# 7

Bir jeneratörü devam ettirilebilir bir fonksiyon olarak düşünün.

yieldverilmiş returnolan değerlerin jeneratör tarafından "geri" alındığı gibi davranır . Ancak geri dönüşün aksine, jeneratör bir dahaki değer istendiğinde, jeneratörün işlevi foo, son verim ifadesinden sonra kaldığı yerden devam eder ve başka bir verim ifadesine ulaşana kadar çalışmaya devam eder.

Perde arkasında, bar=foo(6)jeneratör nesnesini çağırdığınızda bir nextniteliğe sahip olmanız için tanımlanır .

Foo'dan elde edilen değerleri almak için kendiniz diyebilirsiniz:

next(bar)    # Works in Python 2.6 or Python 3.x
bar.next()   # Works in Python 2.5+, but is deprecated. Use next() if possible.

Foo sona erdiğinde (ve artık next(bar)verili değer olmadığında) çağırmak bir StopInteration hatası verir.


5

Bu yazı , Python jeneratörlerinin kullanışlılığını açıklamak için bir araç olarak Fibonacci sayılarını kullanacaktır .

Bu yazı hem C ++ hem de Python koduna sahip olacak.

Fibonacci sayıları dizi olarak tanımlanır: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ....

Veya genel olarak:

F0 = 0
F1 = 1
Fn = Fn-1 + Fn-2

Bu son derece kolay bir C ++ fonksiyonuna aktarılabilir:

size_t Fib(size_t n)
{
    //Fib(0) = 0
    if(n == 0)
        return 0;

    //Fib(1) = 1
    if(n == 1)
        return 1;

    //Fib(N) = Fib(N-2) + Fib(N-1)
    return Fib(n-2) + Fib(n-1);
}

Ancak ilk altı Fibonacci numarasını yazdırmak istiyorsanız, yukarıdaki işlevle birçok değeri yeniden hesaplayacaksınız.

Örneğin:, Fib(3) = Fib(2) + Fib(1)aynı Fib(2)zamanda yeniden hesaplar Fib(1). Hesaplamak istediğiniz değer ne kadar yüksek olursa, o kadar kötü olur.

Dolayısıyla, durumu takip ederek yukarıdakileri yeniden yazmak cazip gelebilir main.

// Not supported for the first two elements of Fib
size_t GetNextFib(size_t &pp, size_t &p)
{
    int result = pp + p;
    pp = p;
    p = result;
    return result;
}

int main(int argc, char *argv[])
{
    size_t pp = 0;
    size_t p = 1;
    std::cout << "0 " << "1 ";
    for(size_t i = 0; i <= 4; ++i)
    {
        size_t fibI = GetNextFib(pp, p);
        std::cout << fibI << " ";
    }
    return 0;
}

Ama bu çok çirkin ve mantığımızı zorlaştırıyor main. Bizim durumumuzda devlet hakkında endişelenmemek daha iyi olurdumain .

Bir vectordeğer döndürebilir ve iteratorbu değerler kümesi üzerinde yineleme yapmak için a kullanabiliriz , ancak bu, çok sayıda dönüş değeri için bir kerede çok fazla bellek gerektirir.

Eski yaklaşımımıza geri dönersek, sayıları yazdırmanın yanı sıra başka bir şey yapmak istersek ne olur? Tüm kod bloğunu kopyalayıp yapıştırmamız mainve çıktı ifadelerini başka ne yapmak istiyorsak değiştirmeliyiz. Ve kodu kopyalayıp yapıştırırsanız, vurulmalısınız. Vurulmak istemiyorsun, değil mi?

Bu sorunları çözmek ve vurulmaktan kaçınmak için, geri arama işlevini kullanarak bu kod bloğunu yeniden yazabiliriz. Her yeni Fibonacci numarasıyla karşılaşıldığında, geri arama fonksiyonunu çağırırdık.

void GetFibNumbers(size_t max, void(*FoundNewFibCallback)(size_t))
{
    if(max-- == 0) return;
    FoundNewFibCallback(0);
    if(max-- == 0) return;
    FoundNewFibCallback(1);

    size_t pp = 0;
    size_t p = 1;
    for(;;)
    {
        if(max-- == 0) return;
        int result = pp + p;
        pp = p;
        p = result;
        FoundNewFibCallback(result);
    }
}

void foundNewFib(size_t fibI)
{
    std::cout << fibI << " ";
}

int main(int argc, char *argv[])
{
    GetFibNumbers(6, foundNewFib);
    return 0;
}

Bu açıkça bir gelişme, mantığınız maino kadar karmaşık değil ve Fibonacci numaraları ile istediğiniz her şeyi yapabilirsiniz, sadece yeni geri aramalar tanımlayın.

Ama bu hala mükemmel değil. Ya sadece ilk iki Fibonacci numarasını almak ve sonra bir şey yapmak, sonra biraz daha almak, sonra başka bir şey yapmak istersen?

mainOlduğu gibi devam edebiliriz ve GetFibNumbers'ın keyfi bir noktadan başlamasına izin vererek tekrar devlet eklemeye başlayabiliriz. Ancak bu, kodumuzu daha da şişirir ve Fibonacci sayılarını yazdırmak gibi basit bir görev için zaten çok büyük görünüyor.

Birkaç iş parçacığıyla üretici ve tüketici modeli uygulayabiliriz. Ancak bu, kodu daha da karmaşık hale getirir.

Bunun yerine jeneratörler hakkında konuşalım.

Python, jeneratör denilen gibi sorunları çözen çok güzel bir dil özelliğine sahiptir.

Jeneratör, bir işlevi yürütmenize, rastgele bir noktada durmanıza ve daha sonra kaldığınız yerden devam etmenize olanak tanır. Her seferinde bir değer döndürür.

Bir jeneratör kullanan aşağıdaki kodu göz önünde bulundurun:

def fib():
    pp, p = 0, 1
    while 1:
        yield pp
        pp, p = p, pp+p

g = fib()
for i in range(6):
    g.next()

Bu da bize sonuçları verir:

0 1 1 2 3 5

Bu yieldifade Python jeneratörleri ile birlikte kullanılır. Fonksiyonun durumunu kaydeder ve sarılık değeri döndürür. Jeneratörde sonraki () fonksiyonunu bir sonraki çağırışınızda, verimin kaldığı yerden devam eder.

Bu, geri arama işlev kodundan çok daha temizdir. Daha temiz kod, daha küçük kod var ve çok daha fonksiyonel koddan bahsetmiyoruz (Python keyfi olarak büyük tamsayılara izin veriyor).

Kaynak


3

Yineleyicilerin ve üreticilerin ilk görünümünün yaklaşık 20 yıl önce Icon programlama dilinde olduğuna inanıyorum.

Sözdizimine odaklanmadan başınızı etraflarına sarmanıza izin veren Simge genel bakışının tadını çıkarabilirsiniz (Icon muhtemelen bilmediğiniz bir dildir ve Griswold, dilinin diğer dillerden gelen insanlara faydalarını açıklıyor).

Orada sadece birkaç paragraf okuduktan sonra, jeneratörlerin ve yineleyicilerin faydası daha belirgin hale gelebilir.


2

Liste kavrayışları ile ilgili deneyim, Python genelinde yaygın bir şekilde kullanılabileceğini göstermiştir. Bununla birlikte, kullanım durumlarının çoğunun bellekte tam bir listeye sahip olması gerekmez. Bunun yerine, sadece birer birer unsurlar üzerinde tekrar etmeleri gerekir.

Örneğin, aşağıdaki toplama kodu bellekteki karelerin tam listesini oluşturacak, bu değerler üzerinde yineleyecek ve referansa artık gerek kalmadığında listeyi silecektir:

sum([x*x for x in range(10)])

Bellek, bunun yerine bir jeneratör ifadesi kullanılarak korunur:

sum(x*x for x in range(10))

Konteyner nesneleri için yapıcılara benzer faydalar sağlanır:

s = Set(word  for line in page  for word in line.split())
d = dict( (k, func(k)) for k in keylist)

Jeneratör ifadeleri, özellikle yinelenebilir bir girişi tek bir değere indirgeyen sum (), min () ve max () gibi işlevlerle kullanışlıdır:

max(len(line)  for line in file  if line.strip())

Daha


1

Jeneratörlerle ilgili 3 temel kavramı açıklayan bu kod parçasını koydum:

def numbers():
    for i in range(10):
            yield i

gen = numbers() #this line only returns a generator object, it does not run the code defined inside numbers

for i in gen: #we iterate over the generator and the values are printed
    print(i)

#the generator is now empty

for i in gen: #so this for block does not print anything
    print(i)
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.