Jenereta katika Python: yield, iterators, na list na dict comprehensions
Jenereta katika Python ni kitu kinachotoa thamani moja moja kadiri zinavyoombwa, bila kuhifadhi mfuatano wote kwenye kumbukumbu; kinatengenezwa kwa kazi yenye neno kuu yield au kwa usemi wa jenereta kwenye mabano ya duara. Jenereta za orodha, kamusi na seti (comprehensions) ni njia fupi ya kujenga mkusanyiko uliokamilika kwa mstari mmoja: [x * 2 for x in xs], {k: v for k, v in pairs}, {x for x in xs}.
Jenereta katika Python ni kitu kinachotoa thamani moja kwa moja, kwa kadiri zinavyoombwa, na hakiweki mfuatano mzima kwenye kumbukumbu. Kinatengenezwa kwa njia mbili: kwa kazi yenye neno yield, au kwa usemi wa jenereta kwenye mabano ya duara. Pamoja nazo kuna comprehensions — njia fupi ya kujenga mkusanyiko uliokamilika kwa mstari mmoja. Hapa tutaangalia jinsi python yield inavyofanya kazi, jenereta inatofautianaje na iterator, na jinsi ya kutoandika comprehension isiyosomeka.
Iterators: msingi wa mzunguko wa for
Ili kuelewa jenereta, kwanza tuelewe iterators. Kitu kinachoweza kupitiwa (iterable) ni chochote ambacho unaweza kukipitia kwa mzunguko: orodha, mfuatano, kamusi, faili. Iterator ni kitu kinachokumbuka nafasi yake ya sasa, na kwa wito wa next() hutoa kipengele kinachofuata; vitu vikiisha, hutupa StopIteration.
nums = [10, 20, 30]
it = iter(nums) # pata iterator kutoka orodha
print(next(it)) # 10
print(next(it)) # 20
print(next(it)) # 30
next(it) # StopIteration
Mzunguko wa for hufanya hasa hivi: huita iter(), kisha next() hadi StopIteration. Iterator yako mwenyewe inaweza kuandikwa kama darasa lenye mbinu __iter__ na __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]
Inafanya kazi, lakini kuna msimbo mwingi wa kurudiarudia. Jenereta hufanya jambo hilo hilo kwa mistari mitatu.
Jenereta katika Python na yield inavyofanya kazi
Kazi ya jenereta ni kazi ya kawaida ambayo mwilini mwake kuna yield. Wito wake hautekelezi msimbo, bali unarudisha kitu cha jenereta. Kila next() huendesha mwili hadi yield iliyo karibu, hutoa thamani, na kuganda kazi pamoja na vigezo vyake vya ndani. next() inayofuata inaendelea kutoka pale pale.
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] — tumeendelea kutoka pale tulipoishia
Kazi inapofika mwisho (au return), jenereta yenyewe hutupa StopIteration, na mzunguko wa for unamaliza kwa utulivu. Hapa ndipo tofauti kati ya yield na return ilipo: return humaliza kazi na kurudisha thamani moja, yield husimamisha kazi kwa muda na inaweza kutoa thamani nyingi.
Jenereta na iterator: tofauti
Kila jenereta ni iterator: ina __iter__ na __next__. Lakini si kila iterator ni jenereta. Tofauti iko katika njia ya kuunda na uwezo.
| Iterator (darasa) | Jenereta | |
|---|---|---|
| Jinsi ya kuunda | darasa lenye __iter__ na __next__ | kazi yenye yield au (x for x in ...) |
| Hali | kwa mkono, kwenye sifa | inahifadhiwa kiotomatiki kwenye vigezo vya ndani |
| StopIteration | unaitupa mwenyewe | inatokea yenyewe kazi inapomaliza |
| Zaidi ya hapo | unachoandika | send(), throw(), close() |
Wanachofanana ni sifa moja muhimu: zinaweza kupitiwa mara moja tu. for ya pili juu ya jenereta iliyokwisha haitatoa kitu — inahitajika jenereta mpya.
Jenereta isiyo na mwisho na kusoma faili kubwa
Jenereta hailazimiki kuisha. Mfuatano usio na mwisho ni salama mradi unachukua idadi maalum ya vipengele tu:
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']
Mfano wa vitendo zaidi ni kuchakata faili kubwa mstari kwa mstari. Jenereta zinaweza kuunganishwa kuwa mfumo wa mtiririko (pipeline), na kwenye kumbukumbu kutakuwa na mstari mmoja kwa wakati, si faili zima:
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 na send
yield from hupitisha thamani zote za kitu kingine kinachoweza kupitiwa, bila mzunguko wa ndani:
def flatten(groups):
for g in groups:
yield from g
print(list(flatten([[1, 2], [3], [4, 5]]))) # [1, 2, 3, 4, 5]
Mbinu send() inatuma thamani ndani ya jenereta: inakuwa matokeo ya usemi wa yield. Kabla ya send ya kwanza, jenereta lazima "iwashwe" kwa next(). Kwa mfano, hivi ndivyo kikokotoo cha wastani kinavyoandikwa:
def averager():
total, count = 0, 0
avg = None
while True:
value = yield avg
total += value
count += 1
avg = total / count
a = averager()
next(a) # fika kwenye yield ya kwanza
print(a.send(10)) # 10.0
print(a.send(20)) # 15.0
Katika msimbo wa kawaida send hutumika mara chache, lakini mara nyingi huulizwa kwenye mahojiano ya kazi.
range: je, ni jenereta?
Hapana. range ni mfuatano wa uvivu: pia haihifadhi namba zote, lakini inaruhusu kupata kwa faharasa, len(), ukaguzi wa in, na kupitiwa mara nyingi kadiri unavyotaka. Jenereta haina uwezo wowote kati ya hayo.
r = range(5)
print(r[2], len(r), 3 in r) # 2 5 True
print(list(r), list(r)) # mara zote mbili [0, 1, 2, 3, 4]
List comprehension: orodha kwa mstari mmoja
List comprehension ni muundo [usemi for x in mkusanyiko]. Inaunda orodha mpya bila kubadilisha ile ya asili. Inasomwa kutoka kushoto kwenda kulia: kwanza "nini cha kuweka", kisha "kinatoka wapi".
amounts = [100, 250, 400]
# mzunguko wa kawaida
with_markup = []
for a in amounts:
with_markup.append(a * 1.2)
# sawa kwa comprehension
with_markup = [a * 1.2 for a in amounts]
print(with_markup) # [120.0, 300.0, 480.0]
Licha ya jina "jenereta" linalotumika mara nyingi, matokeo ya list comprehension ni orodha tayari, si jenereta: vipengele vyote vinakokotolewa mara moja.
Jenereta ya orodha yenye sharti if na if else
Hapa ndipo watu wengi huchanganyikiwa. Sharti la kuchuja huwekwa mwishoni na huondoa vipengele. Sharti la kuchagua (opereta ya masharti) huwekwa mwanzoni na hubadilisha kila kipengele kuwa moja kati ya thamani mbili, bila kuondoa chochote.
amounts = [100, 250, 400]
big = [a for a in amounts if a > 200] # [250, 400]
labels = ["kubwa" if a > 200 else "ndogo" for a in amounts]
# ['ndogo', 'kubwa', 'kubwa']
# kuchuja na kuchagua pamoja
odd_marks = ["shufwa" if n % 2 == 0 else "witiri" for n in range(6) if n > 2]
# ['witiri', 'shufwa', 'witiri']
Muundo [a for a in amounts if a > 200 else 0] ni kosa la sintaksia: else inaruhusiwa tu kwenye sharti lililo mwanzoni.
Orodha zilizoingiana na orodha ya pande mbili
For ya pili huandikwa kwa mpangilio uleule wa mizunguko iliyoingiana, kutoka juu kwenda chini. Hii inasaidia kutochanganya mpangilio unapounganisha orodha ya orodha:
groups = [[1, 2], [3], [4, 5]]
flat = [x for g in groups for x in g]
# sawa na:
# for g in groups:
# for x in g:
# flat.append(x)
print(flat) # [1, 2, 3, 4, 5]
Orodha ya pande mbili (matrix) hujengwa kwa comprehension ndani ya comprehension. Kosa la kawaida ni kuzidisha orodha: safu zitakuwa kitu kile kile.
bad = [[0] * 3] * 2
bad[0][0] = 1
print(bad) # [[1, 0, 0], [1, 0, 0]] — safu zote mbili zimebadilika
grid = [[0] * 3 for _ in range(2)]
grid[0][0] = 1
print(grid) # [[1, 0, 0], [0, 0, 0]]
# kubadilisha safu na nguzo
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]]
Jenereta ya kamusi na ya seti
Mbinu hiyo hiyo inafanya kazi kwa mikusanyiko mingine. Mabano ya mikunjo yenye koloni hukusanya kamusi (dict comprehension), na bila koloni — seti (set comprehension).
spent = [("chakula", 500), ("teksi", 300), ("chakula", 200)]
categories = {cat for cat, _ in spent}
print(categories) # {'chakula', 'teksi'} — mpangilio wa seti haujahakikishwa
last = {cat: amount for cat, amount in spent}
print(last) # {'chakula': 200, 'teksi': 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()}
Angalia last: ufunguo unaporudiwa, thamani ya mwisho ndiyo inayoshinda, na jumla ya 500 imepotea. Ikiwa unataka kujumlisha, comprehension haifai — andika mzunguko au tumia collections.Counter / defaultdict.
Vipi kuhusu jenereta ya tuple?
Python haina "tuple comprehension" tofauti. Mabano ya duara yanatoa usemi wa jenereta, na tuple hupatikana kwa kuupitisha kwa tuple():
t = tuple(n * 2 for n in range(3)) # (0, 2, 4)
Usemi wa jenereta: kutohifadhi, bali kutoa
Ukibadilisha mabano ya mraba kuwa ya duara, unapata usemi wa jenereta. Haihesabu kitu mapema na inatoa thamani moja moja. Kwa milioni ya vipengele tofauti ya kumbukumbu ni kubwa sana:
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)) # takriban baiti milioni 8
print(sys.getsizeof(as_gen)) # mamia machache ya baiti
Mara nyingi usemi wa jenereta huishi ndani ya kazi za kujumlisha. Ikiwa jenereta ndiyo hoja pekee, mabano ya ziada hayahitajiki. any() na all() pia husimama kwenye jibu la kwanza na hazipitii kilichobaki.
spent = [("chakula", 500), ("teksi", 300), ("chakula", 200)]
total = sum(a for _, a in spent) # 1000
has_big = any(a > 400 for _, a in spent) # True, haiangalii zaidi ya 500
biggest = max(spent, key=lambda p: p[1]) # ('chakula', 500)
Kanuni rahisi ya uchaguzi: ikiwa matokeo yanahitajika mara kadhaa, au kuna faharasa au len(), chukua orodha. Ikiwa unahitaji kupitia mara moja na kuhesabu kitu, chukua jenereta.
Upeo wa vigezo: kigezo hakivuji
Swali ambalo hata wazoefu hukwama nalo:
x = 10
squares = [x for x in range(3)]
print(x) # 10
Kigezo cha mzunguko ndani ya comprehension kinaishi katika upeo wake na hakifuti cha nje (ndivyo ilivyo katika Python 3; Python 2 ilikuwa tofauti). Katika mzunguko wa kawaida wa for, baada ya mzunguko x ingekuwa 2. Kuanzia toleo la 3.12 (PEP 709) comprehensions zinaunganishwa ndani ya kazi inayozizunguka na zinafanya kazi kwa kasi zaidi, lakini kanuni ya upeo imebaki ile ile.
Unpacking ya * na ** ndani ya comprehensions: mpya katika Python 3.15 (PEP 798)
Nyota ya kufungua (unpacking) imekuwa ikiandikwa katika wito wa kazi na katika orodha ya literal kwa muda mrefu, lakini ndani ya comprehension ilikuwa SyntaxError. Katika Python 3.15 kizuizi hicho kimeondolewa: *kipengele inamaanisha "weka hapa vipengele vyake vyote". Inafanya kazi katika orodha, seti na jenereta, pamoja na zile za async.
# 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} # seti
lazy = (*g for g in groups) # jenereta ya uvivu
Nyota mbili hufanya hivyo hivyo kwa kamusi. Ni rahisi kwa mipangilio ya tabaka: "chaguo-msingi → faili → bendera", ambapo kila tabaka linalofuata linafunika funguo za lile lililotangulia.
# Python 3.15+
layers = [{"debug": False, "port": 80}, {"port": 8080}, {"debug": True}]
config = {**layer for layer in layers}
# {'debug': True, 'port': 8080}
Tahadhari mbili kutoka kwenye kozi. Nyota haiwezi kuunganishwa na sharti la masharti bila mabano: [*w if w else 0 for w in weeks] ni kosa la sintaksia, sharti linapaswa kuwekwa kwenye mabano. Na msimbo kama huu hautaendeshwa kwenye 3.14 na matoleo ya chini: ikiwa maktaba inasaidia matoleo ya zamani, bakiza mzunguko wa for mara mbili au itertools.chain.from_iterable.
Jenereta za asynchronous
Ikiwa kazi ya async def ina yield, unapata jenereta ya asynchronous. Inapitiwa kwa async for, na ndani yake unaweza kusubiri mtandao au diski kwa await. Pia kuna comprehensions za asynchronous.
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())
Makosa ya kawaida
- Comprehension yenye ghorofa tatu. For mbili, if mbili na opereta ya masharti kwenye mstari mmoja hakuna atakayesoma baada ya wiki. Usipoingia kwenye mstari mmoja, andika mzunguko wa kawaida: PEP 8 inaweka usomaji mbele ya ufupi.
- Kupitia jenereta mara ya pili.
sum(gen)ya pili itarudisha 0, kwa sababu jenereta tayari imeisha. - Comprehension kwa ajili ya athari ya pembeni.
[print(x) for x in xs]inajenga orodha isiyo ya lazima yaNone. Kwa vitendo tumia mzunguko wafor. - Kutarajia orodha kutoka usemi wa jenereta.
(x for x in xs)[0]niTypeError: jenereta haina faharasa.
Kazi za mazoezi kwa jenereta
- Andika jenereta
fib()inayotoa namba za Fibonacci bila kikomo, kisha chukua kumi za kwanza kwaislice. - Kwa mstari mmoja pata kamusi ya "neno → urefu" kwa maneno yenye herufi zaidi ya tatu.
- Hesabu jumla ya miraba ya namba shufwa hadi milioni, bila kuunda orodha.
- Andika jenereta
chunks(seq, n)inayotoa vipande vya orodha vyanvipengele.
Hitimisho
Iterator hutoa vipengele moja moja kupitia next(), na jenereta ndiyo njia rahisi ya kuiandika: kazi yenye python yield, au usemi kwenye mabano ya duara. Jenereta za orodha, kamusi na seti hukusanya mkusanyiko uliokamilika kwa mstari mmoja; sharti la kuchuja if huwekwa mwishoni, na uchaguzi if else mwanzoni. Ukitaka kuokoa kumbukumbu au kuchakata mtiririko wa data mara moja, chukua jenereta; ukihitaji matokeo mara nyingi, chukua orodha. Comprehensions, jenereta na unpacking mpya ya Python 3.15 zimefafanuliwa kwa vitendo katika kozi kwa mfano wa kifuatiliaji cha matumizi.
Maswali ya mara kwa mara
Je, naweza kutumia return ndani ya jenereta?
Ndiyo. return isiyo na thamani inahitimisha jenereta tu, na return yenye thamani huiweka kwenye sifa value ya StopIteration; thamani hiyo inapokelewa na usemi wa yield from.
Ninawezaje kugeuza jenereta kuwa orodha?
Pitisha kwa list(): list(gen). Kumbuka kwamba baada ya hapo jenereta imeisha, na thamani zote zinakuwa kwenye kumbukumbu.
Ipi ni haraka zaidi: list comprehension au mzunguko wa append?
Kwa kawaida comprehension ni kidogo haraka kwa sababu haitoi wito wa append kila mzunguko. Lakini chagua kwa usomaji; tofauti mara chache huamua.
Ninawezaje kujua kwamba kitu ni jenereta?
Tumia inspect.isgenerator(obj) kwa kitu au inspect.isgeneratorfunction(f) kwa kazi; kukagua kama kitu ni iterator, tumia isinstance(obj, collections.abc.Iterator).
Usemi wa jenereta unatofautianaje na list comprehension?
Mabano ya mraba hujenga orodha yote kwenye kumbukumbu mara moja; mabano ya duara huunda jenereta inayokokotoa vipengele inapoombwa, na inaweza kupitiwa mara moja tu.
Mada hii inafundishwa kwa kina pamoja na mazoezi katika kozi “Python 3.15: от новичка до профи”.
Nenda kwenye kozi