Ачааллж байна...
Course 2-т та yield түлхүүр үгтэй анх танилцаж, функц дотор return-ийн оронд yield бичихэд тэр функц generator болдгийг сурсан. Тэр үед бид generator-ийг ихэвчлэн "утгуудыг нэг нэгээр гаргадаг функц" гэж ерөнхийд нь ойлгоод, for loop-д хэрэглэхэд хангалттай байсан. Энэ хичээлд бид илүү гүн асуулт тавина: generator-ийн дотор яг юу болдог вэ? yield хүрэхэд функц хаана "алга болдог" вэ, түүний хувьсагчид хаана хадгалагддаг вэ, дахин next() дуудахад яаж яг тэр газраасаа үргэлжилдэг вэ? Эдгээр асуултын хариулт нүдэнд харагддаггүй тул энэ хичээлд trace бидний гол хэрэгсэл болно.
Өмнөх бүлгийн сүүлийн хичээлд бид Countdown түгээгчийг хоёр class-аар бичихэд бараг хорин мөр шаардсан. Байрлалаа self.current-д хадгалж, __next__ бүрт гараар шинэчилж, дуусахдаа StopIteration-ийг өөрсдөө шидэх хэрэгтэй байсан. Generator энэ бүх ажлыг өөрөө хийдэг. Гэхдээ энэ нь ид шидээр биш, нэг гайхалтай механизмаар хийгддэг: suspension буюу түр зогсолт. Энгийн функц дуудагдаад эхнээс нь дуустал ажиллаж, бүх хувьсагчаа хаяж дуусдаг бол generator yield хүрэхдээ бүх зүйлээ хэвээр нь хадгалан хөлдөөж, дараагийн хүсэлтийг хүлээдэг.
Энэ хичээлийн гол санаа нэг өгүүлбэрт багтана: generator функцийг дуудахад код огт ажиллахгүй, зөвхөн хөлдсөн машин үүснэ. next() бүр тэр машиныг дараагийн yield хүртэл ажиллуулж, дахин хөлдөөнө. Энэ өгүүлбэрийн эхний хэсэг олон хүнийг гайхшруулдаг тул бид түүнийг нүдээрээ харж батлах болно.
Түр зогсоох товчтой машин төсөөлөөд үзээрэй. Жишээлбэл, урт програмтай угаалгын машин. Та програм эхлүүлээд, угаалгын дундуур нэг зүйл нэмэх гэж түр зогсоох товч дарлаа. Машин зогсоно, гэхдээ доторх ус, хувцас, бөмбөрийн байрлал, програмын аль шатанд хүрсэн нь бүгд яг байсан газраа үлдэнэ. Хэдэн минутын дараа товчийг дахин дарахад машин эхнээс нь эхлэхгүй, яг зогссон шатнаасаа үргэлжилнэ. Машин бүх төлөвөө өөрөө санасан тул та юуг ч тэмдэглэх шаардлагагүй.
Generator бол яг ийм машин. yield бол түр зогсоох товч, гэхдээ товчийг машин өөрөө дардаг: yield хүрэхдээ гадагш нэг утга гаргаж өгөөд өөрийгөө зогсооно. next() бол "үргэлжлүүл" товч. Машины доторх төлөв, тухайлбал бүх локал хувьсагч, кодын аль мөрөнд зогссон нь бүгд хадгалагддаг. Өмнөх хичээлийн Countdown-д бид энэ төлөвийг self.current-д гараар хадгалж байсан бол generator-д Python үүнийг автоматаар хийнэ.
Зүйрлэлийн нэг чухал хэсгийг тэмдэглэе. Угаалгын машиныг худалдаж аваад гэртээ тавихад тэр өөрөө ажиллаж эхэлдэггүй. Машин бэлэн, хувцас дотор нь байгаа ч хэн нэгэн "эхлүүл" товч дарах хүртэл бөмбөр нэг ч эргэхгүй. Generator функцийг дуудах нь машиныг худалдаж авахтай адил, эхний next() бол анхны "эхлүүл" товч юм.
Generator-ийн дотоод ажиллагааг харахын тулд функцийн бие дотор print() мөрүүд нэмье. Эдгээр нь зөвхөн машин хэзээ ажиллаж, хэзээ зогсож байгааг сонсох "чагнуур" юм. Мөрийн эхэнд хоёр зай тавьсан нь generator-ийн доторх мэдээг гадна кодын хэвлэлтээс ялгахад туслах зорилготой.
python
def countdown(start):
print(" [эхэллээ]")
current = start
while current > 0:
print(f" [{current}-ийг yield хийх гэж байна]")
yield current
print(f" [{current}-ийн дараа үргэлжиллээ]")
current -= 1
print(" [функц дууслаа]")
gen = countdown(2)
print("generator үүслээ")
print(next(gen))
print(next(gen))
print(next(gen))Ажиллуулахад:
generator үүслээ
[эхэллээ]
[2-ийг yield хийх гэж байна]
2
[2-ийн дараа үргэлжиллээ]
[1-ийг yield хийх гэж байна]
1
[1-ийн дараа үргэлжиллээ]
[функц дууслаа]
Traceback (most recent call last):
File "C:\python-gunzgii\gen_trace.py", line 16, in <module>
print(next(gen))
^^^^^^^^^
StopIterationГаралтын хамгийн эхний мөрийг анхааралтай хараарай. “generator үүслээ” нь “[эхэллээ]”-ээс өмнө хэвлэгдсэн. Гэтэл код дээр gen = countdown(2) нь print("generator үүслээ")-ээс өмнө байна. Хэрэв countdown энгийн функц байсан бол “[эхэллээ]” эхэлж хэвлэгдэх ёстой. Энэ бол энэ хичээлийн хамгийн чухал баримт: generator функцийг дуудахад түүний бие огт ажиллаагүй.
Одоо энэ кодыг санах ойн түвшинд мөрдье. Алхам бүрт хоёр зүйлийг тэмдэглэнэ: generator-ийн локал хувьсагчид юу байна, машин кодын хаана зогсож байна.
Алхам 1 (gen = countdown(2)). Python countdown-ий бие дотор yield байгааг харж, функцийг ажиллуулахын оронд generator объект үүсгэнэ. Argument нь холбогдоно: start = 2. Гэхдээ функцийн биеийн нэг ч мөр ажиллаагүй. Хөлдсөн төлөв: start = 2; current хараахан үүсээгүй; машин функцийн хамгийн эхэнд, эхний мөрөөс ч өмнө зогсож байна. Угаалгын машин гэртээ ирсэн, хэн ч товч дараагүй.
Алхам 2 (print("generator үүслээ")). Энгийн хэвлэлт. Generator хөдлөөгүй.
Алхам 3 (эхний next(gen)). Машин анх удаа асна. Функцийн эхнээс ажиллаж эхэлнэ: “[эхэллээ]” хэвлэгдэнэ, current = 2 болно, while нөхцөл 2 > 0 үнэн, “[2-ийг yield хийх гэж байна]” хэвлэгдэнэ, yield current мөрөнд хүрнэ. Энд машин 2-ыг гадагш гаргаж өгөөд өөрийгөө зогсооно. next() 2-ыг буцааж, гадна талд хэвлэгдэнэ. Хөлдсөн төлөв: start = 2, current = 2; машин yield мөрөн дээр зогссон, дараагийн ажиллах мөр нь “[...үргэлжиллээ]” хэвлэлт.
Алхам 4 (хоёр дахь next(gen)). Машин зогссон газраасаа, yield-ийн яг дараагийн мөрөөс сэрнэ. Функцийн эхэнд буцаагүй гэдгийг анхаар: “[эхэллээ]” дахин хэвлэгдсэнгүй. “[2-ийн дараа үргэлжиллээ]” хэвлэгдэнэ. Энэ мөрөнд current-ийн утга 2 хэвээр байгаа нь хөлдсөн төлөв бүрэн хадгалагдсаныг нотолж байна. current = 1 болно, while 1 > 0 үнэн, “[1-ийг yield хийх гэж байна]”, yield 1-ийг гаргаад дахин зогсоно. Хөлдсөн төлөв: start = 2, current = 1; машин yield мөрөн дээр.
Алхам 5 (гурав дахь next(gen)). Машин дахин yield-ийн дараагаас сэрнэ. “[1-ийн дараа үргэлжиллээ]” хэвлэгдэнэ, current = 0 болно, while 0 > 0 худал тул loop дуусна. “[функц дууслаа]” хэвлэгдэнэ. Функцийн бие төгсгөлдөө хүрсэн тул Python автоматаар StopIteration шиднэ. Бидний кодонд түүнийг барих зүйл байхгүй тул traceback гарав. Төлөв: машин бүрмөсөн унтарсан. Дахин next() дуудвал шууд StopIteration шидэгдэх бөгөөд функцийн бие дахин ажиллахгүй.
Энэ trace-ийг өмнөх хичээлийн Countdown class-ийн trace-тэй харьцуулаарай. Тэнд self.current бидний гараар хадгалсан төлөв байсан бол энд current бол энгийн локал хувьсагч, гэхдээ Python түүнийг yield бүрт хөлдөөж хадгалж байна. Тэнд бид raise StopIteration-ийг өөрсдөө бичсэн бол энд функцийн бие дуусахад Python өөрөө шидэв. Тэнд байрлалыг тоогоор хадгалсан бол энд "кодын аль мөрөнд зогссон" гэдэг нь өөрөө байрлал болж байна. Энэ сүүлийн санаа бол generator-ийн жинхэнэ хүч: та төлөвөө тоо, attribute болгон хадгалахын оронд кодын бүтцээр илэрхийлж чадна.
Одоо print() чагнууруудыг авч хаявал Countdown хэр богино болохыг харъя:
python
def countdown(start):
while start > 0:
yield start
start -= 1
for n in countdown(3):
print(n)3
2
1Дөрвөн мөр. Өмнөх хичээлийн хорин мөрийн хоёр class хийдэг байсан бүх ажлыг энэ гүйцэтгэж байна. Үүний generator мөн гэдгийг өмнөх бүлгийн хэрэгслүүдээр шалгая:
python
gen = countdown(3)
print(gen)
print(iter(gen) is gen)
print(hasattr(gen, "__next__"))<generator object countdown at 0x7f8b1c2d3e40>
True
TrueGenerator объект бол бүрэн iterator: __next__-тэй, iter() дуудахад өөрийгөө буцаадаг. Python бидний өмнөх хичээлд гараар бичсэн хоёр method-ыг generator-т автоматаар өгсөн. Энэ нь мөн generator нэг удаагийн хэрэглээтэй гэсэн үг, яг zip, файл, нэг class-тай Countdown шиг. Гэхдээ дахин давтах шаардлагатай бол шийдэл энгийн: функцийг дахин дуудах. countdown(3) дуудлага бүр шинэ, эхэнд нь зогссон машин үүсгэнэ.
Жишээ 1: олон хувьсагчтай хөлдсөн төлөв
Хөлдөлт зөвхөн нэг тоолуурыг биш, бүх локал хувьсагчийг хадгалдаг гэдгийг харъя. Өдрийн борлуулалтын жагсаалтаас хуримтлагдсан нийлбэрийг алхам алхмаар гаргадаг generator бичье.
python
def running_total(amounts):
total = 0
count = 0
for amount in amounts:
total += amount
count += 1
yield count, total
sales = [12000, 8500, 15000]
gen = running_total(sales)
print(next(gen))
print(next(gen))
print(next(gen))(1, 12000)
(2, 20500)
(3, 35500)Trace хийе. running_total(sales) дуудагдахад машин үүснэ, amounts = sales холбогдоно, бие ажиллаагүй. Эхний next()-ээр total = 0, count = 0 үүснэ. for loop эхэлнэ. Энэ нь функцийн дотор list-ээс шинэ түгээгч авна, Бүлэг 1-ийн мэдлэг энд ч ажиллаж байна. amount = 12000, total = 12000, count = 1, yield (1, 12000). Хөлдсөн төлөв: amounts, total = 12000, count = 1, мөн for loop-ийн далд түгээгч хоёр дахь элемент рүү заасан, машин yield мөрөнд.
Хоёр дахь next(): машин yield-ийн дараагаас сэрээд for loop-ийн дараагийн давталт руу шилжинэ. Далд түгээгч 8500-ийг өгнө, total = 20500, count = 2, yield. Хөлдсөн төлөв: total = 20500, count = 2, далд түгээгч гурав дахь элемент рүү. Гурав дахь next() ижил замаар (3, 35500) өгнө.
Энэ жишээний гүн дэх сонирхолтой зүйл нь хөлдсөн төлөвт зөвхөн бидний нэрлэсэн хувьсагчид биш, функцийн доторх for loop-ийн далд түгээгч ч багтаж байгаа явдал. Generator зогсох бүрт Python түүнийг ч мөн хадгалсан тул дараагийн давталт зөв элементээс үргэлжилсэн. Хэрэв үүнийг iterator class-аар бичсэн бол бид total, count, position гурвыг бүгдийг нь self дээр гараар хадгалж, __next__ бүрт гурвууланг нь шинэчлэх ёстой байсан.
Жишээ 2: нэг функц, бие даасан хоёр машин
Generator функц бол машины загвар, түүнийг дуудах бүр шинэ, тусдаа машин үүсгэдэг. Хоёр машин бие биенийхээ төлөвийг огт мэдэхгүй. Үүнийг хоёр тоолуурыг ээлжлэн хөдөлгөж батлая.
python
def numbered(prefix):
n = 1
while True:
yield f"{prefix}-{n}"
n += 1
orders = numbered("ЗАХ")
tickets = numbered("ТАС")
print(next(orders))
print(next(orders))
print(next(tickets))
print(next(orders))
print(next(tickets))ЗАХ-1
ЗАХ-2
ТАС-1
ЗАХ-3
ТАС-2Trace-ийг хоёр машинаар зэрэгцүүлж бичье. Эхэндээ хоёулаа үүссэн боловч эхэнд нь зогсож байна: orders-д prefix = "ЗАХ", tickets-д prefix = "ТАС", аль алинд нь n хараахан алга. orders хоёр удаа хөдөлсний дараа: orders-д n = 2, yield мөрөнд зогссон; tickets огт хөдлөөгүй. Дараа нь tickets анх удаа хөдлөхөд: tickets-д n = 1, "ТАС-1" гаргаж зогссон; orders-д n = 2 хэвээр. Дараагийн orders дуудлага n-ийг 3 болгоно, tickets-ийн n = 1 хэвээр. Сүүлийн tickets дуудлага n-ийг 2 болгоно.
Хоёр машин нэг ижил кодыг ажиллуулж байгаа ч тус бүр өөрийн n-тэй, өөрийн зогсолтын цэгтэй. Энэ бол Бүлэг 1-ийн "нэг хайрцагнаас олон түгээгч" санааны generator хувилбар. Энд хайрцаг нь generator функц өөрөө, түгээгч бүр нь дуудлага бүрээр үүсэх generator объект.
Мөн энэ жишээнд while True хязгааргүй loop байгааг анзаараарай. Энгийн функцэд энэ нь програмыг гацаана. Харин generator-т бүрэн аюулгүй, учир нь машин yield бүрт зогсож, хэн нэгэн асуух хүртэл хөдлөхгүй. Хязгааргүй дарааллыг санах ойд хадгалах боломжгүй ч "асуухад нь нэгийг гаргадаг" машин болгон бичиж болно. Захиалгын дугаар, давтагдах ID, тасралтгүй цагийн тэмдэг зэрэг "хэзээ дуусахыг урьдчилан мэдэхгүй" дараалалд энэ загвар маш тохиромжтой. Ийм generator-ийг for loop-д шууд өгвөл тэр мөн л хязгааргүй давтах тул тодорхой тооныг next()-ээр эсвэл break-ээр авах хэрэгтэй. Бүлэгийн сүүлийн хичээлд itertools.islice энэ ажлыг цэвэрхэн хийдгийг харна.
Жишээ 3: return ба машиныг эрт унтраах
Generator дотор return бичиж болно. Энэ нь машиныг тэр даруй бүрмөсөн унтраадаг: функцийн бие дуусахтай адил StopIteration автоматаар шидэгдэнэ. Хэрэглэгчийн командуудыг “STOP” хүртэл боловсруулдаг generator бичье.
python
def commands_until_stop(lines):
for line in lines:
if line == "STOP":
return
yield line.upper()
print(" [бүх мөр дууссан]")
inputs = ["start", "load", "STOP", "delete", "reset"]
for cmd in commands_until_stop(inputs):
print(cmd)START
LOADTrace: эхний хоёр next() “start” ба “load”-ыг томоор бичиж гаргана. Гурав дахь next()-ээр машин сэрээд дараагийн мөр “STOP”-ыг авна, нөхцөл үнэн тул return ажиллана. Машин тэр мөчид унтарна: “delete”, “reset” хэзээ ч уншигдахгүй, бүр “[бүх мөр дууссан]” хэвлэлт ч ажиллахгүй, учир нь return функцийг тэр даруй төгсгөсөн. for loop StopIteration-ийг хүлээн аваад чимээгүй зогсоно.
Энэ жишээ нь generator-ийн "асуухад нь л ажилладаг" чанарын бас нэг давуу талыг харуулж байна. Хэрэв inputs нь хэдэн мянган мөртэй файл байсан бол “STOP”-ын дараах мөрүүд огт уншигдахгүй. Generator зөвхөн хэрэгтэй хэсгээ л хийгээд зогсдог. Мөн return-ийг утгагүй, хоосон бичсэнийг анхаар. Generator-т return-ийн хажууд утга бичих боломжтой ч тэр утга энгийн функцийнх шиг буцаж ирэхгүй, for loop түүнийг огт харахгүй. Энэ нарийн боломж энэ сургалтын хүрээнээс гадуур тул generator-т return-ийг "машиныг одоо унтраа" гэсэн утгаар л хэрэглэвэл хангалттай.
Хүсвэл Анхны жишээний чагнууртай countdown-ийг авч, gen = countdown(2) мөрийн дараа ямар ч next() дуудалгүйгээр програмаа ажиллуулж үзээрэй. Функцийн бие огт ажиллахгүй гэдгийг өөрийн нүдээр харах нь энэ хичээлийн гол баримтыг хамгийн тод батална. Өөр санаа: Жишээ 2-ын numbered generator-ийг "ЗАХ" ба "ТАС"-аас гадна гурав дахь "ХҮР" машинтайгаар ээлжлэн хөдөлгөж, гаралтыг урьдчилан таамаглаад шалгаж болно. Мөн running_total дотор yield-ийн яг дараа print(total) нэмж, хэвлэлт хэзээ гарч байгааг харах нь хөлдөлт ба сэрэлтийн цэгийг илүү тод мэдрүүлнэ. Эдгээр бүгд санал бөгөөд дараагийн хичээлүүд үүнийг хийсэн гэж үзэхгүй.
Функцийг дуудсан ч юу ч болохгүй байх. Энэ алдааны мессежгүй, хамгийн түгээмэл төөрөгдөл. Жишээлбэл, generator дотор файл бичдэг эсвэл имэйл илгээдэг код байгаад, та зөвхөн send_reports(users) гэж дуудсан бол юу ч болохгүй, учир нь машин үүссэн ч хэн ч "эхлүүл" товч дараагүй. Шийдэл нь generator-ийг for loop-оор эсвэл list()-ээр хоослох. Хэрэв таны функцийн гол зорилго нь утга гаргах биш үйлдэл хийх бол тэнд yield хэрэглэх шаардлагатай эсэхийг дахин бодоорой.
<generator object countdown at 0x...> хэвлэгдэх. Энэ нь алдаа биш, generator-ийг утгууд мэт хэвлэх гэж оролдсоны үр дүн. Python "энэ бол countdown функцээс үүссэн generator объект" гэж л хэлж байна. Утгуудыг харахын тулд list(gen) ашигла. Гэхдээ list() машиныг эцэс хүртэл ажиллуулж унтраадаг тул дараа нь тэр generator хоосон байна.
TypeError: ‘generator’ object is not subscriptable. “generator объектод индексээр хандах боломжгүй” гэсэн утгатай. gen[0] гэж бичсэн үед гарна. Generator өмнөх утгуудаа хадгалдаггүй, дараагийн утгаа ч урьдчилан мэддэггүй, зөвхөн "дараагийнх" гэсэн ганц үйлдлийг мэддэг. Эхний элемент хэрэгтэй бол next(gen), индекс хэрэгтэй бол эхлээд list(gen) болго.
TypeError: object of type ‘generator’ has no len(). “generator төрлийн объектод len() байхгүй” гэсэн утгатай. Ижил шалтгаанаар: машин хэдэн утга гаргахаа өөрөө ч мэдэхгүй, хязгааргүй байж ч магадгүй. Тоолох шаардлагатай бол len(list(gen)) боломжтой ч энэ нь бүх утгыг санах ойд нэг дор ачаалдаг гэдгийг санаарай.
RuntimeError: generator raised StopIteration. “generator StopIteration шидлээ” гэсэн утгатай. Энэ алдаа өмнөх хичээлээс шууд ирдэг дадлаас үүсдэг: iterator class-д бид raise StopIteration бичдэг байсан тул generator дотор ч "дуусгахын тулд" ижил мөр бичих уруу таталт гардаг. Python 3.7-оос хойш generator дотор StopIteration шидэгдвэл Python түүнийг RuntimeError болгон хувиргадаг. Traceback-д хоёр exception харагдах бөгөөд дунд нь “The above exception was the direct cause of the following exception” гэсэн мөр байна. Энэ нь "дээрх exception нь доорх exception-ийн шууд шалтгаан болсон" гэсэн утгатай, ийм гинжин traceback-ийг бид Бүлэг 11-д бүрэн уншина. Шийдэл энгийн: generator-ийг дуусгахыг хүсвэл return бичих. Мөн generator дотор next()-ийг хамгаалалтгүй дуудаж, тэр нь хоосон түгээгчтэй тулгарсан үед ч ижил алдаа гарна. Ийм үед next()-д хоёр дахь argument өгөх нь хамгийн энгийн хамгаалалт.
yield агуулсан функцийг дуудахад код ажиллахгүй, зөвхөн эхэндээ зогссон generator объект буюу машин үүснэ. next() бүр машиныг дараагийн yield хүртэл ажиллуулж, утгыг гадагш гаргаад, бүх локал хувьсагч, функцийн доторх далд түгээгчид, кодын зогсолтын цэгийг хөлдөөж хадгална. Дараагийн next() яг тэр цэгээс, хувьсагчдын хадгалагдсан утгатайгаар үргэлжилнэ. Функцийн бие дуусах эсвэл return хүрэхэд Python StopIteration-ийг автоматаар шидэх тул бид түүнийг хэзээ ч гараар бичих ёсгүй. Generator объект бол бүрэн iterator. Нэг функцээс үүссэн машин бүр бие даасан төлөвтэй, мөн yield бүрт зогсдог тул хязгааргүй дарааллыг ч аюулгүй илэрхийлж чадна.
Энэ хичээлд бид generator-ийг функц хэлбэрээр бичсэн. Гэхдээ Python-д generator үүсгэх илүү богино арга бий. Та Course 1-ээс эхлэн list comprehension бичиж ирсэн: дөрвөлжин хаалт доторх нэг мөр давталт. Тэр хаалтыг дугуй хаалтаар солиход юу болох вэ? Дараагийн хичээлд бид generator expression-тэй танилцаж, хоёр хаалтын ялгааг санах ой ба хугацааны бодит хэмжилтээр харьцуулна. Энэ сургалтад анх удаа "энэ илүү хэмнэлттэй" гэсэн мэдэгдлийг бид тоогоор батлах болно.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.