Python jeneratör işlevlerini ne için kullanabilirsiniz?


213

Python öğrenmeye başladım ve jeneratör fonksiyonlarıyla karşılaştım, içlerinde verim ifadesi olanlar. Bu fonksiyonların çözmede ne tür problemlerin gerçekten iyi olduğunu bilmek istiyorum.


6
belki daha iyi bir soru 'em
cregox

Yanıtlar:


239

Jeneratörler size tembel bir değerlendirme yapar. Bunları üzerlerinde yineleyerek, açıkça 'for' ile veya dolaylı olarak yineleyen herhangi bir işleve veya yapıya geçirerek kullanırsınız. Üreteçleri, bir liste döndürüyormuş gibi birden çok öğeyi döndürmek olarak düşünebilirsiniz, ancak hepsini bir kerede döndürmek yerine, bunları teker teker döndürmek yerine bir sonraki öğe talep edilene kadar jeneratör işlevi duraklatılır.

Jeneratörler, tüm sonuçlara ihtiyacınız olup olmayacağını bilmediğiniz veya aynı anda tüm sonuçlar için belleği ayırmak istemediğiniz büyük sonuçları (özellikle döngüler içeren hesaplamaları) hesaplamak için iyidir . Veya jeneratörün başka bir jeneratör kullandığı veya başka bir kaynak tükettiği durumlar için ve bu mümkün olduğunca geç gerçekleşmişse daha uygundur.

Jeneratörler için başka bir kullanım (gerçekten aynıdır) geri çağrıları yinelemeyle değiştirmektir. Bazı durumlarda bir işlevin çok fazla iş yapmasını ve arayana geri bildirmesini istersiniz. Geleneksel olarak bunun için bir geri arama işlevi kullanırsınız. Bu geri aramayı çalışma işlevine iletirsiniz ve düzenli olarak bu geri aramayı çağırır. Jeneratör yaklaşımı, çalışma fonksiyonunun (şimdi bir jeneratör) geri arama hakkında hiçbir şey bilmemesi ve yalnızca bir şey raporlamak istediğinde verim vermesidir. Arayan, ayrı bir geri arama yazmak ve bunu çalışma fonksiyonuna aktarmak yerine, tüm raporlama jeneratörün etrafında küçük bir 'for' döngüsünde çalışır.

Örneğin, bir 'dosya sistemi arama' programı yazdığınızı varsayalım. Aramayı tamamen gerçekleştirebilir, sonuçları toplayabilir ve ardından birer birer görüntüleyebilirsiniz. İlk sonuçları göstermeden önce tüm sonuçların toplanması gerekir ve tüm sonuçlar aynı anda hafızada olur. Ya da sonuçları bulduğunuzda görüntüleyebilirsiniz, bu da bellekte daha verimli ve kullanıcıya karşı daha dostça olacaktır. İkincisi, sonuç yazdırma işlevini dosya sistemi arama işlevine geçirerek yapılabilir veya yalnızca arama işlevini bir jeneratör yaparak ve sonuç üzerinde yineleyerek yapılabilir.

Son iki yaklaşımın bir örneğini görmek istiyorsanız, bkz. Os.path.walk () (geri çağrı ile eski dosya sistemi yürüme işlevi) ve os.walk () (yeni dosya sistemi yürüme jeneratörü.) gerçekten tüm sonuçları bir listede toplamak istediniz, jeneratör yaklaşımı büyük liste yaklaşımına dönüştürmek önemsizdir:

big_list = list(the_generator)

Dosya sistemi listeleri üreten gibi bir jeneratör, bu jeneratörü döngüde çalıştıran koda paralel olarak eylemler gerçekleştiriyor mu? İdeal olarak bilgisayar, bir sonraki değeri elde etmek için jeneratörün yapması gereken şeyi yaparken eşzamanlı olarak döngü gövdesini çalıştırır (son sonucu işler).
Steven Lu

@StevenLu: Bir sonraki sonucu elde etmek için iş parçacıklarını önce yieldve joinsonra el ile başlatma sorununa gitmedikçe , paralel olarak çalışmaz (ve hiçbir standart kütüphane oluşturucusu bunu yapmaz; gizlice başlatma iş parçacıkları kaşlarını çatmaz). Bir yieldsonraki değer talep edilene kadar jeneratör her birinde duraklar . Jeneratör G / Ç'yi sarıyorsa, işletim sistemi kısa süre içinde talep edileceği varsayımıyla dosyadan proaktif olarak önbellek veriyor olabilir, ancak bu işletim sistemi, Python dahil değildir.
ShadowRanger

