Uygulamada, Python 3.3'teki yeni “getiri” sözdiziminin ana kullanım alanları nelerdir?


407

Beynimi PEP 380'in etrafına sarmakta zorlanıyorum .

  1. "Verim kaynağı" nın yararlı olduğu durumlar nelerdir?
  2. Klasik kullanım durumu nedir?
  3. Mikro ipliklerle neden karşılaştırılır?

[ Güncelleme ]

Şimdi zorluklarımın nedenini anlıyorum. Jeneratörler kullandım, ama gerçekten hiç kullanmadım couteinler ( PEP-342 tarafından tanıtıldı ). Bazı benzerliklere rağmen, jeneratörler ve koroutinler temel olarak iki farklı kavramdır. Koroutinleri anlamak (sadece jeneratörler değil), yeni sözdizimini anlamak için anahtardır.

IMHO koroutinleri en belirsiz Python özelliğidir , çoğu kitap onu işe yaramaz ve ilgisiz gösterir.

Harika cevaplar için teşekkürler, ama agf ve David Beazley sunumlarına bağlantı veren yorumu için teşekkürler . David sallanır.



Yanıtlar:


571

Önce bir şey yoldan çekelim. yield from gEşdeğer olan açıklama , tümüyle ilgili olanlara for v in g: yield v adalet yapmaya bile başlamıyoryield from . Çünkü, bununla yüzleşelim, eğer her yield fromşey fordöngüyü genişletirse , o zaman yield fromdile eklemeyi garanti etmez ve bir sürü yeni özelliğin Python 2.x'te uygulanmasını engellemez.

Ne yield fromyapar o arayan ve alt jeneratör arasında şeffaf bir çift yönlü bağlantı kurar :

  • Bağlantı, yalnızca üretilen unsurları değil, her şeyi de doğru bir şekilde yayması anlamında "şeffaftır" (örn. İstisnalar yayılır).

  • Bağlantı verileri hem gönderileceği olabilir anlamda "çift yönlü" dır dan ve karşı bir jeneratör.

