Python: ekspresi generator vs. hasil

90

Di Python, apakah ada perbedaan antara membuat objek generator melalui ekspresi generator versus menggunakan pernyataan yield ?

Menggunakan hasil :

def Generator(x, y):
    for i in xrange(x):
        for j in xrange(y):
            yield(i, j)

Menggunakan ekspresi generator :

def Generator(x, y):
    return ((i, j) for i in xrange(x) for j in xrange(y))

Kedua fungsi tersebut mengembalikan objek generator, yang menghasilkan tupel, misalnya (0,0), (0,1) dll.

Adakah keuntungan dari satu atau lainnya? Pikiran?


Terimakasih semuanya! Ada banyak informasi bagus dan referensi lebih lanjut dalam jawaban ini!

cschol.dll
sumber
2
Pilih salah satu yang menurut Anda paling mudah dibaca.
pengguna238424

Jawaban:

74

Hanya ada sedikit perbedaan di antara keduanya. Anda dapat menggunakan dismodul untuk memeriksa sendiri hal semacam ini.

Edit: Versi pertama saya mendekompilasi ekspresi generator yang dibuat di module-scope di prompt interaktif. Itu sedikit berbeda dari versi OP yang digunakan di dalam fungsi. Saya telah mengubah ini agar sesuai dengan kasus sebenarnya dalam pertanyaan.

Seperti yang Anda lihat di bawah, generator "yield" (kasus pertama) memiliki tiga instruksi tambahan dalam penyiapan, tetapi dari yang pertama FOR_ITERmereka hanya berbeda dalam satu hal: pendekatan "yield" menggunakan a LOAD_FASTsebagai pengganti LOAD_DEREFdi dalam loop. Ini LOAD_DEREFadalah "agak lebih lambat" daripada LOAD_FAST, sehingga membuat versi "hasil" sedikit lebih cepat daripada ekspresi generator untuk nilai yang cukup besar dari x(loop luar) karena nilai dari ydimuat sedikit lebih cepat pada setiap lintasan. Untuk nilai yang lebih kecil xakan sedikit lebih lambat karena biaya tambahan dari kode pengaturan.

Mungkin juga penting untuk menunjukkan bahwa ekspresi generator biasanya akan digunakan sebaris dalam kode, daripada membungkusnya dengan fungsi seperti itu. Hal itu akan menghapus sedikit overhead penyiapan dan menjaga ekspresi generator sedikit lebih cepat untuk nilai loop yang lebih kecil bahkan jika LOAD_FASTversi "yield" memberikan keuntungan sebaliknya.

Dalam kedua kasus, perbedaan kinerja tidak akan cukup untuk membenarkan keputusan antara satu atau lainnya. Keterbacaan jauh lebih penting, jadi gunakan mana saja yang dirasa paling mudah dibaca untuk situasi yang dihadapi.

>>> def Generator(x, y):
...     for i in xrange(x):
...         for j in xrange(y):
...             yield(i, j)
...
>>> dis.dis(Generator)
  2           0 SETUP_LOOP              54 (to 57)
              3 LOAD_GLOBAL              0 (xrange)
              6 LOAD_FAST                0 (x)
              9 CALL_FUNCTION            1
             12 GET_ITER
        >>   13 FOR_ITER                40 (to 56)
             16 STORE_FAST               2 (i)

  3          19 SETUP_LOOP              31 (to 53)
             22 LOAD_GLOBAL              0 (xrange)
             25 LOAD_FAST                1 (y)
             28 CALL_FUNCTION            1
             31 GET_ITER
        >>   32 FOR_ITER                17 (to 52)
             35 STORE_FAST               3 (j)

  4          38 LOAD_FAST                2 (i)
             41 LOAD_FAST                3 (j)
             44 BUILD_TUPLE              2
             47 YIELD_VALUE
             48 POP_TOP
             49 JUMP_ABSOLUTE           32
        >>   52 POP_BLOCK
        >>   53 JUMP_ABSOLUTE           13
        >>   56 POP_BLOCK
        >>   57 LOAD_CONST               0 (None)
             60 RETURN_VALUE
