Dalam praktiknya, apa kegunaan utama sintaks “hasil dari” baru dalam Python 3.3?

407

Saya mengalami kesulitan membungkus otak saya di sekitar PEP 380 .

  1. Apa situasi di mana "hasil dari" berguna?
  2. Apa kasus penggunaan klasik?
  3. Mengapa dibandingkan dengan micro-threads?

[perbarui]

Sekarang saya mengerti penyebab kesulitan saya. Saya telah menggunakan generator, tetapi tidak pernah benar-benar menggunakan coroutine (diperkenalkan oleh PEP-342 ). Meskipun ada beberapa kesamaan, generator dan coroutine pada dasarnya adalah dua konsep yang berbeda. Memahami coroutine (bukan hanya generator) adalah kunci untuk memahami sintaks baru.

Coroutine IMHO adalah fitur Python yang paling tidak jelas , sebagian besar buku membuatnya tampak tidak berguna dan tidak menarik.

Terima kasih atas jawaban yang luar biasa, tetapi terima kasih khusus kepada agf dan komentarnya yang menghubungkan dengan presentasi David Beazley . David batu.

Paulo Scardine
sumber
7
Video presentasi dabeaz.com/coroutines David Beazley : youtube.com/watch?v=Z_OAlIhXziw
jcugat

Jawaban:

571

Mari kita menyingkir dulu. Penjelasan yang yield from gsetara dengan for v in g: yield v bahkan tidak mulai melakukan keadilan untuk apa yield fromsemua tentang. Karena, mari kita hadapi itu, jika semua yield frommemang memperluas forloop, maka itu tidak menjamin menambah yield frombahasa dan menghalangi sejumlah fitur baru diimplementasikan dalam Python 2.x.

Apa yang yield fromdilakukan adalah membuat koneksi dua arah transparan antara pemanggil dan sub-generator :

  • Koneksi "transparan" dalam arti bahwa itu akan menyebarkan semuanya dengan benar juga, bukan hanya elemen yang dihasilkan (mis. Pengecualian diperbanyak).

  • Koneksi adalah "dua arah" dalam arti bahwa data dapat dikirim dari dan ke generator.

( Jika kami berbicara tentang TCP, yield from gmungkin berarti "sekarang lepaskan sementara soket klien saya dan sambungkan kembali ke soket server lain ini". )

BTW, jika Anda tidak yakin apa artinya mengirim data ke generator , Anda harus meninggalkan semuanya dan membaca tentang coroutine terlebih dahulu — mereka sangat berguna (kontraskan dengan subrutin ), tetapi sayangnya kurang dikenal dengan Python. Kursus Curious karya Dave Beazley tentang Coroutines adalah awal yang baik. Baca slide 24-33 untuk primer cepat.

Membaca data dari generator menggunakan hasil dari

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

Alih-alih secara manual mengulangi reader(), kita bisa yield frommelakukannya.

def reader_wrapper(g):
    yield from g

Itu berhasil, dan kami menghilangkan satu baris kode. Dan mungkin maksudnya sedikit lebih jelas (atau tidak). Tapi tidak ada kehidupan yang berubah.

Mengirim data ke generator (coroutine) menggunakan hasil dari - Bagian 1

Sekarang mari kita lakukan sesuatu yang lebih menarik. Mari kita buat coroutine yang disebut writeryang menerima data yang dikirim kepadanya dan menulis ke soket, fd, dll.

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

Sekarang pertanyaannya adalah, bagaimana seharusnya fungsi pembungkus menangani pengiriman data ke penulis, sehingga setiap data yang dikirim ke pembungkus dikirim secara transparan ke 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

Pembungkus perlu menerima data yang dikirim ke sana (jelas) dan juga harus menangani StopIterationketika loop for habis. Jelas hanya melakukan for x in coro: yield xtidak akan berhasil. Ini adalah versi yang berfungsi.

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

Atau, kita bisa melakukan ini.

def writer_wrapper(coro):
    yield from coro

Itu menghemat 6 baris kode, membuatnya jauh lebih mudah dibaca dan hanya berfungsi. Sihir!

Mengirim data ke generator hasil dari - Bagian 2 - Penanganan pengecualian

Mari kita buat lebih rumit. Bagaimana jika penulis kita perlu menangani pengecualian? Katakanlah writergagang a SpamExceptiondan mencetak ***jika bertemu satu.