90

Jeneratörü kullanmanın nedenlerinden biri, çözümü bir tür çözüm için daha net hale getirmektir.

Diğeri, sonuçları birer birer işlemek, yine de ayrılmış olarak işleyeceğiniz büyük sonuç listeleri oluşturmaktan kaçınmaktır.

Böyle bir fibonacci-up-to-n fonksiyonunuz varsa:

# function version
def fibon(n):
    a = b = 1
    result = []
    for i in xrange(n):
        result.append(a)
        a, b = b, a + b
    return result

Fonksiyonu şu şekilde daha kolay yazabilirsiniz:

# generator version
def fibon(n):
    a = b = 1
    for i in xrange(n):
        yield a
        a, b = b, a + b

İşlev daha nettir. Ve eğer işlevi böyle kullanırsanız:

for x in fibon(1000000):
    print x,

bu örnekte, jeneratör sürümü kullanılıyorsa, 1000000 öğe listesinin tamamı oluşturulmaz, her seferinde sadece bir değer. İlk olarak bir listenin oluşturulacağı liste sürümü kullanılırken durum böyle olmaz.


18
ve bir listeye ihtiyacınız varsa, her zaman yapabilirsinizlist(fibon(5))
endolith

41

PEP 255'teki "Motivasyon" bölümüne bakın .

Üreticilerin aşikar olmayan bir kullanımı, iş parçacığı kullanmadan güncelleme kullanıcı arayüzü veya birkaç işi "aynı anda" (aslında araya eklenmiş gibi) çalıştırmanıza izin veren kesilebilir işlevler oluşturuyor.


1
Motivasyon bölümü özel bir örneğe sahip olması açısından hoş: "Bir üretici işlevi üretilen değerler arasında durumun korunmasını gerektirecek kadar zor bir işe sahip olduğunda, çoğu programlama dili üreticinin argümanına geri çağrı işlevi eklemenin ötesinde hoş ve verimli bir çözüm sunmaz. liste ... Örneğin, standart kütüphanedeki tokenize.py bu yaklaşımı benimser "
Ben Creasy

38

Şüphemi temizleyen bu açıklamayı buluyorum. Çünkü bilmeyen bir kişinin Generatorsde bilmediği bir olasılık varyield

Dönüş

Return ifadesi, tüm yerel değişkenlerin yok edildiği ve ortaya çıkan değerin arayana geri verildiği (döndürüldüğü) yerdir. Aynı fonksiyon bir süre sonra çağrılırsa, fonksiyon yeni bir değişken seti alacaktır.

Yol ver

Fakat bir fonksiyondan çıktığımızda yerel değişkenler atılmazsa ne olur? Bu, kaldığımız yeri yapabileceğimizi gösterir resume the function. Burası kavramın generatorstanıtıldığı ve yieldifadenin functionkaldığı yerden devam ettiği yerdir .

  def generate_integers(N):
    for i in xrange(N):
    yield i

    In [1]: gen = generate_integers(3)
    In [2]: gen
    <generator object at 0x8117f90>
    In [3]: gen.next()
    0
    In [4]: gen.next()
    1
    In [5]: gen.next()

Python'daki returnve yieldifadeler arasındaki fark budur .

Verim deyimi, bir işlevi bir jeneratör işlevi yapan şeydir.

Yani jeneratörler yineleyiciler oluşturmak için basit ve güçlü bir araçtır. Normal işlevler gibi yazılırlar, ancak yieldveri döndürmek istediklerinde ifadeyi kullanırlar . Next () her çağrıldığında, jeneratör kaldığı yerden devam eder (tüm veri değerlerini ve en son hangi cümleyi çalıştırdığını hatırlar).


33

Gerçek Dünya Örneği

Diyelim ki MySQL tablonuzda 100 milyon alanınız var ve her alan için Alexa derecesini güncellemek istiyorsunuz.

İhtiyacınız olan ilk şey, alan adlarınızı veritabanından seçmektir.

Diyelim ki tablo adınız domainsve sütun adınız domain.

Eğer kullanırsanız SELECT domain FROM domains, çok fazla bellek tüketecek 100 milyon satır döndürecek. Böylece sunucunuz çökebilir.

