Batayan

Mga generator sa Python: yield, iterator, at list at dictionary comprehension

Na-update: · Skillaria.Top

Ang generator sa Python ay bagay na naglalabas ng mga halaga isa-isa, kapag hiningi lang, nang hindi itinatago ang lahat sa memorya; ginagawa ito ng function na may keyword na yield o ng generator expression sa loob ng mga bilog na bracket. Ang mga list, dictionary, at set comprehension ay maikling paraan para bumuo ng handang koleksiyon sa isang linya: [x * 2 for x in xs], {k: v for k, v in pairs}, {x for x in xs}.

Ang generator sa Python ay bagay na naglalabas ng mga halaga nang isa-isa, kapag hiniling lang, at hindi itinatago ang buong sunod-sunod na datos sa memorya. Dalawang paraan ang paggawa nito: gamit ang function na may keyword na yield, o gamit ang generator expression sa loob ng mga bilog na bracket. Kasama nito ang mga "list comprehension", "dict comprehension", at "set comprehension" — maikling paraan para bumuo ng handang koleksiyon sa isang linya. Tatalakayin natin kung paano gumagana ang python yield, ano ang pagkakaiba ng generator at iterator, at paano hindi gawing hindi na mabasang linya ang comprehension.

Mga iterator: ang sandigan ng loop na for

Para maintindihan ang mga generator, kailangan munang unawain ang mga iterator. Ang iterable ay anumang bagay na maaaring daanan ng loop: listahan, string, dictionary, file. Ang iterator naman ay object na naaalala ang kasalukuyang posisyon, at sa bawat tawag ng next() ay nagbibigay ng susunod na elemento; kapag naubos na ang mga elemento, nagtataas ito ng StopIteration.

nums = [10, 20, 30]
it = iter(nums)      # kumuha ng iterator mula sa listahan
print(next(it))      # 10
print(next(it))      # 20
print(next(it))      # 30
next(it)             # StopIteration

Ginagawa mismo ng loop na for ang ganito: tinatawag nito ang iter(), at pagkatapos ang next() hanggang sa StopIteration. Maaaring gumawa ng sariling iterator gamit ang class na may mga method na __iter__ at __next__:

class Countdown:
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current <= 0:
            raise StopIteration
        value = self.current
        self.current -= 1
        return value

print(list(Countdown(3)))  # [3, 2, 1]

Gumagana ito, pero maraming paulit-ulit na code. Ginagawa ng generator ang parehong bagay sa tatlong linya.

Ano ang generator sa Python at paano gumagana ang yield

Ang generator function ay karaniwang function na may yield sa katawan nito. Hindi tumatakbo ang code kapag tinawag ito; nagbabalik ito ng generator object. Tatakbo ang katawan hanggang sa pinakamalapit na yield sa bawat next(), magbibigay ng halaga, at ifi-freeze ang function kasama ang mga lokal na variable nito. Ang susunod na next() ay magpapatuloy mismo sa parehong lugar.

def countdown(start):
    while start > 0:
        yield start
        start -= 1

gen = countdown(3)
print(gen)          # <generator object countdown at 0x...>
print(next(gen))    # 3
print(list(gen))    # [2, 1] — nagpatuloy mula sa huling hintuan

Kapag narating ng function ang dulo (o ang return), kusang magtataas ng StopIteration ang generator, at maayos na natatapos ang loop na for. Dito nakasalalay ang pagkakaiba ng yield at return: tinatapos ng return ang function at nagbabalik ng isang halaga; sinasuspinde ng yield ang function at maaaring maglabas ng maraming halaga.

Generator at iterator: mga pagkakaiba

Ang bawat generator ay iterator: mayroon itong __iter__ at __next__. Pero hindi bawat iterator ay generator. Ang pagkakaiba ay nasa paraan ng paggawa at sa mga kakayahan.

Iterator (class)Generator
Paano gawinclass na may __iter__ at __next__function na may yield, o (x for x in ...)
Estadomanu-manong nasa mga attributeawtomatikong naiimbak sa mga lokal na variable
StopIterationikaw ang nagtataaskusang lumilitaw kapag lumabas sa function
Karagdaganano ang isusulat mosend(), throw(), close()

Iisa ang mahalagang katangian ng pareho: isang beses lang sila madadaanan. Ang paulit-ulit na for sa generator na naubos na ay walang ibibigay — kailangang gumawa ng bago.

Walang hanggang generator at pagbasa ng malalaking file

Hindi kailangang magtapos ang generator. Ligtas ang walang hanggang sunod-sunod basta't limitado ang bilang ng kukunin mula rito:

from itertools import islice

def ids():
    n = 1
    while True:
        yield f"ID-{n:04d}"
        n += 1