class SpamException(Exception):
    pass

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

Bagaimana jika kita tidak berubah writer_wrapper? Apakah itu bekerja? Mari mencoba

# 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, itu tidak berfungsi karena x = (yield)hanya menimbulkan pengecualian dan semuanya terhenti. Mari kita membuatnya bekerja, tetapi secara manual menangani pengecualian dan mengirimkannya atau melemparkannya ke sub-generator ( 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

Ini bekerja.

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

Tapi begitu juga ini!

def writer_wrapper(coro):
    yield from coro

Secara yield fromtransparan menangani pengiriman nilai-nilai atau melemparkan nilai-nilai ke dalam sub-generator.

Ini masih belum mencakup semua kasus sudut. Apa yang terjadi jika generator luar ditutup? Bagaimana dengan kasus ketika sub-generator mengembalikan nilai (ya, dengan Python 3.3+, generator dapat mengembalikan nilai), bagaimana seharusnya nilai pengembalian disebarkan? Yang yield fromtransparan menangani semua kasus sudut sangat mengesankan . yield fromhanya bekerja secara ajaib dan menangani semua kasus itu.

Saya pribadi merasa yield fromadalah pilihan kata kunci yang buruk karena tidak membuat sifat dua arah menjadi jelas. Ada kata kunci lain yang diusulkan (seperti delegatetetapi ditolak karena menambahkan kata kunci baru ke bahasa ini jauh lebih sulit daripada menggabungkan yang sudah ada.

Singkatnya, yang terbaik yield fromadalah transparent two way channelantara penelepon dan sub-generator.

Referensi:

  1. PEP 380 - Sintaks untuk mendelegasikan ke sub-generator (Ewing) [v3.3, 2009-02-13]
  2. PEP 342 - Coroutine via Enhanced Generator (GvR, Eby) [v2.5, 2005-05-10]
Praveen Gollakota
sumber
3
@PraveenGollakota, di bagian kedua pertanyaan Anda, Mengirim data ke generator (coroutine) menggunakan hasil dari - Bagian 1 , bagaimana jika Anda memiliki lebih dari coroutine untuk meneruskan barang yang diterima? Seperti skenario penyiar atau pelanggan di mana Anda memberikan beberapa coroutine kepada pembungkus dalam contoh Anda dan barang harus dikirim ke semua atau sebagian dari mereka?
Kevin Ghaboosi
3
@PraveenGollakota, Kudos untuk jawaban yang bagus. Contoh-contoh kecil biarkan saya mencoba hal-hal sebagai balasan. Tautan ke kursus Dave Beazley adalah bonus!
BiGYaN
1
melakukan except StopIteration: passDI DALAM while True:loop bukan representasi akurat yield from coro- yang bukan loop tak terbatas dan setelah corohabis (yaitu menimbulkan StopIteration), writer_wrapperakan menjalankan pernyataan berikutnya. Setelah pernyataan terakhir, ia akan otomatis dinaikkan StopIterationkarena generator yang kelelahan ...
Aprillion
1
... jadi jika writerterkandung for _ in range(4)alih-alih while True, maka setelah mencetaknya >> 3JUGA akan meningkatkan otomatis StopIterationdan ini akan ditangani secara otomatis oleh yield fromdan kemudian writer_wrapperakan otomatis meningkatkan itu sendiri StopIterationdan karena wrap.send(i)tidak di dalam tryblok, itu akan benar-benar dinaikkan pada titik ini ( yaitu traceback hanya akan melaporkan saluran dengan wrap.send(i), bukan apa pun dari dalam generator)
Aprillion
3
Setelah membaca " bahkan tidak mulai melakukan keadilan ", saya tahu saya telah sampai pada jawaban yang benar. Terima kasih atas penjelasannya!
Hot.PxL
89

Apa situasi di mana "hasil dari" berguna?

Setiap situasi di mana Anda memiliki lingkaran seperti ini:

for x in subgenerator:
  yield x

Seperti yang dijelaskan oleh PEP, ini adalah upaya yang agak naif dalam menggunakan subgenerator, ini kehilangan beberapa aspek, terutama penanganan mekanisme .throw()/ .send()/ .close()yang diperkenalkan oleh PEP 342 . Untuk melakukan ini dengan benar, kode yang agak rumit diperlukan.

Apa kasus penggunaan klasik?

Pertimbangkan bahwa Anda ingin mengekstrak informasi dari struktur data rekursif. Katakanlah kita ingin mendapatkan semua simpul daun di pohon:

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

Yang lebih penting adalah kenyataan bahwa sampai saat itu yield from, tidak ada metode sederhana untuk refactoring kode generator. Misalkan Anda memiliki generator (tidak masuk akal) seperti ini:

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)