Böylece programı toplu olarak çalıştırmaya karar verdiniz. Parti boyutumuzun 1000 olduğunu varsayalım.

İlk partimizde ilk 1000 satırı sorgulayacağız, her alan için Alexa derecesini kontrol edeceğiz ve veritabanı satırını güncelleyeceğiz.

İkinci partimizde önümüzdeki 1000 satır üzerinde çalışacağız. Üçüncü partimizde 2001'den 3000'e kadar olacak.

Şimdi yığınlarımızı üreten bir jeneratör fonksiyonuna ihtiyacımız var.

İşte jeneratör fonksiyonumuz:

def ResultGenerator(cursor, batchsize=1000):
    while True:
        results = cursor.fetchmany(batchsize)
        if not results:
            break
        for result in results:
            yield result

Gördüğünüz gibi, fonksiyonumuz yieldsonuçlara devam ediyor . returnBunun yerine anahtar kelimeyi kullandıysanız yield, tüm işlev, dönüşe ulaştığında sona erer.

return - returns only once
yield - returns multiple times

Eğer bir fonksiyon anahtar kelimeyi kullanıyorsa, yieldo zaman bu bir jeneratör olur.

Şimdi şöyle tekrarlayabilirsiniz:

db = MySQLdb.connect(host="localhost", user="root", passwd="root", db="domains")
cursor = db.cursor()
cursor.execute("SELECT domain FROM domains")
for result in ResultGenerator(cursor):
    doSomethingWith(result)
db.close()

eğer verim, özyinelemeli / dyanmik programlama açısından açıklanabilseydi daha pratik olurdu!
Igaurav

27

Tamponlama. Büyük parçalar halinde veri almak verimli, ancak küçük parçalar halinde işlemek verimli olduğunda, bir jeneratör yardımcı olabilir:

def bufferedFetch():
  while True:
     buffer = getBigChunkOfData()
     # insert some code to break on 'end of data'
     for i in buffer:    
          yield i

Yukarıdakiler, tamponlamayı işlemden kolayca ayırmanızı sağlar. Tüketici işlevi artık değerleri arabelleğe alma konusunda endişelenmeden tek tek alabilir.


3
GetBigChuckOfData tembel değilse, o zaman burada ne fayda getirisi anlamıyorum. Bu işlev için kullanım durumu nedir?
Sean Geoffrey Pietz

1
Ama mesele şu ki, IIUC, bufferedFetch getBigChunkOfData çağrısını tembelleştiriyor. GetBigChunkOfData zaten tembel olsaydı, bufferedFetch işe yaramazdı. BufferedFetch () öğesine yapılan her çağrı, bir BigChunk zaten okunmuş olmasına rağmen bir arabellek öğesi döndürür.
hmijail resignees

21

Jeneratörlerin kodunuzu temizlemede ve kodu kapsüllemek ve modüle etmek için benzersiz bir yol vererek çok yardımcı olduğunu gördüm. Bir şeylerin ihtiyaçları Kodunuzdaki her yerde aradı (ve sadece bir döngü veya örneğin bir blok içinde) vakti gelince sürekli kendi iç işlemeye ve esaslı değerleri tükürmek bir şey lazım bir durumda, jeneratörleri için özellik kullanın.

Soyut bir örnek, bir döngü içinde yaşamayan bir Fibonacci sayı üreteci olabilir ve herhangi bir yerden çağrıldığında her zaman dizideki bir sonraki sayıyı döndürür:

def fib():
    first = 0
    second = 1
    yield first
    yield second

    while 1:
        next = first + second
        yield next
        first = second
        second = next

fibgen1 = fib()
fibgen2 = fib()

Artık kodunuzun herhangi bir yerinden çağırabileceğiniz iki Fibonacci sayı üreteci nesneniz var ve her zaman daha büyük Fibonacci sayılarını aşağıdaki gibi sırayla döndürecekler:

>>> fibgen1.next(); fibgen1.next(); fibgen1.next(); fibgen1.next()
0
1
1
2
>>> fibgen2.next(); fibgen2.next()
0
1
>>> fibgen1.next(); fibgen1.next()
3
5

Jeneratörlerle ilgili en güzel şey, nesneleri yaratma çemberinden geçmek zorunda kalmadan durumu kapsüllemeleridir. Onlar hakkında düşünmenin bir yolu, içsel durumlarını hatırlayan "işlevler" dir.