print(list(islice(ids(), 3)))  # ['ID-0001', 'ID-0002', 'ID-0003']

Ang pinakapraktikal na gamit ay ang pagproseso ng malaking file nang linya-linya. Maaaring pagdugtungin ang mga generator sa isang pipeline, at sa bawat sandali, isang linya lang ang nasa memorya, hindi ang buong file:

def read_lines(path):
    with open(path, encoding="utf-8") as f:
        for line in f:
            yield line.rstrip("\n")

def only_errors(lines):
    for line in lines:
        if "ERROR" in line:
            yield line

for line in only_errors(read_lines("app.log")):
    print(line)

yield from at send

Ipinapasa ng yield from ang lahat ng halaga ng ibang iterable, kaya hindi na kailangan ng nested na loop:

def flatten(groups):
    for g in groups:
        yield from g

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

Ipinapadala ng method na send() ang halaga papasok sa generator: nagiging resulta ito ng ekspresyong yield. Bago ang unang send, kailangang "simulan" muna ang generator gamit ang next(). Ganito isinusulat, halimbawa, ang pang-ipon ng average:

def averager():
    total, count = 0, 0
    avg = None
    while True:
        value = yield avg
        total += value
        count += 1
        avg = total / count

a = averager()
next(a)            # umabot sa unang yield
print(a.send(10))  # 10.0
print(a.send(20))  # 15.0

Bihira ang send sa praktikal na code, pero madalas itong itanong sa mga job interview.

Ang range ba ay generator?

Hindi. Ang range ay lazy na sunod-sunod: hindi rin nito iniimbak ang lahat ng numero, pero sinusuportahan nito ang indexing, len(), pagsusuri gamit ang in, at maaaring daanan nang maraming beses. Wala sa mga ito ang kayang gawin ng generator.

r = range(5)
print(r[2], len(r), 3 in r)  # 2 5 True
print(list(r), list(r))      # parehong beses [0, 1, 2, 3, 4]

List comprehension: generator ng listahan

Ang list comprehension ay anyong [expression for x in collection]. Gumagawa ito ng bagong listahan at hindi binabago ang orihinal. Binabasa ito mula kaliwa pakanan: unang "ano ang ilalagay", pagkatapos "saan kukunin".

amounts = [100, 250, 400]

# karaniwang loop
with_markup = []
for a in amounts:
    with_markup.append(a * 1.2)

# pareho gamit ang comprehension
with_markup = [a * 1.2 for a in amounts]
print(with_markup)  # [120.0, 300.0, 480.0]

Sa kabila ng pangalang "generator ng listahan" sa orihinal na teksto, handang listahan ang resulta, hindi generator: kinakalkula agad ang lahat ng elemento.

Generator ng listahan na may if at if else

Dito kadalasang nagkakamali. Ang kondisyong filter ay inilalagay sa dulo at nag-aalis ng mga elemento. Ang kondisyong pagpili (ternary operator) ay inilalagay sa unahan at ginagawang isa sa dalawang halaga ang bawat elemento, nang hindi nag-aalis ng kahit ano.

amounts = [100, 250, 400]

big = [a for a in amounts if a > 200]                   # [250, 400]
labels = ["marami" if a > 200 else "kaunti" for a in amounts]
# ['kaunti', 'marami', 'marami']

# filter at pagpili nang magkasama
odd_marks = ["pares" if n % 2 == 0 else "di-pares" for n in range(6) if n > 2]
# ['di-pares', 'pares', 'di-pares']

Ang [a for a in amounts if a > 200 else 0] ay syntax error: ang else ay nasa kondisyon lamang sa unahan.

Nested na comprehension at dalawang-dimensiyong listahan

Ang dobleng for ay isinusulat sa parehong pagkakasunod ng nested na loop, mula itaas pababa. Nakatutulong ito para hindi malito sa pagdugtong ng listahan ng mga listahan:

groups = [[1, 2], [3], [4, 5]]
flat = [x for g in groups for x in g]
# katumbas ng:
# for g in groups:
#     for x in g:
#         flat.append(x)
print(flat)  # [1, 2, 3, 4, 5]

Ang dalawang-dimensiyong listahan (matrix) ay ginagawa gamit ang comprehension sa loob ng comprehension. Karaniwang pagkakamali ang pagmu-multiply ng listahan: ang mga row ay magiging iisang object.

bad = [[0] * 3] * 2
bad[0][0] = 1
print(bad)   # [[1, 0, 0], [1, 0, 0]] — nagbago ang dalawang row

grid = [[0] * 3 for _ in range(2)]
grid[0][0] = 1
print(grid)  # [[1, 0, 0], [0, 0, 0]]