( TCP'den bahsediyorsak, yield from g"şimdi istemcimin soketinin geçici olarak bağlantısını kesin ve bu diğer sunucu soketine yeniden bağlayın" anlamına gelebilir. )

BTW, bir jeneratöre veri göndermenin ne anlama geldiğinden bile emin değilseniz, önce her şeyi bırakmanız ve koroutinleri okumalısınız - çok kullanışlıdırlar ( alt rutinlerle kontrast ), ancak ne yazık ki Python'da daha az bilinir. Dave Beazley'nin Coroutines üzerindeki Meraklı Kursu mükemmel bir başlangıç. Hızlı astar için slaytları 24-33 okuyun .

Verimi kullanarak bir jeneratörden veri okuma

def reader():
    """A generator that fakes a read from a file, socket, etc."""
    for i in range(4):
        yield '<< %s' % i

def reader_wrapper(g):
    # Manually iterate over data produced by reader
    for v in g:
        yield v

wrap = reader_wrapper(reader())
for i in wrap:
    print(i)

# Result
<< 0
<< 1
<< 2
<< 3

Manuel olarak tekrarlamak yerine, reader()sadece yield frombunu yapabiliriz .

def reader_wrapper(g):
    yield from g

Bu işe yarar ve bir satır kod kaldırdık. Ve muhtemelen niyet biraz daha açık (ya da değil). Ama hiçbir şey hayat değiştirmiyor.

Verimi kullanarak bir jeneratöre (koroutin) veri gönderme - Bölüm 1

Şimdi daha ilginç bir şey yapalım. Haydi kendisine writergönderilen verileri kabul eden ve bir sokete, fd'ye vb. Yazan bir coututin oluşturalım .

def writer():
    """A coroutine that writes data *sent* to it to fd, socket, etc."""
    while True:
        w = (yield)
        print('>> ', w)

Şimdi soru şudur: Sarıcı işlevi, yazıcıya veri göndermeyi nasıl ele almalıdır, böylece sargıya gönderilen herhangi bir veri saydam olarak şuraya gönderilir writer()?

def writer_wrapper(coro):
    # TBD
    pass

w = writer()
wrap = writer_wrapper(w)
wrap.send(None)  # "prime" the coroutine
for i in range(4):
    wrap.send(i)

# Expected result
>>  0
>>  1
>>  2
>>  3

Sarıcı kendisine gönderilen verileri kabul etmelidir (belli ki) ve ayrıca StopIterationfor döngüsünün ne zaman tükendiğini de ele almalıdır . Açıkçası sadece yapmak for x in coro: yield xyapmaz. İşte çalışan bir sürüm.

def writer_wrapper(coro):
    coro.send(None)  # prime the coro
    while True:
        try:
            x = (yield)  # Capture the value that's sent
            coro.send(x)  # and pass it to the writer
        except StopIteration:
            pass

Veya bunu yapabiliriz.

def writer_wrapper(coro):
    yield from coro

Bu, 6 satır kod tasarrufu sağlar, çok daha okunabilir hale getirir ve çalışır. Sihirli!

Jeneratör verimine veri gönderme - Bölüm 2 - İstisna yönetimi

Daha karmaşık hale getirelim. Yazarımızın istisnaları ele alması gerekiyorsa ne olur? Diyelim ki writera tutamakları SpamExceptionve ***bir tane ile karşılaşırsa yazdırıyor .

class SpamException(Exception):
    pass

def writer():
    while True:
        try:
            w = (yield)
        except SpamException:
            print('***')
        else:
            print('>> ', w)

Değişmezsek ne olur writer_wrapper? Çalışıyor mu? Hadi deneyelim

# writer_wrapper same as above

w = writer()
wrap = writer_wrapper(w)
wrap.send(None)  # "prime" the coroutine
for i in [0, 1, 2, 'spam', 4]:
    if i == 'spam':
        wrap.throw(SpamException)
    else:
        wrap.send(i)

# Expected Result
>>  0
>>  1
>>  2
***
>>  4

# Actual Result
>>  0
>>  1
>>  2
Traceback (most recent call last):
  ... redacted ...
  File ... in writer_wrapper
    x = (yield)
__main__.SpamException

Um, işe yaramıyor çünkü x = (yield)sadece istisnayı yükseltiyor ve her şey çöküyor. Çalışmasını sağlayalım, ancak istisnaları manuel olarak ele alıp gönderme veya alt jeneratöre atma ( writer)

def writer_wrapper(coro):
    """Works. Manually catches exceptions and throws them"""
    coro.send(None)  # prime the coro
    while True:
        try:
            try:
                x = (yield)
            except Exception as e:   # This catches the SpamException
                coro.throw(e)
            else:
                coro.send(x)
        except StopIteration:
            pass

Bu çalışıyor.

# Result
>>  0
>>  1
>>  2
***
>>  4

Ama bu da öyle!

def writer_wrapper(coro):
    yield from coro

yield fromŞeffaf kolları değerleri gönderme veya alt jeneratörüne değerlerini atma.

Bu yine de tüm köşe vakalarını kapsamıyor. Dış jeneratör kapatılırsa ne olur? Alt jeneratör bir değer döndürdüğünde (evet, Python 3.3+, jeneratörler değer döndürebilir), dönüş değeri nasıl yayılmalıdır? Bu yield fromşeffaf her durumu ele gerçekten etkileyici .yield fromsihirli bir şekilde çalışır ve tüm bu vakaları ele alır.

Ben şahsen yield fromkötü bir anahtar kelime seçim olduğunu hissediyorum çünkü iki yönlü doğa belirgin yapmaz . delegateDile yeni bir anahtar kelime eklemek mevcut anahtar kelimeleri birleştirmekten çok daha zor olduğu için önerilen başka anahtar kelimeler vardı (beğenildi ancak reddedildi).

Özetle, düşünmek en iyisi yield frombir şekildetransparent two way channel arayan ile alt jeneratör arasında .

Referanslar:

  1. PEP 380 - Bir alt jeneratöre devretme sözdizimi (Ewing) [v3.3, 2009-02-13]
  2. PEP 342 - Geliştirilmiş Jeneratörler (GvR, Eby) ile Eşsiz Çalışmalar [v2.5, 2005-05-10]

3
@PraveenGollakota, sorunuzun ikinci bölümünde, - Bölüm 1'deki verimi kullanarak bir jeneratöre (coroutine) veri gönderme - Alınan öğeyi iletmek için coroutines'den daha fazlasına sahipseniz ne olur? Örneğinizdeki sarmalayıcıya birden fazla coututin sağladığınız ve öğelerin hepsine veya alt kümesine gönderilmesi gereken bir yayıncı veya abone senaryosu gibi mi?
Kevin Ghaboosi

3
@PraveenGollakota, harika cevap için Kudos. Küçük örnekler bana bir şeyleri denememe izin veriyor. Dave Beazley kursuna bağlantı bir bonus oldu!
BiGYaN

1
yapıyor except StopIteration: passİÇ while True:döngü doğru bir temsili değil yield from coro- sonsuz döngü değildir ve sonra coro, (yani StopIteration yükseltir) tükenmiş writer_wrapperbir sonraki deyimi çalıştırır. Son açıklamadan sonra, StopIterationherhangi bir bitmiş jeneratör olarak kendiliğinden yükselecektir ...
Aprillion

1
... yani writeriçermek for _ in range(4)yerine, eğer while Trueo zaman baskı >> 3sonra da otomatik olarak yükseltmek StopIterationve bu otomatik olarak ele yield fromve daha sonra writer_wrapperkendi otomatik yükseltmek olacaktır StopIterationve çünkü blok wrap.send(i)içinde değil try, aslında bu noktada yükseltilmiş olacaktır ( yani geri wrap.send(i)izleme jeneratörün içinden herhangi bir şeyi değil , sadece hattı rapor edecektir )
Aprillion

3
" Adalet yapmaya bile başlamıyor " yazısının ardından doğru cevaba geldiğimi biliyorum. Büyük açıklama için teşekkürler!
Hot.PxL

89

"Verim kaynağı" nın yararlı olduğu durumlar nelerdir?

Böyle bir döngüye sahip olduğunuz her durum:

for x in subgenerator:
  yield x

PEP'in açıkladığı gibi, bu, alt jeneratörü kullanmak için oldukça naif bir girişimdir, çeşitli yönleri, özellikle PEP 342 tarafından sunulan .throw()/ .send()/ .close()mekanizmalarının doğru kullanımı eksiktir . Bunu doğru bir şekilde yapmak için oldukça karmaşık bir kod gereklidir.

Klasik kullanım durumu nedir?

Özyinelemeli bir veri yapısından bilgi almak istediğinizi düşünün. Diyelim ki tüm yaprak düğümlerini bir ağaca almak istiyoruz:

def traverse_tree(node):
  if not node.children:
    yield node
  for child in node.children:
    yield from traverse_tree(child)

Daha da önemlisi, yield fromjeneratör kodunu yeniden düzenlemenin basit bir yöntemi olmadığı gerçeğidir . Bunun gibi (anlamsız) bir jeneratörünüz olduğunu varsayalım:

def get_list_values(lst):
  for item in lst:
    yield int(item)
  for item in lst:
    yield str(item)
  for item in lst:
    yield float(item)

Şimdi bu döngüleri ayrı jeneratörlere ayırmaya karar veriyorsunuz. Olmadan yield from, bu çirkin, gerçekten yapmak isteyip istemediğinizi iki kez düşüneceğiniz noktaya kadar. İle yield from, bakmak için aslında güzel:

def get_list_values(lst):
  for sub in [get_list_values_as_int, 
              get_list_values_as_str, 
              get_list_values_as_float]:
    yield from sub(lst)

Mikro ipliklerle neden karşılaştırılır?

PEP'deki bu bölümün neden bahsettiğini düşünüyorum , her jeneratörün kendi izole yürütme bağlamı var. Yürütmenin, jeneratör-yineleyici ve arayan arasında sırasıyla değiştirildiği gerçeğiyle birlikte yieldve __next__()sırasıyla, bu, işletim sisteminin yürütme iş parçacığını yürütme bağlamıyla (yığın, kayıtlar, ...).

Bunun etkisi de karşılaştırılabilir: Hem jeneratör-yineleyici hem de arayan aynı zamanda yürütme durumlarında ilerler, yürütmeleri serpiştirilir. Örneğin, jeneratör bir tür hesaplama yaparsa ve arayan sonuçları yazdırırsa, sonuçları en kısa sürede görürsünüz. Bu bir eşzamanlılık biçimidir.

Bu benzetmeye özgü bir şey yield fromdeğil - Python'daki jeneratörlerin genel bir özelliği.


Jeneratörleri yeniden düzenleme bugün acı verici .
Josh Lee

1
Itertools'u jeneratörleri (itertools.chain gibi şeyler) yeniden düzenlemek için çok fazla kullanma eğilimindeyim, bu çok önemli değil. Verimi seviyorum ama hala ne kadar devrimci olduğunu göremiyorum. Muhtemelen, Guido bu konuda çılgın olduğu için, ama büyük resmi kaçırmam gerekiyor. Sanırım send () için harikadır, çünkü bu yeniden düzenlenmesi zordur, ancak bunu çok sık kullanmıyorum.
e-satis

Bunların get_list_values_as_xxxtek bir satıra sahip basit jeneratörler olduğunu for x in input_param: yield int(x)ve sırasıyla diğer iki ile strandfloat
madtyn

@NiklasB. re "özyinelemeli bir veri yapısından bilgi çıkar." Veri için sadece Py'e giriyorum. Bu Q'da bir bıçak alabilir misin ?
alancalvitti

33

Eğer bir jeneratör içinden bir jeneratör çağırmak bir "pompa" ihtiyaç her yerde yeniden için yielddeğerler: for v in inner_generator: yield v. PEP'in işaret ettiği gibi, çoğu insanın görmezden geldiği ince karmaşıklıklar vardır. Lokal olmayan akış kontrol benzeri throw()PEP'te verilen bir örnektir. Yeni sözdizimi yield from inner_generator, daha forönce açık döngüyü yazdığınız her yerde kullanılır . Yine de sadece sözdizimsel şeker değil: forDöngü tarafından göz ardı edilen tüm köşe vakalarını ele alıyor . "Şekerli" olmak insanları kullanmaya ve böylece doğru davranışları almaya teşvik eder.

Tartışma dizisindeki bu mesaj şu karmaşıklıklardan bahsediyor:

PEP 342 tarafından sunulan ek jeneratör özellikleri ile artık durum böyle değil: Greg'in PEP'inde açıklandığı gibi basit yineleme send () ve throw () yöntemlerini doğru şekilde desteklemiyor. Send () ve throw () 'ı desteklemek için gereken jimnastik, onları yıktığınızda aslında o kadar karmaşık değil, ama onlar da önemsiz değil.

Jeneratörlerin bir tür paralellik olduğunu gözlemlemekten başka, mikro ipliklerle yapılan bir karşılaştırmayla konuşamam . Askıdaki jeneratörü yield, bir tüketici işlem parçasına değer gönderen bir evre olarak düşünebilirsiniz . Gerçek uygulama böyle bir şey olmayabilir (ve gerçek uygulama açıkça Python geliştiricileri için büyük ilgi çekicidir), ancak bu kullanıcıları ilgilendirmez.

Yeni yield fromsözdizimi, iş parçacığı oluşturma açısından dile herhangi bir ek özellik eklemez, sadece varolan özelliklerin doğru kullanımını kolaylaştırır. Daha doğrusu , bir uzman tarafından yazılan karmaşık bir iç jeneratörün acemi bir tüketicisi için kolaylaştırır , karmaşık özelliklerinden herhangi birini bozmadan bu jeneratörün içinden geçmesini .


23

Kısa bir örnek, yield fromkullanım durumlarından birini anlamanıza yardımcı olacaktır : başka bir jeneratörden değer alma

def flatten(sequence):
    """flatten a multi level list or something
    >>> list(flatten([1, [2], 3]))
    [1, 2, 3]
    >>> list(flatten([1, [2], [3, [4]]]))
    [1, 2, 3, 4]
    """
    for element in sequence:
        if hasattr(element, '__iter__'):
            yield from flatten(element)
        else:
            yield element

print(list(flatten([1, [2], [3, [4]]])))

2
Sadece bir listeye dönüşüm olmadan sonunda baskı biraz daha güzel görüneceğini önermek istedim -print(*flatten([1, [2], [3, [4]]]))
yoniLavi

6

yield from temel olarak yineleyicileri etkili bir şekilde zincirler:

# chain from itertools:
def chain(*iters):
    for it in iters:
        for item in it:
            yield item

# with the new keyword
def chain(*iters):
    for it in iters:
        yield from it

Gördüğünüz gibi bir saf Python döngüsünü kaldırıyor. Yaptığı hemen hemen hepsi bu, ancak yineleyicileri zincirlemek Python'da oldukça yaygın bir model.

İş parçacıkları temel olarak işlevlerden tamamen rastgele noktalardan atlamanızı ve başka bir işlevin durumuna geri atlamanızı sağlayan bir özelliktir. İş parçacığı süpervizörü bunu çok sık yapar, bu nedenle program tüm bu işlevleri aynı anda çalıştırıyor gibi görünür. Sorun noktaların rastgele olmasıdır, bu yüzden süpervizörün fonksiyonu sorunlu bir noktada durdurmasını önlemek için kilitleme kullanmanız gerekir.

Jeneratörler bu anlamda iş parçacıklarına oldukça benzer: Belirli noktaları (her ne zaman yield İçeri ve dışarı atlayabileceğiniz ) . Bu şekilde kullanıldığında, jeneratörlere koroutin denir.

Daha fazla bilgi için Python'daki coroutines hakkında bu mükemmel eğiticileri okuyun


10
Bu cevap yanıltıcıdır, çünkü yukarıda belirtildiği gibi "verim" in belirgin özelliğini kullanmaktadır: send () ve throw () desteği.
Justin W

2
@Justin W: Ben size noktası alamadım çünkü aslında yanıltıcıdır önce okumak ne olursa olsun sanırım throw()/send()/close()olan yieldözellikler yield frombu basitleştirmek kod gerekiyordu olarak açıkçası düzgün uygulamak zorundadır. Bu tür önemsizliklerin kullanımla hiçbir ilgisi yoktur.
Jochen Ritzel

5
Yukarıda Ben Jackson'ın cevabını tartışıyor musunuz? Cevabınızı okuduğumda, sağladığınız kod dönüşümünü izleyen sözdizimsel şeker olması. Ben Jackson'ın cevabı bu iddiayı özellikle yalanlamaktadır.
Justin W

@JochenRitzel Zaten var chainolduğu için asla kendi işlevinizi yazmanıza gerek itertools.chainyoktur. Kullanın yield from itertools.chain(*iters).
Acumenus

4

İçin uygulanan kullanımda Asenkron IO eşyordamın , yield frombenzer bir davranışı vardır awaita eşyordam fonksiyonu . Her ikisi de koroutinin yürütülmesini askıya almak için kullanılır.

Asyncio için, daha eski bir Python sürümünü (yani> 3.5) desteklemeye gerek yoksa async def/ await, bir ortak program tanımlamak için önerilen sözdizimidir. Dolayısıyla yield fromartık bir koroutinde gerekli değildir.

Ancak genel olarak asyncio dışında , önceki cevapta belirtildiği gibi alt jeneratörüyield from <sub-generator> yinelemede başka bir kullanımı daha vardır.


1

Bu kod, fixed_sum_digitsrakamların toplamı 20 olacak şekilde altı basamaklı sayıların tümünü numaralandıran bir jeneratörü döndüren bir işlevi tanımlar .

def iter_fun(sum, deepness, myString, Total):
    if deepness == 0:
        if sum == Total:
            yield myString
    else:  
        for i in range(min(10, Total - sum + 1)):
            yield from iter_fun(sum + i,deepness - 1,myString + str(i),Total)

def fixed_sum_digits(digits, Tot):
    return iter_fun(0,digits,"",Tot) 

Onsuz yazmaya çalışın yield from. Etkili bir yol bulursanız bana bildirin.

Bunun gibi durumlar için: ağaçları ziyaret yield frometmek, kodu daha basit ve daha temiz hale getirdiğini düşünüyorum.


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.