Sekarang Anda memutuskan untuk memfaktorkan loop ini ke generator terpisah. Tanpa yield from, ini jelek, sampai pada titik di mana Anda akan berpikir dua kali apakah Anda benar-benar ingin melakukannya. Dengan yield from, sebenarnya menyenangkan untuk melihat:

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)

Mengapa dibandingkan dengan micro-threads?

Saya pikir apa yang dibicarakan oleh bagian dalam PEP ini adalah bahwa setiap generator memiliki konteks eksekusi tersendiri. Bersama dengan fakta bahwa eksekusi diaktifkan antara generator-iterator dan pemanggil menggunakan yielddan __next__(), masing-masing, ini mirip dengan utas, di mana sistem operasi mengalihkan utas pelaksana dari waktu ke waktu, bersama dengan konteks eksekusi (stack, register, ...).

Efek dari ini juga sebanding: Baik generator-iterator dan pemanggil mengalami kemajuan dalam kondisi eksekusi mereka pada saat yang sama, eksekusi mereka saling terkait. Misalnya, jika generator melakukan semacam perhitungan dan penelepon mencetak hasilnya, Anda akan melihat hasilnya segera setelah tersedia. Ini adalah bentuk konkurensi.

Analogi itu bukan sesuatu yang spesifik yield from, - ini adalah properti umum dari generator dengan Python.

Niklas B.
sumber
Generator Refactoring menyakitkan hari ini.
Josh Lee
1
Saya cenderung menggunakan itertools banyak untuk refactoring generator (hal-hal seperti itertools.chain), itu bukan masalah besar. Saya suka hasil dari, tetapi saya masih gagal melihat betapa revolusionernya itu. Mungkin, karena Guido sangat tergila-gila dengan itu, tapi aku pasti kehilangan gambaran besarnya. Saya kira itu bagus untuk mengirim () karena ini sulit untuk refactor, tapi saya jarang menggunakannya.
e-satis
Saya kira itu get_list_values_as_xxxadalah generator sederhana dengan satu baris for x in input_param: yield int(x)dan dua lainnya masing-masing dengan strdanfloat
madtyn
@ NiklasB. "mengekstrak informasi dari struktur data rekursif." Saya baru saja masuk ke Py untuk data. Bisakah Anda menikam Q ini ?
alancalvitti
33

Di mana pun Anda menjalankan generator dari dalam generator Anda memerlukan "memompa" untuk kembali yieldnilai-nilai: for v in inner_generator: yield v. Seperti yang ditunjukkan oleh PEP, ada kerumitan halus yang diabaikan oleh kebanyakan orang. Kontrol aliran non-lokal seperti throw()adalah salah satu contoh yang diberikan dalam PEP. Sintaks baru yield from inner_generatordigunakan di mana pun Anda akan menulis forloop eksplisit sebelumnya. Ini bukan hanya gula sintaksis, ia menangani semua kotak sudut yang diabaikan oleh forloop. Menjadi "manis" mendorong orang untuk menggunakannya dan dengan demikian mendapatkan perilaku yang benar.

Pesan ini dalam utas diskusi berbicara tentang kompleksitas ini:

Dengan fitur generator tambahan yang diperkenalkan oleh PEP 342, itu tidak lagi terjadi: seperti yang dijelaskan dalam PE's Greg, iterasi sederhana tidak mendukung send () dan throw () dengan benar. Senam yang dibutuhkan untuk mendukung send () dan throw () sebenarnya tidak terlalu rumit ketika Anda memecahnya, tetapi mereka juga tidak sepele.

Saya tidak dapat berbicara dengan perbandingan dengan micro-threads, selain untuk mengamati bahwa generator adalah jenis paralellism. Anda dapat menganggap generator yang ditangguhkan sebagai utas yang mengirimkan nilai melalui yieldutas konsumen. Implementasi aktual mungkin tidak seperti ini (dan implementasi sebenarnya jelas sangat menarik bagi pengembang Python) tetapi ini tidak menjadi perhatian pengguna.