# transpose
m = [[1, 2, 3], [4, 5, 6]]
print([[row[i] for row in m] for i in range(3)])  # [[1, 4], [2, 5], [3, 6]]

Dictionary at set comprehension

Pareho ang prinsipyo para sa ibang koleksiyon. Ang curly brace na may colon ay bumubuo ng dictionary (dict comprehension), at ang curly brace na walang colon ay bumubuo ng set (set comprehension).

spent = [("pagkain", 500), ("taksi", 300), ("pagkain", 200)]

categories = {cat for cat, _ in spent}
print(categories)  # {'pagkain', 'taksi'} — hindi ginagarantiya ang pagkakasunod ng set

last = {cat: amount for cat, amount in spent}
print(last)        # {'pagkain': 200, 'taksi': 300}

squares = {n: n ** 2 for n in range(4)}  # {0: 0, 1: 1, 2: 4, 3: 9}
swapped = {v: k for k, v in squares.items()}

Pansinin ang last: kapag inulit ang key, ang huling halaga ang nananaig, kaya nawala ang 500. Kung kailangang pagsamahin ang mga halaga, hindi angkop ang comprehension — sumulat ng loop, o gamitin ang collections.Counter o defaultdict.

Paano naman ang tuple comprehension?

Walang hiwalay na "tuple comprehension" sa Python. Ang mga bilog na bracket ay nagbibigay ng generator expression, at nakukuha ang tuple sa pamamagitan ng pagpasa nito sa tuple():

t = tuple(n * 2 for n in range(3))  # (0, 2, 4)

Generator expression: hindi nag-iimbak, kundi naglalabas

Kapag pinalitan ang mga square bracket ng bilog, nagiging generator expression ito. Wala itong kinakalkula nang maaga at naglalabas ng mga halaga isa-isa. Malaki ang pagkakaiba sa memorya kapag isang milyong elemento ang pinag-uusapan:

import sys

as_list = [n for n in range(1_000_000)]
as_gen = (n for n in range(1_000_000))
print(sys.getsizeof(as_list))  # mga 8 MB
print(sys.getsizeof(as_gen))   # ilang daang byte

Kadalasang nakatira ang generator expression sa loob ng mga aggregate function. Kapag ito ang nag-iisang argumento, maaaring huwag nang isulat ang dagdag na bracket. Ang any() at all() ay humihinto sa unang sagot at hindi na dumadaan sa natitira.

spent = [("pagkain", 500), ("taksi", 300), ("pagkain", 200)]

total = sum(a for _, a in spent)            # 1000
has_big = any(a > 400 for _, a in spent)    # True, hindi na tumingin pagkatapos ng 500
biggest = max(spent, key=lambda p: p[1])    # ('pagkain', 500)

Simple ang tuntunin sa pagpili: kung kailangan ang resulta nang maraming beses, may indexing, o kailangan ang len(), gumamit ng listahan. Kung kailangan lang dumaan nang isang beses at magkalkula, gumamit ng generator.

Saklaw ng variable: hindi tumatagas ang variable

Tanong na nakakalito kahit sa mga bihasa:

x = 10
squares = [x for x in range(3)]
print(x)  # 10

Ang variable ng loop sa loob ng comprehension ay nabubuhay sa sarili nitong saklaw at hindi nito pinapalitan ang panlabas na variable (ganito sa Python 3; iba ito sa Python 2). Sa karaniwang loop na for, pagkatapos nito ay magiging 2 ang x. Mula bersyon 3.12 (PEP 709), isinasama na ang mga comprehension sa nakapaligid na function at mas mabilis ang mga ito, pero nanatili ang tuntunin sa saklaw.

Unpacking gamit ang * at ** sa comprehension: bago sa Python 3.15 (PEP 798)

Matagal nang maaaring gamitin ang asterisk para sa unpacking sa tawag ng function at sa literal ng listahan, pero sa loob ng comprehension, SyntaxError ito noon. Sa Python 3.15, inalis ang pagbabawal: ang *elemento ay nangangahulugang "ilagay dito ang lahat ng elemento nito". Gumagana ito sa mga listahan, set, at generator, pati sa mga asynchronous.

# Python 3.15+
groups = [[1, 2], [3], [4, 5]]
flat = [*g for g in groups]        # [1, 2, 3, 4, 5]
uniq = {*g for g in groups}        # set
lazy = (*g for g in groups)        # lazy na generator

Ginagawa rin ng double asterisk ang parehong bagay para sa mga dictionary. Kapaki-pakinabang ito para sa mga patong-patong na setting na "default → file → flag": tinatakpan ng bawat susunod na patong ang mga key ng nauna.

# Python 3.15+
layers = [{"debug": False, "port": 80}, {"port": 8080}, {"debug": True}]
config = {**layer for layer in layers}
# {'debug': True, 'port': 8080}