Python Jeneratörlerinden Fibonacci örneğini aldım - Nedir bunlar? ve küçük bir hayal gücü ile, jeneratörlerin fordöngülere ve diğer geleneksel yineleme yapılarına mükemmel bir alternatif oluşturduğu birçok başka durumla karşılaşabilirsiniz .


19

Basit açıklama: Bir forifade düşünün

for item in iterable:
   do_stuff()

Çoğu zaman, tüm öğelerin iterablebaşlangıçtan itibaren orada olması gerekmez, ancak gerektiğinde anında oluşturulabilir. Bu her ikisinde de çok daha verimli olabilir

  • boşluk (asla tüm öğeleri aynı anda saklamanız gerekmez) ve
  • zaman (yineleme tüm öğeleri gerekli önce bitebilir).

Diğer zamanlarda, tüm öğeleri önceden bilmiyorsunuz bile. Örneğin:

for command in user_input():
   do_stuff_with(command)

Kullanıcının tüm komutlarını önceden bilmenin bir yolu yoktur, ancak size komutları teslim eden bir jeneratörünüz varsa böyle güzel bir döngü kullanabilirsiniz:

def user_input():
    while True:
        wait_for_command()
        cmd = get_command()
        yield cmd

Jeneratörler ile, elbette kaplar üzerinde yineleme yaparken mümkün olmayan sonsuz diziler üzerinde yineleme yapabilirsiniz.


... ve sonsuz bir sekans, küçük bir liste üzerinde tekrar tekrar çevrim yapılarak üretilir ve sona ulaşıldıktan sonra başa döner. Bunu grafiklerde renk seçmek veya metinde meşgul atıcılar veya döndürücüler üretmek için kullanıyorum.
Andrej Panjkov

@mataap: Bunun itertooliçin bir var - bakın cycles.
martineau

12

Favori kullanımlarım "filtre" ve "azaltma" işlemleridir.

Diyelim ki bir dosya okuyoruz ve yalnızca "##" ile başlayan satırları istiyoruz.

def filter2sharps( aSequence ):
    for l in aSequence:
        if l.startswith("##"):
            yield l

Daha sonra jeneratör fonksiyonunu uygun bir döngüde kullanabiliriz

source= file( ... )
for line in filter2sharps( source.readlines() ):
    print line
source.close()

İndirgeme örneği benzerdir. Diyelim ki <Location>...</Location>satır bloklarını bulmamız gereken bir dosya var . [HTML etiketleri değil, etiket benzeri görünen satırlar.]

def reduceLocation( aSequence ):
    keep= False
    block= None
    for line in aSequence:
        if line.startswith("</Location"):
            block.append( line )
            yield block
            block= None
            keep= False
        elif line.startsWith("<Location"):
            block= [ line ]
            keep= True
        elif keep:
            block.append( line )
        else:
            pass
    if block is not None:
        yield block # A partial block, icky

Yine, bu jeneratörü döngü için uygun bir şekilde kullanabiliriz.

source = file( ... )
for b in reduceLocation( source.readlines() ):
    print b
source.close()

Fikir, bir jeneratör fonksiyonunun bir sekansı filtrelememize veya azaltmamıza izin vererek, her seferinde bir değer olan başka bir sekans üretmesidir.


8
fileobj.readlines()jeneratörleri kullanma amacını yenerek tüm dosyayı hafızadaki bir listeye okurdu. Dosya nesneleri zaten yinelenebilir olduğundan, for b in your_generator(fileobject):bunun yerine kullanabilirsiniz . Bu şekilde, tüm dosyayı okumaktan kaçınmak için dosyanız her seferinde bir satır okunacaktır.
nosklo

reduceLocation bir liste oldukça garip bir veridir, neden sadece her bir çizgiyi vermiyorsunuz? Ayrıca filtreleme ve azaltma, beklenen davranışlara sahip yapılardır (ipython vb. Yardım bölümüne bakın), "azaltma" kullanımınız filtre ile aynıdır.
James Antill

Readlines () iyi bir nokta. Genellikle ünite testi sırasında dosyaların birinci sınıf satır yineleyicileri olduğunu anlıyorum.
S.Lott

Aslında, "azaltma" birden çok tekli çizgiyi bileşik bir nesnede birleştiriyor. Tamam, bu bir liste, ama yine de kaynaktan alınan bir azalma.
S.Lott

