Ачааллж байна...
Түвшин 5 эхэлж байна — сүүлчийн түвшин.
Бүлэг 13-т бид дөрвөн "гүнзгий хэрэгсэл" -ийг үзнэ. Тэдгээр нь та Python-ы кодыг УНШИХАД хэрэгтэй болно, мөн зарим нь өдөр тутам ашиглагдана.
Эхнийх нь generator ба yield.
Энэ сэдэв нь эхэндээ хачирхалтай санагдаж болно. Гэхдээ би түүнийг ГЭНЭТИЙН БИШ болгохыг хичээе, учир нь та түүнийг АЛЬ ХЭДИЙН олон удаа ашигласан — зөвхөн мэдэлгүйгээр.
range()? Generator шиг. zip()? Generator шиг. enumerate()? Generator шиг. sum(i for i in ...)? Тэр бол ЖИНХЭНЭ generator.
Энэ хичээлийн зорилго нь: тэдгээр нь ЯАЖ ажилладгийг ойлгож, өөрөө generator бичих чадвартай болох.
Гэхдээ шударгаар хэлье: generator-ыг ЭЗЭМШИХ шаардлагагүй. Тэдгээрийг УНШИЖ ТАНИХ нь хангалттай. Хэрэв та тэднийг хэзээ ч өөрөө бичихгүй бол — тэр ч бас зүгээр.
Асуудлыг эхнээс нь харъя.
Та нэг сая тооны квадратыг тооцоолохыг хүсэж байна.
python
def kvadratuud(hemjee: int) -> list[int]:
ur_dun = []
for i in range(hemjee):
ur_dun.append(i * i)
return ur_dun
tootsuud = kvadratuud(1_000_000)
print(f"Эхний 5: {tootsuud[:5]}")
print(f"Нийлбэр: {sum(tootsuud)}")Эхний 5: [0, 1, 4, 9, 16]
Нийлбэр: 333332833333500000Ажиллаж байна. Гэвч нэг асуудал бий.
Тэр жагсаалт нь САНАХ ОЙД БҮХЭЛДЭЭ БАЙНА. Нэг сая тоо.
Хэр их санах ой вэ? Шалгая:
python
import sys
tootsuud = kvadratuud(1_000_000)
print(f"Хэмжээ: {sys.getsizeof(tootsuud) / 1024 / 1024:.1f} МБ")Хэмжээ: 8.0 МБ8 мегабайт. Тэр нь одоогийн компьютерт асуудалгүй.
Гэвч арван сая бол? Зуун сая бол?
python
tootsuud = kvadratuud(100_000_000)MemoryErrorMemoryError — санах ой дуусав. Программ унана.
Асуудал нь тодорхой: бид БҮХ утгыг НЭГ ДОР санах ойд хадгалж байна.
Гэвч бидэнд тэр хэрэгтэй юу? Хэрэв бид зөвхөн НИЙЛБЭР бодох гэж байгаа бол — бид тоо бүрийг НЭГ УДАА л харна. Тэднийг бүгдийг хадгалах шаардлагагүй.
Generator нь яг тэр асуудлыг шийднэ.
Generator-ыг ойлгох хамгийн сайн зүйрлэл нь энэ.
Жагсаалт бол АГУУЛАХ. Та түүн рүү орох үед — БҮХ бараа тэнд байна, өрөөсөн, бэлэн. Та тэднийг тоолж, эрэмбэлж, хэдэн ч удаа харж болно.
Гэхдээ тэдгээр нь БҮГД тавиур эзэлж байна. Мянган бараа = мянган байрлал.
Generator бол АВТОМАТ ХУДАЛДААНЫ МАШИН. Түүнд бүх бараа БЭЛЭН БАЙХГҮЙ. Тэр нь:
Та товч дарна
Тэр НЭГ бараа гаргана
Тэр ЗОГСОНО — дараагийн товч дарах хүртэл
Та дахин дарна → дараагийн бараа
Хамгийн чухал зүйл: машин нь дараагийн барааг АЛЬ ХЭДИЙН ГАРГААГҮЙ. Тэр нь зөвхөн та ГУЙХАД гаргана.
Тиймээс машин нь сая ширхэг барааг санах ойд хадгалахгүй. Тэр нь зөвхөн "дараагийнх нь юу вэ" гэдгийг мэдэж байхад л хангалттай.
Generator бол яг тэр:
Утгыг ХЭРЭГТЭЙ ҮЕД НЬ гаргана
Гаргасны дараа ЗОГСОНО
Дараагийнхыг гуйхад — ҮРГЭЛЖЛҮҮЛНЭ
Бүх утгыг санах ойд хадгалахгүй — ЗӨВХӨН ОДООГИЙН БАЙРЛАЛАА санана
yieldpython
def kvadratuud(hemjee: int):
for i in range(hemjee):
yield i * i # RETURN БИШ — YIELD!
gen = kvadratuud(5)
print(gen)
print(type(gen))<generator object kvadratuud at 0x000001A2B3C4D5E0>
<class 'generator'>Гэнэтийн зүйл! Функц нь тоо буцаагаагүй. Тэр нь GENERATOR ОБЪЕКТ буцаасан.
Гол дүрэм:
Функц дотор
yieldбайвал — тэр нь ГЕНЕРАТОР болно.
Түүнийг дуудахад код нь АЖИЛЛАХГҮЙ. Зөвхөн generator объект үүснэ.
Код нь ХЭЗЭЭ ажиллах вэ? Утга ГУЙХАД.
python
gen = kvadratuud(5)
for kvadrat in gen:
print(kvadrat)0
1
4
9
16Одоо ажиллалаа. for loop нь утга гуйж байна, generator нь өгч байна.
next() — гараар гуйхЮу болж байгааг ТОД харахын тулд next() ашиглая.
python
def toolokh():
print(" → Эхлэв")
yield 1
print(" → Хоёрдугаар хэсэг")
yield 2
print(" → Гуравдугаар хэсэг")
yield 3
print(" → Дууслаа")
gen = toolokh()
print("Generator үүсэв (код ажиллаагүй)")
print()
print("next() дуудлаа:")
print(f"Утга: {next(gen)}")
print()
print("Дахин next():")
print(f"Утга: {next(gen)}")
print()
print("Дахин next():")
print(f"Утга: {next(gen)}")Generator үүсэв (код ажиллаагүй)
next() дуудлаа:
→ Эхлэв
Утга: 1
Дахин next():
→ Хоёрдугаар хэсэг
Утга: 2
Дахин next():
→ Гуравдугаар хэсэг
Утга: 3ЭНЭ БОЛ ХИЧЭЭЛИЙН ЗҮРХ. Анхааралтай хараарай.
1. gen = toolokh() — код ОГТ ажиллаагүй. "→ Эхлэв" хэвлэгдээгүй.
2. Эхний next(gen):
Код нь эхнээс эхэлж ажиллана
"→ Эхлэв" хэвлэгдэнэ
yield 1 -д хүрнэ → ЗОГСОНО, 1 -ыг өгнө
3. Хоёр дахь next(gen):
Код нь ЯГ ТЭР ГАЗРААС үргэлжилнэ (yield 1 -ийн дараагаас)
"→ Хоёрдугаар хэсэг" хэвлэгдэнэ
yield 2 -д хүрнэ → ЗОГСОНО
yield нь функцийг ЗОГСООДОГ, гэхдээ УСТГАДАГГҮЙ.
Тэр нь бүх төлөвөө (хувьсагчид, хаана байсныг) САНАЖ байна. Дараагийн next() дуудахад — яг тэндээсээ үргэлжилнэ.
return -тэй харьцуулъя:
return — функц ДУУСНА. Бүх төлөв УСТАНА. Дахин дуудахад ЭХНЭЭС эхэлнэ.
yield — функц ЗОГСОНО. Төлөв ХЭВЭЭРЭЭ. Дараагийн next() -д тэндээсээ ҮРГЭЛЖИЛНЭ.
StopIterationХэрэв утга дуусахад дахин next() дуудвал?
python
def toolokh():
yield 1
yield 2
gen = toolokh()
print(next(gen))
print(next(gen))
print(next(gen)) # утга дууссан!1
2
Traceback (most recent call last):
File "main.py", line 10, in <module>
print(next(gen))
StopIterationStopIteration — "утга дууслаа."
Энэ бол АЛДАА биш, харин ДОХИО.
for loop нь тэр дохиог сонсож, эвлэгээр зогсдог:
python
for x in toolokh():
print(x)1
2Ямар ч алдаа гарсангүй. for нь StopIteration -ыг барьж, зогссон.
Энэ нь Бүлэг 13.4-т бүрэн тайлагдана. Одоохондоо мэдэж байгаарай: for loop нь дотроо next() дуудаж, StopIteration гарах хүртэл үргэлжилдэг.
Одоо жинхэнэ ашиг тусыг харъя.
python
import sys
def kvadratuud_list(hemjee: int) -> list[int]:
return [i * i for i in range(hemjee)]
def kvadratuud_gen(hemjee: int):
for i in range(hemjee):
yield i * i
HEMJEE = 1_000_000
jagsaalt = kvadratuud_list(HEMJEE)
gen = kvadratuud_gen(HEMJEE)
print(f"List: {sys.getsizeof(jagsaalt):>12,} байт")
print(f"Generator: {sys.getsizeof(gen):>12,} байт")List: 8,448,728 байт
Generator: 208 байт8 МЕГАБАЙТ vs 208 БАЙТ.
Дөчин мянга дахин бага!
Яагаад? Учир нь generator нь ЮУ Ч хадгалаагүй. Тэр зөвхөн "би хаана байгаа вэ" гэдгээ санаж байна.
Хэрэв HEMJEE нь ЗУУН САЯ байсан бол:
List → MemoryError, программ унана
Generator → МӨН 208 байт. Хэмжээ нь ХАМААГҮЙ.
Учир нь generator нь хэзээ ч бүх утгыг нэг дор барьдаггүй.
python
import time
def kvadratuud_gen(hemjee: int):
for i in range(hemjee):
yield i * i
ekhelsen = time.time()
niilber = sum(kvadratuud_gen(10_000_000))
duussan = time.time()
print(f"Нийлбэр: {niilber:,}")
print(f"Хугацаа: {duussan - ekhelsen:.2f} секунд")Нийлбэр: 333,333,283,333,335,000,000
Хугацаа: 1.34 секундАРВАН САЯ тоо, ямар ч санах ойн асуудалгүй.
sum() нь generator -аас утгыг НЭГ НЭГЭЭР нь авч, нэмж байна. Хэзээ ч бүгдийг нэг дор барихгүй.
Хэрэв list ашигласан бол — 80 МБ санах ой (эсвэл MemoryError).
Одоо гоё зүйл. Та generator -ыг comprehension шиг бичиж болно.
python
# List comprehension — ДӨРВӨЛЖИН хаалт
jagsaalt = [i * i for i in range(1_000_000)]
# Generator expression — ДУГУЙ хаалт!
gen = (i * i for i in range(1_000_000))
print(type(jagsaalt))
print(type(gen))<class 'list'>
<class 'generator'>Ганц ялгаа: ХААЛТ.
[...] → list (бүгдийг санах ойд үүсгэнэ)
(...) → generator (хэрэгтэй үед нь гаргана)
Бүлэг 2.1-ийн comprehension нь Бүлэг 13-ын generator-той нийлж байна.
Практикт хамгийн түгээмэл хэрэглээ:
python
# Санах ой их — БҮГДИЙГ үүсгэнэ
niilber = sum([i * i for i in range(10_000_000)])
# Санах ой бага — нэг нэгээр нь
niilber = sum(i * i for i in range(10_000_000))Хоёр дахь хувилбарт хаалт ч байхгүй!
Хэрэв generator expression нь функцийн ЦОРЫН ГАНЦ аргумент бол — дугуй хаалтыг орхиж болно. sum(...) -ийн хаалт нь хангалттай.
Та үүнийг АЛЬ ХЭДИЙН ашигласан. Түвшин 1-ийн guided build-аас:
python
def bukh_khamgiin_urt(ugs: list[str]) -> list[str]:
max_urt = max(len(ug) for ug in ugs) # GENERATOR EXPRESSION!
return [ug for ug in ugs if len(ug) == max_urt]Тэр үед би ингэж хэлсэн:
"Түүнийг generator expression гэж нэрлэдэг бөгөөд Бүлэг 13-д бүрэн үзнэ. Одоохондоо 'жагсаалт үүсгэлгүйгээр шууд урсгадаг comprehension' гэж ойлгоод өнгөрөөрэй."
Одоо та түүнийг бүрэн ойлгож байна.
Мөн Бүлэг 12.4-ийн цаг агаарын программаас:
python
@property
def khurtai_udur(self) -> int:
return sum(1 for u in self.udruud if u.khur_uu)Дахин generator expression.
range, zip, enumerate — бүгд ижил зарчимОдоо нэг зүйлийг тодруулъя.
python
r = range(1_000_000)
z = zip([1, 2, 3], ["а", "б", "в"])
e = enumerate(["а", "б", "в"])
import sys
print(f"range: {sys.getsizeof(r)} байт")
print(f"zip: {sys.getsizeof(z)} байт")
print(f"enumerate: {sys.getsizeof(e)} байт")range: 48 байт
zip: 64 байт
enumerate: 64 байтБүгд ЖИЖИГ. Тэдгээр нь утгыг хадгалдаггүй — хэрэгтэй үед нь гаргадаг.
range(1_000_000) нь сая тоо БАРИХГҮЙ. Тэр зөвхөн "0-ээс эхэлж, 999,999 хүртэл, нэгээр нэмэгдэнэ" гэдгийг мэднэ.
Тиймээс Бүлэг 2.3-ын сургамж одоо тодорхой боллоо:
"
zip-ийн үр дүнг ХОЁР удаа ашиглаж болохгүй. Эхний loop түүнийг бүрэн уншиж дуусгасан тул хоёр дахь loop-д юу ч үлдээгүй."
python
hosud = zip([1, 2], ["а", "б"])
print(list(hosud))
print(list(hosud)) # ХОЁР дахь удаа[(1, 'а'), (2, 'б')]
[]Хоосон.
Учир нь generator нь НЭГ УДААГИЙН. Автомат машинаас бараа гарсны дараа — тэр буцаж орохгүй.
Хэрэв та олон удаа ашиглах бол — list() -д хийж, жинхэнэ жагсаалт болго:
python
hosud = list(zip([1, 2], ["а", "б"]))
print(hosud)
print(hosud) # одоо ажиллана[(1, 'а'), (2, 'б')]
[(1, 'а'), (2, 'б')]Одоо generator -ын ЖИНХЭНЭ, өдөр тутмын хэрэглээ.
Асуудал: 10 ГБ хэмжээтэй файлыг унших.
Буруу арга:
python
def bukh_mur_unshikh(zam: str) -> list[str]:
with open(zam, "r", encoding="utf-8") as f:
return f.readlines() # БҮХ файлыг санах ойд!
murууd = bukh_mur_unshikh("asar_tom.txt")MemoryError10 ГБ файлыг 8 ГБ RAM -д хийж болохгүй.
Зөв арга — generator:
python
def murууd_unshikh(zam: str):
with open(zam, "r", encoding="utf-8") as f:
for mur in f:
yield mur.strip()
too = 0
for mur in murууd_unshikh("asar_tom.txt"):
if "алдаа" in mur.lower():
too += 1
print(f"'алдаа' гэсэн үг {too} мөрөнд байна")Ажиллана. Файл хэдий том байсан ч — санах ойд ЗӨВХӨН НЭГ МӨР байна.
Дашрамд: for mur in f: гэсэн бичлэг нь өөрөө ЗӨВ ажилладаг. Python-ы файл объект нь ӨӨРӨӨ generator шиг ажилладаг — тэр нь мөрийг нэг нэгээр нь өгдөг.
Тиймээс readlines() -ыг ХЭЗЭЭ Ч бүү ашигла (жижиг файлаас бусад тохиолдолд). for mur in f: нь илүү сайн.
Generator ашигла:
Өгөгдөл нь ТОМ (файл, өгөгдлийн сан, API-гийн олон хуудас)
Утгуудыг НЭГ УДАА л ашиглана
ХЯЗГААРГҮЙ дараалал (жишээ нь санамсаргүй тоонууд)
Санах ой чухал
List ашигла:
Өгөгдөл нь ЖИЖИГ (хэдэн зуун, хэдэн мянган элемент)
Утгуудыг ОЛОН УДАА ашиглана
Index хэрэгтэй (jagsaalt[5])
Урт хэрэгтэй (len())
Эрэмбэлэх хэрэгтэй
Generator нь ХИЙЖ ЧАДАХГҮЙ зүйлс:
python
gen = (i * i for i in range(10))
print(len(gen))TypeError: object of type 'generator' has no len()Бүлэг 8.3-аас танил алдаа! Generator -д __len__ байхгүй — учир нь тэр нь хэдэн утга гаргахаа МЭДЭХГҮЙ.
python
print(gen[3])TypeError: 'generator' object is not subscriptableIndex байхгүй. Тэр нь дараалал БИШ.
Практик зөвлөгөө: эргэлзвэл LIST ашигла.
Generator нь ЗӨВХӨН санах ой асуудал болоход л хэрэгтэй. Ихэнх программд тэр нь хэзээ ч болдоггүй.
Хэрэв сонирхвол: next() -тэй жишээг өөрөө ажиллуулж, print -үүд ХЭЗЭЭ хэвлэгдэж байгааг ажиглаарай. Тэр туршилт нь "yield нь зогсоодог, устгадаггүй" гэдгийг бүрэн ойлгуулна.
Мөн хүсвэл: sys.getsizeof() -ээр list ба generator-ын хэмжээг харьцуулж үзээрэй. Дөчин мянга дахин ялгаа нь ямар ч тайлбараас илүү үнэмшилтэй.
Мөн хүсвэл: хязгааргүй generator бичээд үзээрэй:
python
def toolch():
i = 0
while True: # ХЯЗГААРГҮЙ!
yield i
i += 1
gen = toolch()
for x in gen:
if x > 10:
break
print(x)Тэр нь ажиллана! Учир нь generator нь хэзээ ч бүх утгыг үүсгэдэггүй. break -ээр зогсооход л хангалттай.
Жагсаалтаар үүнийг хийх нь БОЛОМЖГҮЙ.
1. return ба yield -ыг андуурах
python
def kvadratuud(hemjee: int):
for i in range(hemjee):
return i * i # RETURN — эхнийхийг л буцаана!python
print(kvadratuud(5))0return нь функцийг ДУУСГАНА. Зөвхөн ЭХНИЙ утга л гарна. yield бич.
2. Generator -ыг ХОЁР удаа ашиглах
python
gen = (i * i for i in range(5))
print(sum(gen))
print(sum(gen)) # хоёр дахь удаа30
0Хоосон! Generator нь НЭГ УДААГИЙН. Олон удаа хэрэгтэй бол list() -д хий.
3. len() эсвэл index ашиглах
TypeError: object of type 'generator' has no len()
TypeError: 'generator' object is not subscriptableGenerator нь дараалал БИШ. list(gen) гэж хувирга (гэхдээ тэр үед санах ойн давуу тал алга болно).
4. Хаалтыг андуурах
python
jagsaalt = (i * i for i in range(5)) # GENERATOR, list БИШ!
print(jagsaalt[0])TypeError: 'generator' object is not subscriptable[...] = list. (...) = generator.
5. Хязгааргүй generator -ыг list() -д хийх
python
def toolch():
i = 0
while True:
yield i
i += 1
jagsaalt = list(toolch()) # ХЭЗЭЭ Ч дуусахгүй!Программ хөлдөнө, дараа нь MemoryError. Хязгааргүй generator -ыг list() -д ХЭЗЭЭ Ч бүү хий.
Generator нь утгыг ХЭРЭГТЭЙ ҮЕД НЬ, НЭГ НЭГЭЭР нь гаргадаг. Бүгдийг санах ойд хадгалдаггүй.
Зүйрлэл: автомат худалдааны машин — товч дарахад НЭГ бараа гаргаж, дараа нь ЗОГСОНО.
Функц дотор yield байвал — тэр нь GENERATOR болно. Дуудахад код АЖИЛЛАХГҮЙ, зөвхөн generator объект үүснэ.
yield нь функцийг ЗОГСООДОГ, гэхдээ УСТГАДАГГҮЙ. Төлөв нь хэвээрээ. Дараагийн next() -д тэндээсээ ҮРГЭЛЖИЛНЭ.
return → функц ДУУСНА, төлөв УСТАНА
yield → функц ЗОГСОНО, төлөв ХЭВЭЭРЭЭ
next(gen) — дараагийн утгыг гуйна. Утга дуусахад StopIteration (алдаа биш, ДОХИО).
for loop нь дотроо next() дуудаж, StopIteration -ыг барьдаг (Бүлэг 13.4-т бүрэн).
САНАХ ОЙН ЯЛГАА АСАР ТОМ: сая элемент → list нь 8 МБ, generator нь 208 байт.
Generator expression: (i * i for i in ...) — дугуй хаалт. [...] = list, (...) = generator.
Функцийн цорын ганц аргумент бол хаалтыг орхиж болно: sum(i * i for i in range(10))
range, zip, enumerate — бүгд ижил зарчмаар ажилладаг. Тиймээс zip -ийг хоёр удаа ашиглаж болохгүй (Бүлэг 2.3-ын сургамж одоо тодорхой).
Generator нь НЭГ УДААГИЙН. Олон удаа хэрэгтэй бол → list().
Generator -д БАЙХГҮЙ: len(), index, slicing, эрэмбэлэлт.
Бодит хэрэглээ: том файл унших. for mur in f: — санах ойд зөвхөн НЭГ мөр.
ЭРГЭЛЗВЭЛ LIST АШИГЛА. Generator нь зөвхөн санах ой асуудал болоход л хэрэгтэй.
Дараагийн хичээлд бид raise ба өөрийн exception -ыг үзнэ.
Курс 1-д та алдаа БАРЬЖ сурсан (try/except). Одоо та алдааг ҮҮСГЭЖ сурна.
Мөн та Бүлэг 12.4-т ApiAlda гэсэн өөрийн алдааны class-ыг АЛЬ ХЭДИЙН ашигласан. Тэр үед би "Бүлэг 13.2-т бүрэн үзнэ" гэж хэлсэн. Одоо тэр цаг ирлээ — та түүнийг зохих ёсоор нь ойлгож, яагаад тэр нь салгалтыг хамгаалдгийг мэдэх болно.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.