Dalawang paalala mula sa kurso. Hindi maaaring pagsamahin ang asterisk at ang conditional expression nang walang panaklong: ang [*w if w else 0 for w in weeks] ay syntax error; kailangang ilagay sa panaklong ang kondisyon. At hindi tatakbo ang ganitong code sa 3.14 pababa: kung sinusuportahan ng library ang mas lumang bersyon, manatili sa dobleng for o sa itertools.chain.from_iterable.

Asynchronous na generator

Kapag may yield ang function na async def, nagiging asynchronous generator ito. Dinadaanan ito gamit ang async for, at sa loob nito ay maaaring maghintay ng network o disk gamit ang await. May asynchronous comprehension din.

import asyncio

async def ticks(n):
    for i in range(n):
        await asyncio.sleep(0.1)
        yield i

async def main():
    result = [i async for i in ticks(3)]
    print(result)  # [0, 1, 2]

asyncio.run(main())

Karaniwang pagkakamali

  • Comprehension na tatlong palapag. Dalawang for, dalawang if, at ternary sa iisang linya — walang makakabasa nito pagkalipas ng isang linggo. Kapag hindi kasya sa isang linya, sumulat ng karaniwang loop: mas inuuna ng PEP 8 ang kalinawan kaysa sa ikli.
  • Pag-ulit sa generator nang dalawang beses. Ang pangalawang sum(gen) ay magbabalik ng 0, dahil naubos na ang generator.
  • Comprehension para sa side effect. Ang [print(x) for x in xs] ay bumubuo ng hindi kailangang listahan ng None. Para sa mga aksyon, gamitin ang loop na for.
  • Pag-asa ng listahan mula sa generator expression. Ang (x for x in xs)[0] ay TypeError: walang index ang generator.

Mga gawain sa generator para sa praktika

  1. Sumulat ng generator na fib() na walang hanggang naglalabas ng mga numerong Fibonacci, at kunin ang unang 10 gamit ang islice.
  2. Sa isang linya, kumuha ng dictionary na "salita → haba" para sa mga salitang mas mahaba sa tatlong letra.
  3. Kalkulahin ang kabuuan ng mga parisukat ng mga pares na numero hanggang isang milyon, nang hindi gumagawa ng listahan.
  4. Sumulat ng generator na chunks(seq, n) na naglalabas ng mga piraso ng listahan na tig-n elemento.

Buod

Iniisa-isa ng iterator ang mga elemento gamit ang next(), at ang generator ang pinakasimpleng paraan para gumawa nito: function na may python yield, o expression sa mga bilog na bracket. Bumubuo ang list, dictionary, at set comprehension ng handang koleksiyon sa isang linya; ang filter na if ay nasa dulo, at ang pagpili na if else ay nasa unahan. Kapag kailangang tipirin ang memorya o isang beses lang iproseso ang datos, gamitin ang generator; kapag kailangan ang resulta nang maraming beses, gamitin ang listahan. Ang mga comprehension, generator, at ang bagong unpacking sa Python 3.15 ay tinalakay nang praktikal sa kurso, gamit ang tagasubaybay ng gastos.

Mga madalas itanong

Maaari bang gumamit ng return sa generator?

Oo. Ang return na walang halaga ay tumatapos lang sa generator; ang return na may halaga ay inilalagay ito sa attribute na value ng StopIteration, at ang halagang iyon ang natatanggap ng ekspresyong yield from.

Paano gawing listahan ang generator?

Ipasa ito sa list(): list(gen). Pagkatapos nito, naubos na ang generator, at nasa memorya na ang lahat ng halaga.

Alin ang mas mabilis, list comprehension o loop na may append?

Kadalasan bahagyang mas mabilis ang comprehension dahil hindi nito tinatawag ang append sa bawat ulit. Pero piliin ito ayon sa kalinawan ng code; bihira maging mapagpasya ang pagkakaiba sa bilis.

Paano malalaman kung generator ang isang object?

Gamitin ang inspect.isgenerator(obj) para sa object, o inspect.isgeneratorfunction(f) para sa function; para malaman kung iterator ang object, gamitin ang isinstance(obj, collections.abc.Iterator).

Ano ang pagkakaiba ng generator expression at list comprehension?

Agad na binubuo ng mga square bracket ang buong listahan sa memorya; ang mga bilog na bracket ay gumagawa ng generator na kumakalkula ng mga elemento kapag hiningi at dumadaan lamang nang isang beses.

Pag-aralan ito sa video

Detalyadong tinatalakay ang paksang ito, may praktis, sa kursong “Python 3.15: от новичка до профи”.

Pumunta sa kurso