9

Bir jeneratörü kullanabileceğiniz pratik bir örnek, bir çeşit şekle sahipseniz ve köşeleri, kenarları veya her şeyi tekrarlamak istiyorsanız. Kendi projem için ( burada kaynak kodu ) bir dikdörtgen vardı:

class Rect():

    def __init__(self, x, y, width, height):
        self.l_top  = (x, y)
        self.r_top  = (x+width, y)
        self.r_bot  = (x+width, y+height)
        self.l_bot  = (x, y+height)

    def __iter__(self):
        yield self.l_top
        yield self.r_top
        yield self.r_bot
        yield self.l_bot

Şimdi bir dikdörtgen oluşturabilir ve köşelerinde döngü yapabilirim:

myrect=Rect(50, 50, 100, 100)
for corner in myrect:
    print(corner)

Bunun yerine __iter__bir yönteminiz olabilir iter_cornersve bunu ile çağırabilirsiniz for corner in myrect.iter_corners(). O __iter__zamandan beri kullanmak daha zarif , sınıf örneği adını doğrudan forifadede kullanabiliriz.


Benzer sınıf alanlarını jeneratör olarak geçirme fikrine
bayıldım

7

Temelde, giriş koruma durumu üzerinden yineleme yaparken geri arama işlevlerinden kaçınmak.

Jeneratörler kullanılarak neler yapılabileceğine genel bir bakış için buraya ve buraya bakın .


4

Bununla birlikte, bazı iyi cevaplar, ayrıca , jeneratörlerin daha güçlü kullanım durumlarından bazılarının açıklanmasına yardımcı olan Python Fonksiyonel Programlama öğreticisinin tamamen okunmasını da tavsiye ederim .


3

Bir jeneratörün gönderme yönteminden bahsedilmediğinden, işte bir örnek:

def test():
    for i in xrange(5):
        val = yield
        print(val)

t = test()

# Proceed to 'yield' statement
next(t)

# Send value to yield
t.send(1)
t.send('2')
t.send([3])

Çalışan bir jeneratöre bir değer gönderme olasılığını gösterir. Aşağıdaki videodaki jeneratörler hakkında daha gelişmiş bir kurs (açıklama yield, paralel işleme için jeneratörler, özyineleme sınırından kaçınma vb. Dahil )

David Beazley, PyCon 2014'teki jeneratörlerde


2

Web sunucumuz bir proxy işlevi görürken jeneratörleri kullanıyorum:

  1. İstemci, sunucudan proxy kullanan bir URL ister
  2. Sunucu hedef URL'yi yüklemeye başlar
  3. Sunucu sonuçları alır almaz müşteriye geri döndürür.

1

Malzeme yığınları. Bir öğe dizisi oluşturmak istediğinizde, ancak hepsini bir seferde bir listeye 'gerçekleştirmek' istemiyorsanız. Örneğin, asal sayıları döndüren basit bir oluşturucunuz olabilir:

def primes():
    primes_found = set()
    primes_found.add(2)
    yield 2
    for i in itertools.count(1):
        candidate = i * 2 + 1
        if not all(candidate % prime for prime in primes_found):
            primes_found.add(candidate)
            yield candidate

Daha sonra bunu sonraki primerlerin ürünlerini üretmek için kullanabilirsiniz:

def prime_products():
    primeiter = primes()
    prev = primeiter.next()
    for prime in primeiter:
        yield prime * prev
        prev = prime

Bunlar oldukça önemsiz örneklerdir, ancak büyük (potansiyel olarak sonsuz!) Veri kümelerini önceden oluşturmadan nasıl işleyebileceğini görebilirsiniz; bu, daha açık kullanımlardan sadece biridir.


değilse (tüm primes_found'da asal% aday)) tüm olmalıdır (primes_found'da asal için% aday)
rjmunro

Evet, ben yazmak istedim "yoksa (aday% prime == 0 prime için primes_found). Seninki biraz daha temiz. :)
Nick Johnson

Ben olmasa bile 'not' silmek unuttum sanırım (primes_found prime% aday)
Thava

0

Ayrıca n'ye kadar asal sayıları yazdırmak için de iyidir:

def genprime(n=10):
    for num in range(3, n+1):
        for factor in range(2, num):
            if num%factor == 0:
                break
        else:
            yield(num)

for prime_num in genprime(100):
    print(prime_num)
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.