yield fromSintaks baru tidak menambahkan kemampuan tambahan ke bahasa dalam hal threading, itu hanya membuatnya lebih mudah untuk menggunakan fitur yang ada dengan benar. Atau lebih tepatnya hal itu membuatnya lebih mudah bagi konsumen pemula dari generator batin yang kompleks yang ditulis oleh seorang ahli untuk melewati generator itu tanpa merusak salah satu fitur kompleksnya.

Ben Jackson
sumber
23

Contoh singkat akan membantu Anda memahami salah satu yield fromuse case: dapatkan nilai dari generator lain

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]]])))
ospider
sumber
2
Hanya ingin menyarankan bahwa hasil cetak pada akhirnya akan terlihat sedikit lebih baik tanpa konversi ke daftar -print(*flatten([1, [2], [3, [4]]]))
yoniLavi
6

yield from pada dasarnya rantai iterator dengan cara yang efisien:

# 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

Seperti yang Anda lihat, ia menghapus satu loop Python murni. Itu cukup banyak, tetapi rantai iterator adalah pola yang cukup umum di Python.

Utas pada dasarnya adalah fitur yang memungkinkan Anda untuk melompat keluar dari fungsi pada titik yang benar-benar acak dan melompat kembali ke keadaan fungsi lainnya. Pengawas utas melakukan ini sangat sering, sehingga program tampaknya menjalankan semua fungsi ini secara bersamaan. Masalahnya adalah bahwa poin-poin itu acak, sehingga Anda perlu menggunakan penguncian untuk mencegah pengawas menghentikan fungsi pada titik yang bermasalah.

Generator sangat mirip dengan utas dalam hal ini: Generator memungkinkan Anda menentukan titik tertentu (kapan pun mereka yield) di mana Anda dapat melompat masuk dan keluar. Ketika digunakan dengan cara ini, generator disebut coroutine.

Baca tutorial luar biasa ini tentang coroutine dengan Python untuk lebih jelasnya

Jochen Ritzel
sumber
10
Jawaban ini menyesatkan karena menghilangkan fitur menonjol "hasil dari", seperti yang disebutkan di atas: kirim () dan lempar () dukungan.
Justin W
2
@Justin W: Saya kira apa pun yang Anda baca sebelumnya sebenarnya menyesatkan, karena Anda tidak mendapatkan poin yang throw()/send()/close()merupakan yieldfitur yang yield fromjelas harus diimplementasikan dengan benar karena seharusnya menyederhanakan kode. Hal-hal sepele seperti itu tidak ada hubungannya dengan penggunaan.
Jochen Ritzel
5
Apakah Anda membantah jawaban Ben Jackson di atas? Bacaan saya atas jawaban Anda adalah bahwa itu pada dasarnya adalah gula sintaksis yang mengikuti transformasi kode yang Anda berikan. Jawaban Ben Jackson secara khusus membantah klaim itu.
Justin W
@JochenRitzel Anda tidak perlu menulis chainfungsi Anda sendiri karena itertools.chainsudah ada. Gunakan yield from itertools.chain(*iters).
Acumenus
5

Dalam penggunaan yang diterapkan untuk Asynchronous IO coroutine , yield frommemiliki perilaku yang sama seperti awaitpada fungsi coroutine . Keduanya digunakan untuk menunda eksekusi coroutine.

Untuk Asyncio, jika tidak perlu mendukung versi Python yang lebih lama (yaitu> 3.5), async def/ awaitadalah sintaks yang disarankan untuk mendefinisikan coroutine. Dengan demikian yield fromtidak diperlukan lagi dalam coroutine.

Tetapi secara umum di luar asyncio, yield from <sub-generator>masih ada beberapa penggunaan lain dalam iterasi sub-generator seperti yang disebutkan dalam jawaban sebelumnya.

Yeo
sumber
1

Kode ini mendefinisikan fungsi fixed_sum_digitsmengembalikan generator yang menghitung semua angka enam digit sehingga jumlah digit adalah 20.

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) 

Cobalah untuk menulisnya tanpa yield from. Jika Anda menemukan cara yang efektif untuk melakukannya, beri tahu saya.

Saya pikir untuk kasus seperti ini: mengunjungi pohon, yield frommembuat kodenya lebih mudah dan bersih.

jimifiki
sumber
0

Sederhananya, yield frommemberikan rekursi ekor untuk fungsi iterator.

DomQ
sumber