Mga generator sa Python: yield, iterator, at list at dictionary comprehension
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 gawin | class na may __iter__ at __next__ | function na may yield, o (x for x in ...) |
| Estado | manu-manong nasa mga attribute | awtomatikong naiimbak sa mga lokal na variable |
| StopIteration | ikaw ang nagtataas | kusang lumilitaw kapag lumabas sa function |
| Karagdagan | ano ang isusulat mo | send(), 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, dalawangif, 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 ngNone. Para sa mga aksyon, gamitin ang loop nafor. - Pag-asa ng listahan mula sa generator expression. Ang
(x for x in xs)[0]ayTypeError: walang index ang generator.
Mga gawain sa generator para sa praktika
- Sumulat ng generator na
fib()na walang hanggang naglalabas ng mga numerong Fibonacci, at kunin ang unang 10 gamit angislice. - Sa isang linya, kumuha ng dictionary na "salita → haba" para sa mga salitang mas mahaba sa tatlong letra.
- Kalkulahin ang kabuuan ng mga parisukat ng mga pares na numero hanggang isang milyon, nang hindi gumagawa ng listahan.
- Sumulat ng generator na
chunks(seq, n)na naglalabas ng mga piraso ng listahan na tig-nelemento.
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.
Detalyadong tinatalakay ang paksang ito, may praktis, sa kursong “Python 3.15: от новичка до профи”.
Pumunta sa kurso