>>> def Generator_expr(x, y):
...    return ((i, j) for i in xrange(x) for j in xrange(y))
...
>>> dis.dis(Generator_expr.func_code.co_consts[1])
  2           0 SETUP_LOOP              47 (to 50)
              3 LOAD_FAST                0 (.0)
        >>    6 FOR_ITER                40 (to 49)
              9 STORE_FAST               1 (i)
             12 SETUP_LOOP              31 (to 46)
             15 LOAD_GLOBAL              0 (xrange)
             18 LOAD_DEREF               0 (y)
             21 CALL_FUNCTION            1
             24 GET_ITER
        >>   25 FOR_ITER                17 (to 45)
             28 STORE_FAST               2 (j)
             31 LOAD_FAST                1 (i)
             34 LOAD_FAST                2 (j)
             37 BUILD_TUPLE              2
             40 YIELD_VALUE
             41 POP_TOP
             42 JUMP_ABSOLUTE           25
        >>   45 POP_BLOCK
        >>   46 JUMP_ABSOLUTE            6
        >>   49 POP_BLOCK
        >>   50 LOAD_CONST               0 (None)
             53 RETURN_VALUE
Peter Hansen
sumber
Diterima - untuk penjelasan rinci tentang perbedaan menggunakan dis. Terima kasih!
cschol
Saya memperbarui untuk menyertakan tautan ke sumber yang mengklaim bahwa LOAD_DEREF"agak lambat", jadi jika kinerja benar-benar penting, beberapa waktu nyata dengan timeitakan bagus. Analisis teoretis hanya berlaku sejauh ini.
Peter Hansen
36

Dalam contoh ini, tidak juga. Tetapi yielddapat digunakan untuk konstruksi yang lebih kompleks - misalnya ia dapat menerima nilai dari pemanggil juga dan sebagai hasilnya mengubah aliran. Bacalah PEP 342 untuk lebih jelasnya (ini adalah teknik menarik yang perlu diketahui).

Bagaimanapun, saran terbaik adalah menggunakan apa pun yang lebih jelas untuk kebutuhan Anda .

PS Berikut contoh coroutine sederhana dari Dave Beazley :

def grep(pattern):
    print "Looking for %s" % pattern
    while True:
        line = (yield)
        if pattern in line:
            print line,

# Example use
if __name__ == '__main__':
    g = grep("python")
    g.next()
    g.send("Yeah, but no, but yeah, but no")
    g.send("A series of tubes")
    g.send("python generators rock!")
Eli Bendersky
sumber
8
1 untuk ditautkan ke David Beazley. Presentasinya tentang coroutine adalah hal paling menakjubkan yang pernah saya baca dalam waktu yang lama. Mungkin tidak begitu berguna seperti presentasinya tentang generator, tapi tetap mengagumkan.
Robert Rossney
18

Tidak ada perbedaan untuk jenis loop sederhana yang dapat Anda masukkan ke dalam ekspresi generator. Namun hasil dapat digunakan untuk membuat generator yang melakukan pemrosesan yang jauh lebih kompleks. Berikut adalah contoh sederhana untuk menghasilkan urutan fibonacci:

>>> def fibgen():
...    a = b = 1
...    while True:
...        yield a
...        a, b = b, a+b

>>> list(itertools.takewhile((lambda x: x<100), fibgen()))
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
Dave Kirby
sumber
5
+1 yang sangat keren ... tidak bisa mengatakan saya pernah melihat implementasi fib yang singkat dan manis tanpa rekursi.
JudoWill
Potongan kode sederhana yang menipu - Saya pikir Fibonacci akan senang melihatnya !!
pengguna-asterix
10

Dalam penggunaannya, perhatikan perbedaan antara objek generator vs fungsi generator.

Objek generator hanya digunakan sekali, berbeda dengan fungsi generator, yang bisa digunakan kembali setiap kali Anda memanggilnya lagi, karena mengembalikan objek generator baru.

Ekspresi generator dalam praktiknya biasanya menggunakan "mentah", tanpa membungkusnya dalam sebuah fungsi, dan mereka mengembalikan objek generator.

Misalnya:

def range_10_gen_func():
    x = 0
    while x < 10:
        yield x
        x = x + 1

print(list(range_10_gen_func()))
print(list(range_10_gen_func()))
print(list(range_10_gen_func()))

keluaran yang mana:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Bandingkan dengan penggunaan yang sedikit berbeda:

range_10_gen = range_10_gen_func()
print(list(range_10_gen))
print(list(range_10_gen))
print(list(range_10_gen))

keluaran yang mana:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[]
[]

Dan bandingkan dengan ekspresi generator:

range_10_gen_expr = (x for x in range(10))
print(list(range_10_gen_expr))
print(list(range_10_gen_expr))
print(list(range_10_gen_expr))

yang juga menghasilkan:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[]
[]
Craig McQueen
sumber
8

Penggunaannya yieldbagus jika ekspresi lebih rumit daripada sekadar loop bersarang. Antara lain Anda dapat mengembalikan nilai pertama khusus atau nilai terakhir khusus. Mempertimbangkan:

def Generator(x):
  for i in xrange(x):
    yield(i)
  yield(None)
Tor Valamo
sumber
5

Saat memikirkan iterator, itertoolsmodul:

... menstandarkan seperangkat inti alat yang cepat dan hemat memori yang berguna sendiri atau dalam kombinasi. Bersama-sama, mereka membentuk "aljabar iterator" sehingga memungkinkan untuk membangun alat khusus secara ringkas dan efisien dalam Python murni.

Untuk performa, pertimbangkan itertools.product(*iterables[, repeat])

Produk Cartesian dari input iterable.

Setara dengan loop-for bersarang dalam ekspresi generator. Misalnya, product(A, B)mengembalikan sama seperti ((x,y) for x in A for y in B).

>>> import itertools
>>> def gen(x,y):
...     return itertools.product(xrange(x),xrange(y))
... 
>>> [t for t in gen(3,2)]
[(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1)]
>>> 
gimel
sumber
4

Ya ada bedanya.

Untuk ekspresi generator (x for var in expr), iter(expr)dipanggil saat ekspresi dibuat .

Saat menggunakan defdan yieldmembuat generator, seperti dalam:

def my_generator():
    for var in expr:
        yield x

g = my_generator()

iter(expr)belum dipanggil. Ini akan dipanggil hanya saat iterasi g(dan mungkin tidak dipanggil sama sekali).

Mengambil iterator ini sebagai contoh:

from __future__ import print_function


class CountDown(object):
    def __init__(self, n):
        self.n = n

    def __iter__(self):
        print("ITER")
        return self

    def __next__(self):
        if self.n == 0:
            raise StopIteration()
        self.n -= 1
        return self.n

    next = __next__  # for python2

Kode ini:

g1 = (i ** 2 for i in CountDown(3))  # immediately prints "ITER"
print("Go!")
for x in g1:
    print(x)

sementara:

def my_generator():
    for i in CountDown(3):
        yield i ** 2


g2 = my_generator()
print("Go!")
for x in g2:  # "ITER" is only printed here
    print(x)

Karena sebagian besar iterator tidak melakukan banyak hal __iter__, mudah untuk melewatkan perilaku ini. Contoh dunia nyata adalah Django QuerySet, yang mengambil data__iter__ dan data = (f(x) for x in qs)mungkin membutuhkan banyak waktu, sementara def g(): for x in qs: yield f(x)diikuti oleh data=g()akan segera kembali.

Untuk info lebih lanjut dan definisi formal merujuk ke PEP 289 - Ekspresi Generator .

Udi
sumber
0

Ada perbedaan yang mungkin penting dalam beberapa konteks yang belum dikemukakan. Menggunakan yieldmencegah Anda menggunakan returnuntuk hal lain selain secara implisit memunculkan StopIteration (dan hal-hal terkait coroutine) .

Ini berarti kode ini salah format (dan memberikannya kepada penerjemah akan memberi Anda AttributeError):

class Tea:

    """With a cloud of milk, please"""

    def __init__(self, temperature):
        self.temperature = temperature

def mary_poppins_purse(tea_time=False):
    """I would like to make one thing clear: I never explain anything."""
    if tea_time:
        return Tea(355)
    else:
        for item in ['lamp', 'mirror', 'coat rack', 'tape measure', 'ficus']:
            yield item

print(mary_poppins_purse(True).temperature)

Di sisi lain, kode ini berfungsi seperti pesona:

class Tea:

    """With a cloud of milk, please"""

    def __init__(self, temperature):
        self.temperature = temperature

def mary_poppins_purse(tea_time=False):
    """I would like to make one thing clear: I never explain anything."""
    if tea_time:
        return Tea(355)
    else:
        return (item for item in ['lamp', 'mirror', 'coat rack',
                                  'tape measure', 'ficus'])

print(mary_poppins_purse(True).temperature)
Adrien
sumber