Ачааллж байна...
Өнгөрсөн гурван хичээлд бид түгээгчийг гаднаас нь судалсан. Iterable ба iterator хоёрыг ялгаж, iter() ба next()-ээр давталтыг гараараа удирдаж, StopIteration дохиог сонсож сурсан. Тэр бүх хугацаанд бидний ашигласан түгээгчид бусдын бүтээсэн, Python-д бэлэн байсан list_iterator, zip, map, файл зэрэг байлаа. Энэ хичээлд бид эсрэг тал руу шилжинэ: өөрсдийн түгээгчийг эхнээс нь бүтээнэ. Үүний дараа таны бичсэн class for loop-д ороход Python-ий бэлэн төрлүүдээс огт ялгаагүй ажиллах болно.
Үүнд бидэнд хоёр тусгай method хэрэгтэй: __iter__ ба __next__. Course 2-т та __init__, __str__, __len__ зэрэг хоёр талдаа хоёр доогуур зураастай method-уудтай танилцсан. Англиар тэднийг “dunder method” буюу “double underscore” гэж нэрлэдэг. Тэр үед бид нэг чухал санааг сурсан: Python-ий суурь үйлдлүүд, тухайлбал print(), len(), нэмэх тэмдэг, далдуур тодорхой нэртэй method дууддаг. print(x) нь x.__str__()-ийг, len(x) нь x.__len__()-ийг дууддаг. Өмнөх хичээлүүдээс бид iter(x) ба next(x)-ийн тухай ижил зүйлийг мэдэж авсан. iter(x) нь x.__iter__()-ийг, next(x) нь x.__next__()-ийг дууддаг. Тиймээс эдгээр хоёр method-ыг бичвэл бидний class түгээгч болно.
Энэ хичээлийн гол санааг нэг өгүүлбэрээр хэлбэл: iterator гэдэг нь __next__ method-оороо дараагийн утгаа өгч, дуусахдаа StopIteration шиддэг, __iter__ method-оороо өөрийгөө буцаадаг аливаа объект юм. Энэ тодорхойлолтод ямар ч ид шид байхгүй. Бид үүнийг Countdown буюу "тоолох дараалал" гэсэн class бичиж, алхам бүрийг мөрдөж батална.
Одоог хүртэл бид түгээгчийг ажилд нь харж байсан бол одоо түгээгчийн ажлын зааврыг бичих ээлж ирлээ. Ангид шинэ сурагч ирж, багш түүнийг түгээгч болгон томилохоор шийдсэн гэж бодъё. Шинэ сурагч юу хийхээ огт мэдэхгүй тул багш хоёр зүйлийг тодорхой хэлэх ёстой. Нэгдүгээрт, "хэн нэгэн чамаас түгээгч хэн бэ гэж асуувал, би гэж хариулаарай." Хоёрдугаарт, "хэн нэгэн дараагийнхыг гуйвал, дараагийн дэвтрийг өг, хэдийг өгснөө тэмдэглэ, дэвтэр дууссан бол 'дууслаа' гэж хашгир."
Эдгээр хоёр заавар бол яг __iter__ ба __next__ юм. Эхний заавар нь iterator-ийн __iter__ өөрийгөө буцаадаг тэр дүрэм. Хоёр дахь заавар нь __next__-ийн гурван үүрэг: дараагийн утгаа өгөх, байрлалаа шинэчлэх, дуусахдаа StopIteration шидэх. Анзаарсан бол хоёр дахь заавар дотор "хэдийг өгснөө тэмдэглэ" гэсэн хэсэг байна. Түгээгчийн бүх ухаан тэр тэмдэглэлд оршдог. Python-ий class-д тэр тэмдэглэл нь self дээрх attribute байна.
Өгсөн тооноос эхлээд 1 хүртэл буурч тоолдог class бичье. Пуужин хөөргөхийн өмнөх “3, 2, 1” гэсэн тоолол шиг.
python
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
for n in Countdown(3):
print(n)3
2
1Кодыг хэсэг хэсгээр нь уншъя. __init__ нь Course 2-оос танил: объект үүсэхэд дуудагдаж, эхлэх тоог self.current-д хадгална. Энэ бол түгээгчийн "тэмдэглэл", одоо хаана явж байгаагаа санах газар. __iter__ ердөө нэг мөр: return self. Зүйрлэлийн эхний заавар, "түгээгч хэн бэ гэвэл би". __next__ бол гол ажлын хэсэг. Эхлээд дууссан эсэхээ шалгана: self.current 0 эсвэл түүнээс бага болсон бол StopIteration шиднэ. raise гэдэг нь exception-ийг өөрсдөө шидэх түлхүүр үг бөгөөд өмнөх хичээлд бидний сонссон "дууслаа" дохиог энэ удаа бид өөрсдөө өгч байна. Дуусаагүй бол одоогийн утгыг value-д хадгалж, self.current-ийг нэгээр бууруулж, хадгалсан утгаа буцаана.
Яагаад эхлээд value-д хадгалаад дараа нь бууруулдаг вэ гэж асууж магадгүй. Хэрэв эхлээд бууруулчихаад self.current-ийг буцаавал 3-ын оронд 2-оос эхэлнэ. "Өгөх утгаа барь, тэмдэглэлээ шинэчил, барьсан утгаа өг" гэсэн дараалал нь түгээгч бичихэд маш түгээмэл загвар тул энэ хичээлийн бүх жишээнд та түүнийг дахин харна.
Одоо энэ кодыг санах ойн түвшинд мөрдье. Өмнөх хичээлийн мэдлэгээр бид for loop-ийн ард юу болдгийг мэднэ: iter() нэг удаа, next() олон удаа, StopIteration чимээгүй баригдана. Одоо тэр дуудлага бүр бидний бичсэн method руу хэрхэн ордгийг харна.
Алхам 1 (Countdown(3)). Шинэ Countdown объект үүсч, __init__ дуудагдана. Санах ойд: нэг Countdown объект, түүний self.current = 3.
Алхам 2 (for loop эхлэх). for loop далдуур iter() дуудна, тэр нь бидний __iter__-ийг дуудна. __iter__ self-ийг буцаана. Санах ойд шинэ объект үүссэнгүй: for loop-ийн барьж буй түгээгч бол Countdown объект өөрөө. self.current = 3 хэвээр.
Алхам 3 (эхний next). for loop next() дуудна, тэр нь __next__-ийг дуудна. 3 > 0 тул StopIteration шидэгдэхгүй. value = 3 болно, self.current 2 болно, 3 буцна. n = 3, хэвлэгдэнэ. Санах ойд: self.current = 2.
Алхам 4 (хоёр дахь next). 2 > 0. value = 2, self.current = 1, 2 буцна, хэвлэгдэнэ. Санах ойд: self.current = 1.
Алхам 5 (гурав дахь next). 1 > 0. value = 1, self.current = 0, 1 буцна, хэвлэгдэнэ. Санах ойд: self.current = 0.
Алхам 6 (дөрөв дэх next). __next__ дуудагдана. Энэ удаа 0 <= 0 нөхцөл үнэн тул raise StopIteration ажиллана. Бидний бичсэн exception for loop-ийн далд try/except-д баригдаж, loop чимээгүй зогсоно. Санах ойд: self.current = 0 хэвээр үлдсэн, Countdown объект "хоосорсон" байдалд байна.
Энэ trace-ийн гол сургамж нь алхам 2 ба алхам 6. Алхам 2-т for loop шинэ түгээгч хүсэхэд манай объект "би өөрөө" гэж хариулсан. Алхам 6-д for loop-ийг зогсоосон дохиог Python биш, бидний бичсэн raise мөр гаргасан. Python-ий бэлэн list_iterator ч яг ийм дүрмээр, зөвхөн C хэл дээр бичигдсэн байдаг.
Өмнөх хичээлийн хэрэгслүүдээр бид үүнийг гараар ч шалгаж болно:
python
c = Countdown(2)
print(iter(c) is c)
print(next(c))
print(next(c))
print(next(c, "дууслаа"))True
2
1
дууслааiter(c) is c True гарсан нь бидний class жинхэнэ iterator гэдгийг баталж байна, өмнөх хичээлийн zip, файлтай яг адил. next()-ийн хоёр дахь argument ч манай class-тай хэвийн ажилласан, учир нь next() бидний шидсэн StopIteration-ийг барьж, анхдагч утгыг буцаасан. Бид class-даа энэ боломжийн тухай нэг ч мөр бичээгүй. Протоколыг дагасан тул Python-ий бүх давталтын хэрэгслүүд бидний class-тай автоматаар ажиллаж байна.
Жишээ 1: протокол дагавал бүх хэрэгсэл ажиллана
Сүүлийн санааг илүү тод харъя. Python-д iterable хүлээн авдаг олон функц бий: list(), sum(), max(), sorted(), tuple(), in операторыг хүртэл. Тэд бүгд далдуур iter() ба next() ашигладаг. Тиймээс бидний Countdown тэдэнтэй бүгдтэй нь ажиллана.
python
print(list(Countdown(5)))
print(sum(Countdown(4)))
print(sorted(Countdown(3)))
print(2 in Countdown(5))[5, 4, 3, 2, 1]
10
[1, 2, 3]
Truelist() түгээгчийг хоосолж, утга бүрийг шинэ list-д хийв. sum() бүх утгыг нэмэв: 4 + 3 + 2 + 1 = 10. sorted() бүх утгыг цуглуулаад өсөх дарааллаар эрэмбэлэв. 2 in Countdown(5) нь түгээгчээс 5, 4, 3, 2 гэж нэг нэгээр асуугаад 2-ыг олмогц True буцаав, 1-ийг хэзээ ч асуугаагүй. Мөр бүрт бид шинэ Countdown объект үүсгэсэн гэдгийг анхаар. Яагаад гэдгийг дараагийн жишээ тайлбарлана.
Энэ бол протокол гэдэг санааны анхны бодит амт юм. Protocol гэдэг нь Python-д "хэрэв чи эдгээр method-уудыг тодорхой дүрмээр бичвэл, чамайг тэр төрлийн гишүүнд тооцно" гэсэн гэрээ юм. Countdown list-ээс удамшаагүй, ямар нэг тусгай бүртгэлд нэрээ бичүүлээгүй. Тэр зүгээр л __iter__ ба __next__-ийг зөв бичсэн бөгөөд энэ нь хангалттай. Түвшин 3-т бид энэ санааг нэмэх, харьцуулах, индексээр хандах зэрэг олон үйлдэлд өргөтгөнө.
Жишээ 2: нэг удаагийн асуудал ба түүний шийдэл
Өмнөх хичээлд бид zip, файл зэрэг iterator-ууд нэг удаагийн хэрэглээтэй гэдгийг харсан. Бидний Countdown ч мөн iterator тул ижил зан чанартай. Шалгая:
python
launch = Countdown(3)
for n in launch:
print(n)
print("--- дахин ---")
for n in launch:
print(n)3
2
1
--- дахин ---Хоёр дахь loop юу ч хэвлэсэнгүй. Trace-ийн алхам 6-ийн төгсгөлийн төлөвийг санаарай: self.current = 0 хэвээр үлдсэн. Хоёр дахь for loop iter(launch) дуудахад __iter__ дахин self-ийг, тэр хоосорсон объектыг буцаана. Эхний next() шууд 0 <= 0-ыг олж, StopIteration шиднэ. Энэ бол алдаа биш, бидний бичсэн дизайны үр дүн. Гэхдээ "пуужингийн тоолол" гэдэг зүйлийг хэдэн ч удаа ажиллуулж болох мэт санагдах тул энэ зан чанар энд тохиромжгүй.
Шийдэл нь өмнөх хичээлийн хайрцаг ба түгээгчийн ялгаанд байна. List хэдэн ч удаа давтагддаг, учир нь list өөрөө түгээгч биш, loop бүрд шинэ түгээгч томилдог. Бид ч мөн ижил зүйл хийж болно: ажлыг хоёр class-д хуваах. Нэг нь хайрцаг (iterable), нөгөө нь түгээгч (iterator).
python
class Countdown:
def __init__(self, start):
self.start = start
def __iter__(self):
return CountdownIterator(self.start)
class CountdownIterator:
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 valueCountdownIterator бол бидний өмнөх Countdown-тай яг ижил, зөвхөн нэр нь өөрчлөгдсөн. Шинэ Countdown class-д __next__ огт байхгүй, тэр зөвхөн эхлэх тоог self.start-д хадгалдаг. Түүний __iter__ нь self-ийг биш, харин шинэ CountdownIterator объект буцаадаг. Анзаарвал Countdown-д self.current байхгүй, self.start л бий. Хайрцаг байрлал санадаггүй, хэдээс эхлэхээ л мэддэг.
Одоо дахин хоёр удаа давтаж trace хийе:
python
launch = Countdown(3)
for n in launch:
print(n)
print("--- дахин ---")
for n in launch:
print(n)3
2
1
--- дахин ---
3
2
1Алхам 1. launch үүснэ. Санах ойд: Countdown объект, self.start = 3. Ямар ч түгээгч байхгүй.
Алхам 2. Эхний for loop iter(launch) дуудна. Countdown-ийн __iter__ шинэ CountdownIterator(3) үүсгэж буцаана. Санах ойд: Countdown (start = 3), мөн нэргүй түгээгч А (current = 3).
Алхам 3. Түгээгч А 3, 2, 1-ийг өгч, current = 0 болоод StopIteration шиднэ. Loop зогсоно. Түгээгч А-г хэн ч барихаа больсон тул Python түүнийг хаяна. Санах ойд: зөвхөн Countdown (start = 3), огт өөрчлөгдөөгүй.
Алхам 4. Хоёр дахь for loop iter(launch) дуудна. __iter__ дахин шинэ CountdownIterator(3) үүсгэнэ. Санах ойд: Countdown (start = 3), мөн шинэ түгээгч Б (current = 3).
Алхам 5. Түгээгч Б 3, 2, 1-ийг бүрэн өгнө.
Энэ бол яг өмнөх хичээлийн names list-ийн trace-тэй ижил загвар. Countdown одоо list шиг хайрцаг болж, хоёр дахь loop шинэ түгээгч авсан тул бүрэн ажиллав. Энэ хоёр class-ийн загвар нь Python-ий бэлэн төрлүүдийн бодит бүтэц юм: list ба list_iterator, dict ба dict_keyiterator, range ба range_iterator бүгд яг ийм хос.
Аль загварыг хэзээ сонгох вэ? Нэг class-тай хувилбар (iterator өөрөө) нь нэг удаа урсаж дуусдаг зүйлд тохиромжтой: файлаас мөр унших, сүлжээнээс өгөгдөл хүлээн авах, эсвэл давтах ямар ч утгагүй үйл явц. Хоёр class-тай хувилбар (iterable + iterator) нь дахин дахин давтагдах ёстой цуглуулгад тохиромжтой: тоолол, тохиргооны жагсаалт, өөрийн бүтээсэн өгөгдлийн бүтэц. Хэрэглэгчийн хүлээлт ихэвчлэн хоёр дахь хувилбар руу хардаг тул эргэлзвэл түүнийг сонгоорой.
Жишээ 3: бодит хэрэглээ — багцаар хуваагч
Countdown ойлголтыг тод харуулдаг ч бодит ажилд ховор хэрэглэгдэнэ. Одоо илүү практик түгээгч бичье. Олон API болон имэйл үйлчилгээ нэг удаад хязгаартай тооны элемент хүлээн авдаг. Жишээлбэл "нэг хүсэлтэд дээд тал нь 3 хүлээн авагч." Тийм үед урт жагсаалтыг тогтмол хэмжээтэй багцуудад хуваах хэрэгтэй болдог. Үүнийг iterable + iterator загвараар бичье.
python
class Batches:
def __init__(self, items, size):
self.items = items
self.size = size
def __iter__(self):
return BatchIterator(self.items, self.size)
class BatchIterator:
def __init__(self, items, size):
self.items = items
self.size = size
self.position = 0
def __iter__(self):
return self
def __next__(self):
if self.position >= len(self.items):
raise StopIteration
batch = self.items[self.position:self.position + self.size]
self.position += self.size
return batch
students = ["Бат", "Сараа", "Тэмүүлэн", "Нараа", "Болд", "Оюун", "Ганаа"]
for group in Batches(students, 3):
print(group)['Бат', 'Сараа', 'Тэмүүлэн']
['Нараа', 'Болд', 'Оюун']
['Ганаа']Batches бол хайрцаг: жагсаалт болон багцын хэмжээг хадгалж, loop бүрд шинэ BatchIterator өгдөг. BatchIterator-ийн тэмдэглэл нь self.position буюу дараагийн багц хаанаас эхлэх байрлал. __next__ эхлээд position жагсаалтын уртад хүрсэн эсэхийг шалгаж, хүрсэн бол StopIteration шиднэ. Хүрээгүй бол Course 1-ээс танил slice ашиглан position-оос position + size хүртэлх хэсгийг авна. Дараа нь position-ийг size-аар нэмэгдүүлж, багцыг буцаана. "Өгөх утгаа барь, тэмдэглэлээ шинэчил, барьсан утгаа өг" гэсэн дараалал энд ч хэвээр байна.
Сүүлийн багцад нэг л нэр байгааг анзаараарай. position = 6 үед slice 6-аас 9 хүртэл авахыг оролдох боловч жагсаалтад 7 элемент л бий. Slice хил давахад алдаа шидэхгүй, байгаа хэсгээ л буцаадаг тул ['Ганаа'] гарч ирлээ. Дараагийн дуудлагад position = 9 болж, 9 >= 7 тул StopIteration шидэгдэнэ. Энэ class Countdown-ийн ижил бүтэцтэй боловч одоо бодит асуудлыг шийдэж байна. Хайрцаг ба түгээгчийн хуваалттай тул нэг Batches объектыг хэд дахин давтаж болно, жишээлбэл эхний удаад багцуудыг шалгаж, хоёр дахь удаад бодитоор илгээж болно.
Хүсвэл Countdown-ийг 1-ээс өгсөн тоо хүртэл өсөх CountUp болгон өөрчилж үзээрэй. __next__ дотор зөвхөн хэдэн тэмдэгт өөрчлөгдөнө. Сонирхвол BatchIterator-ийн __next__-д нэг print() нэмж, position-ийн утгыг алхам бүрт хэвлүүлээд, дээрх trace-ийн дагуу таны таамагтай тохирч байгаа эсэхийг шалгаж болно. Мөн Batches(students, 3)-ийг list()-ээр хоёр удаа хувиргаж, хоёр үр дүн ижил гарч байгааг өөрийн нүдээр харах нь хоёр class-ийн загварын утгыг батлах энгийн туршилт юм. Эдгээр бүгд санал бөгөөд дараагийн хичээлүүд үүнийг хийсэн гэж үзэхгүй.
TypeError: ‘Countdown’ object is not iterable. “Countdown объект бол iterable биш” гэсэн утгатай. Class-д __iter__ method байхгүй, эсвэл нэрийг нь буруу бичсэн үед гарна. Хамгийн түгээмэл шалтгаан нь доогуур зураасны тоо: _iter_ (нэг нэг), __iter_ (хоёр ба нэг), эсвэл __iterator__ гэх мэт. Python яг __iter__ гэсэн нэрийг хайдаг, ойролцоо нэрийг огт танихгүй. Мөн method-ыг class дотор бус, санамсаргүй class-ийн гадна бичсэн бол ч энэ алдаа гарна, тиймээс догол мөрийг шалгаарай.
TypeError: iter() returned non-iterator of type ‘Countdown’. “iter() iterator биш, ‘Countdown’ төрлийн объект буцаалаа” гэсэн утгатай. Энэ нь илүү нарийн алдаа: __iter__ байгаа ч түүний буцаасан объектод __next__ байхгүй. Ихэвчлэн __iter__-д return self бичсэн атлаа __next__-ийг бичихээ мартсан, эсвэл нэрийг нь буруу бичсэн (__nxt__, next) үед гарна. Хоёр class-ийн загварт __iter__ нь iterator class-ийн оронд санамсаргүй өөр объект, жишээлбэл self.items list-ийг буцаасан үед ч ижил төстэй мессеж гарна. Шийдэл нь __iter__ юу буцааж байгааг, тэр объектод зөв нэртэй __next__ байгаа эсэхийг шалгах.
Хязгааргүй None хэвлэгдэх. Энэ алдаа мессежгүй, харин програм зогсохгүй None, None, None гэж хэвлэсээр байна. Шалтгаан нь __next__ дотор raise StopIteration мөрийг мартсан эсвэл нөхцөлийг нь буруу бичсэн явдал. Python-д функц return-гүйгээр дуусвал None буцаадаг тул түгээгч "дууслаа" гэхийн оронд None өгсөөр байна, for loop түүнийг хэвийн утга гэж хүлээн авна. Ctrl+C дарж програмыг зогсоогоод, StopIteration шидэх нөхцлийг шалгаарай.
Нэг утга хязгааргүй давтагдах. Мөн мессежгүй алдаа: 3, 3, 3, 3 гэж зогсолтгүй хэвлэгдэнэ. Шалтгаан нь __next__ дотор тэмдэглэлээ шинэчлэх мөрийг, тухайлбал self.current -= 1 эсвэл self.position += self.size-ийг мартсан явдал. Түгээгч байрлалаа шинэчлэхгүй бол үргэлж нэг ижил газраас нэг ижил утга өгнө. Ctrl+C-ээр зогсоогоод, __next__ бүр дуудагдахад self дээрх ямар нэг зүйл заавал өөрчлөгдөж байгаа эсэхийг шалгаарай.
Өөрийн iterator бүтээхэд хоёр method хэрэгтэй. __iter__ нь iterator-т өөрийгөө буцаадаг, __next__ нь дараагийн утгаа өгч, байрлалаа self дээр шинэчилж, дуусахдаа raise StopIteration-ээр дохио өгдөг. Countdown-ийн trace-ээс бид for loop-ийн дуудлага бүр бидний бичсэн method руу шууд ордгийг, мөн loop-ийг зогсоох дохиог бид өөрсдөө өгдгийг харлаа. Протоколыг дагасан class list(), sum(), sorted(), in зэрэг бүх давталтын хэрэгсэлтэй автоматаар ажилладаг. Нэг class-тай iterator нэг удаагийн хэрэглээтэй тул дахин давтагдах ёстой зүйлд iterable class ба iterator class гэсэн хоёр хуваалтыг хэрэглэнэ. Энэ нь Python-ий бэлэн төрлүүдийн бодит бүтэц юм. Ингээд Бүлэг 1 дуусч, for loop-ийн ард юу байдгийг бид бүрэн ойлголоо.
Гэхдээ та нэг зүйлийг анзаарсан байх. Энгийн тоолол бичихэд хоёр class, хэдэн method, байрлал хадгалах attribute, StopIteration-ийн нөхцөл хэрэгтэй болж, бараг хорин мөр болсон. Түгээгчийн тэмдэглэлийг гараар хөтлөх нь ажил ихтэй. Дараагийн бүлэгт бид Python-ий хамгийн гоё шийдлүүдийн нэг рүү буцна: generator. yield түлхүүр үг нь энэ бүх ажлыг, байрлал санах, StopIteration шидэх, __iter__ ба __next__-ийг бүгдийг нь автоматаар хийж, Countdown-ийг дөрөвхөн мөрөнд багтаана. Гэхдээ тэр дөрвөн мөрийн ард яг энэ хичээлд бичсэн механизм ажилладгийг та одоо мэдэж байгаа тул generator ид шид биш, ухаалаг товчлол болж харагдах болно.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.