Ачааллж байна...
Энэ хичээл тойргийг хаана. Сургалтын эхний бүлэгт бид for loop-ийн ард нуугдсан iterator протоколыг нээж, хайрцаг ба түгээгчийн ялгааг ойлгож, Countdown class-ийг хоёр хэсэгт хуваан бичсэн: давтан хэрэглэгдэх iterable ба нэг удаагийн iterator. Хоёр дахь бүлэгт yield-ийн түр зогсоох машинтай танилцаж, тэр хорин мөрийн ажлыг дөрвөн мөрөнд багтаасан. Хичээл 8.2-т Playlist for loop-д __iter__-гүйгээр, хуучин sequence протоколоор орсон бөгөөд бид тэр замын хязгаарлалтыг тэмдэглэж, жинхэнэ __iter__-ийг энэ хичээлд нэмнэ гэж амласан.
Энэ хичээлд эдгээр гурван бүлгийн мэдлэг нэг жижиг method-д нийлнэ. Бид Playlist-д __iter__ нэмнэ, бас generator функц хэлбэрээр: method-ын биед yield бичнэ. Энэ нь гурван мөр бөгөөд Хичээл 1.4-ийн хоёр class-тай загварын бүх ажлыг автоматаар хийдэг. Яагаад гэдгийг trace-ээр алхам алхмаар харна. Дараа нь нэг Playlist дээр хоёр for loop-ийг үүрлэн ажиллуулж, тэдгээр бие биедээ саад болохгүй байгааг баталж, энэ шинж чанар хаанаас ирж байгааг тайлбарлана.
Хичээлийн хоёр дахь хагас нь шинэ боломжууд: Playlist-ийг өөр өөр дарааллаар давтах method-ууд, жишээлбэл богиноос урт руу эсвэл тодорхой хугацаанаас урт дуунуудыг л. Энэ нь Хичээл 7.1-ийн classmethod generator-тэй төстэй, гэхдээ энгийн method хэлбэрээр. Мөн давталтын явцад жагсаалтыг өөрчлөх аюулыг шударгаар харуулна: энэ бол алдааны мессеж огт өгдөггүй, хамгийн хэцүү олддог алдаануудын нэг.
Хичээл 1.2-ын зүйрлэл рүү буцъя: дэвтрээр дүүрэн хайрцаг ба түгээгч сурагч. Хайрцаг өөрөө тараадаггүй, тэр түгээгч томилдог. Нэг хайрцгаас хэдэн ч түгээгч томилж болох бөгөөд тус бүр эхнээс нь эхэлдэг. Хичээл 1.4-т бид түгээгчийг тусдаа class болгон бичсэн: CountdownIterator. Тэр байрлалаа self.current-д тэмдэглэж, __next__ бүрт гараар шинэчилдэг байсан.
Хичээл 2.1-т бид шинэ төрлийн түгээгчтэй танилцсан: түр зогсоох товчтой машин. Тэр байрлалаа өөрөө санадаг, тэмдэглэл хөтлөх шаардлагагүй, бүх төлвөө yield дээр хөлддөг. Generator функцийг дуудах бүрт шинэ машин үүсдэг.
Одоо энэ хоёр санааг нийлүүлье. Хайрцагт "түгээгч томил" гэсэн хүсэлт ирэх бүрт, __iter__ дуудагдах бүрт, хайрцаг гараар бичсэн CountdownIterator-ийн оронд шинэ түр зогсоох машин үйлдвэрлэдэг байвал? __iter__-ийг generator функц болгоход яг ийм зүйл болно. Хайрцаг ба түгээгч хоёр тусдаа class биш, нэг class-ийн нэг method-д нийлнэ: method нь хайрцгийн "түгээгч томилох" хариулт, түүний биеийн доторх yield нь машин.
__iter__Хичээл 8.2-ын Playlist-ийг үргэлжлүүлье. Song class болон Playlist-ийн өмнөх method-уудыг товчилж, шинэ method-д төвлөрье:
python
from dataclasses import dataclass
@dataclass(frozen=True)
class Song:
title: str
artist: str
seconds: int
class Playlist:
def __init__(self, name, songs=None):
self.name = name
self._songs = [] if songs is None else list(songs)
def __len__(self):
return len(self._songs)
def __getitem__(self, index):
print(f" [__getitem__({index!r})]")
if isinstance(index, slice):
return type(self)(f"{self.name} (хэсэг)", self._songs[index])
return self._songs[index]
def __iter__(self):
for song in self._songs:
yield song
morning = Playlist("Өглөөний", [
Song("Ээж минь", "Б.Шарав", 245),
Song("Миний нутаг", "Д.Жаргалсайхан", 198),
Song("Талын салхи", "Н.Жанцанноров", 212),
])
for song in morning:
print(song.title)Ээж минь
Миний нутаг
Талын салхиХичээл 8.2-ын __getitem__ чагнуурыг санаатайгаар үлдээсэн. Энэ удаа тэр юу ч хэвлээгүй. Python for loop-д эхлээд __iter__ хайдаг бөгөөд олдсон тул хуучин sequence протокол руу огт шилжээгүй. Индексээр хандах __getitem__ одоо зөвхөн morning[0] гэх мэт шууд хандалтад л ажиллана.
__iter__-ийн бие гурван мөр: доторх list-ийн дуу бүрийг yield хийнэ. Биед yield байгаа тул Хичээл 2.1-ийн дүрмээр энэ method generator функц болсон.
Trace: __iter__ дуудагдах бүрт шинэ машин
Алхам 1 (for loop эхлэх). for loop iter(morning)-ийг дуудна. iter() Playlist-ийн __iter__-ийг олж, morning.__iter__() гэж дуудна.
Алхам 2 (__iter__ дуудагдах). __iter__ бол generator функц тул Хичээл 2.1-ийн гол баримт энд хэрэгжинэ: түүний бие огт ажиллахгүй, оронд нь эхэндээ зогссон шинэ generator объект үүснэ. Санах ойд: Playlist объект (хайрцаг), шинэ generator А (машин), түүний хөлдсөн frame-д self = morning, эхэндээ зогссон.
Алхам 3 (iter()-ийн шалгалт). Хичээл 1.4-ийн алдааг санаарай: “iter() returned non-iterator”. iter() __iter__-ийн буцаасан зүйл iterator мөн эсэхийг шалгадаг. Generator объект бол бүрэн iterator, __next__-тэй, тиймээс шалгалт давна.
Алхам 4 (эхний next). for loop generator А-аас next() асууна. Машин асна: for song in self._songs: мөр доторх list-ээс шинэ list_iterator авна, Хичээл 1.2. Эхний дууг yield хийж хөлдөнө. Хөлдсөн frame-д: self, доторх list_iterator 1-р байрлалд, song = "Ээж минь".
Алхам 5-6 (дараагийн next-үүд). Машин yield-ийн дараагаас сэрж, дараагийн дууг yield хийнэ.
Алхам 7 (дуусах). Доторх for loop дуусч, generator-ийн бие төгсгөлдөө хүрч, Python StopIteration-ийг автоматаар шиднэ. Гадна талын for loop чимээгүй зогсоно. Generator А хаягдана. Санах ойд: зөвхөн Playlist, огт өөрчлөгдөөгүй.
Одоо Хичээл 1.4-ийн хоёр class-тай Countdown-тай харьцуулаарай. Тэнд Countdown нь хайрцаг байсан бөгөөд түүний __iter__ нь CountdownIterator(self.start) гэж тусдаа class-ийн шинэ объектыг буцаадаг байсан. CountdownIterator өөрийн __iter__, __next__, байрлалын тэмдэглэл, StopIteration шидэх нөхцөлтэй байсан. Энд Playlist нь хайрцаг, generator А нь CountdownIterator-ийн үүргийг гүйцэтгэж байна. Гэвч бид тэр class-ийг бичээгүй: Python generator функцээс түүнийг автоматаар үйлдвэрлэсэн. Байрлалын тэмдэглэл нь хөлдсөн frame, StopIteration нь функцийн төгсгөл. Хичээл 1.4-т бид "хайрцаг ба түгээгчийн хоёр class-ийн загвар бол Python-ий бэлэн төрлүүдийн бодит бүтэц" гэж хэлсэн. Generator хэлбэрийн __iter__ нь тэр бүтцийг гурван мөрөнд өгдөг.
Delegation-ийн хувилбар
Энэ тохиолдолд доторх list аль хэдийн давтагддаг тул бүр богино хувилбар бий:
python
def __iter__(self):
return iter(self._songs)Энэ нь доторх list-ээс шинэ list_iterator авч шууд буцаана. Хичээл 8.2-ын __len__ ба __getitem__-ийн delegation загвартай ижил: ажлыг доторх list-д даалгах. Хоёр хувилбар хоёулаа зөв бөгөөд дуудах бүрт шинэ түгээгч өгнө. Тэгвэл generator хувилбарыг хэзээ сонгох вэ? Давталтад ямар нэг логик нэмэх шаардлагатай үед: шүүх, хувиргах, дарааллыг өөрчлөх. Энгийн delegation-д iter(self._songs) товч. Логик хэрэгтэй үед generator. Мөн Python-д yield from self._songs гэсэн бичлэг бий: энэ нь for song in self._songs: yield song-ийн товчлол бөгөөд бусдын кодонд харж магадгүй.
Жишээ 1: үүрлэсэн давталт — бие даасан машинууд
Дуудах бүрт шинэ машин үүсдэг нь онолын нарийн ширийн зүйл биш, практикт шийдвэрлэх ач холбогдолтой. Өглөөний жагсаалтаас хоёр өөр дууны бүх хосыг гаргая, жишээлбэл дараалан тоглуулах хосуудыг сонгох:
python
pairs = []
for first in morning:
for second in morning:
if first != second:
pairs.append((first.title, second.title))
print(len(pairs))
for a, b in pairs[:3]:
print(f"{a} -> {b}")6
Ээж минь -> Миний нутаг
Ээж минь -> Талын салхи
Миний нутаг -> Ээж миньГурван дуунаас өөр өөр хоёрын дараалсан 6 хос. Trace: гадна for loop __iter__-ийг дуудаж generator А авна. А "Ээж минь"-ийг өгнө. Дотоод for loop дахин __iter__-ийг дуудаж, шинэ generator Б авна. Б гурван дууг бүгдийг өгч дуусна. Гадна loop А-аас дараагийнхыг асууна. А "Миний нутаг" дээр үргэлжилнэ, Б-ийн ажил түүнд огт нөлөөлөөгүй. Дотоод loop дахин __iter__-ийг дуудаж, шинэ generator В авна, гэх мэт. Аль ч мөчид хоёр машин зэрэг амьд, тус бүр өөрийн хөлдсөн frame, өөрийн доторх list_iterator-тэй.
Хэрэв Playlist нь Хичээл 1.4-ийн нэг class-тай Countdown шиг, __iter__ нь self-ийг буцаадаг iterator байсан бол яах байсан бэ? Гадна ба дотоод loop нэг түгээгчийг хуваалцана. Дотоод loop эхний давталтанд бүх дууг хоослох тул гадна loop дараагийн дуу олохгүй, ердөө хоёр хос гарах байсан. Энэ нь Хичээл 1.2 ба 1.4-т бидний харсан "нэг удаагийн түгээгч" асуудал, гэхдээ илүү далд хэлбэрээр. Generator хэлбэрийн __iter__ үүнээс автоматаар хамгаална.
Жишээ 2: өөр дарааллаар давтах method-ууд
__iter__ бол Playlist-ийн "анхдагч" давталт: for loop-д шууд ордог. Гэвч нэг жагсаалтыг өөр өөр аргаар давтах хэрэгцээ гардаг. Тэдгээрийг тусдаа generator method болгон нэмье:
python
def by_duration(self):
for song in sorted(self._songs, key=lambda s: s.seconds):
yield song
def longer_than(self, seconds):
for song in self._songs:
if song.seconds > seconds:
yield song
def __reversed__(self):
for song in reversed(self._songs):
yield songpython
print("Богиноос урт руу:")
for song in morning.by_duration():
print(" ", song.title, song.seconds)
print("3:30-аас урт:")
for song in morning.longer_than(210):
print(" ", song.title)
print("Урвуу:")
for song in reversed(morning):
print(" ", song.title)Богиноос урт руу:
Миний нутаг 198
Талын салхи 212
Ээж минь 245
3:30-аас урт:
Ээж минь
Талын салхи
Урвуу:
Талын салхи
Миний нутаг
Ээж миньby_duration ба longer_than бол энгийн method-ууд бөгөөд биед нь yield байгаа тул дуудахад generator буцаана. Тэдгээрийг for loop-д шууд өгнө: morning.by_duration(), хаалттай, учир нь эхлээд method-ыг дуудаж generator авах хэрэгтэй. by_duration дотор Хичээл 3.2-ын key= ашигласан. longer_than Хичээл 2.3-ын шүүлтүүр шатны хэлбэртэй, нэг жагсаалтын pipeline. Тус бүрийг дуудах бүрт шинэ машин үүснэ.
__reversed__ тусгай нэртэй: reversed() суурь функц түүнийг хайдаг. Хичээл 8.2-т reversed() __len__ ба __getitem__-ээр хуучин аргаар ажилласан. Одоо __reversed__ байгаа тул түүнийг шууд хэрэглэнэ. Нэг шударга тэмдэглэл: энд __reversed__ бичих нь заавал биш, хуучин зам ч ижил үр дүн өгнө. Харин доторх хадгалалт индексээр хандах боломжгүй байсан бол, жишээлбэл дуунууд файлаас уншигддаг бол, __reversed__ цорын ганц зам болно.
Жишээ 3: протоколууд нийлэх нь
Одоо Playlist-ийн бүх протокол нэг дор ажиллах нь. Бүлэг 1-ээс хойш сурсан хэрэгслүүд нэмэлт ажилгүйгээр Playlist-тэй ажиллана, учир нь бүгд iter() ба next()-ээр дамждаг:
python
from itertools import cycle, islice
total = sum(song.seconds for song in morning)
print(f"Нийт: {total // 60} мин {total % 60} сек")
longest = max(morning, key=lambda s: s.seconds)
print("Хамгийн урт:", longest.title)
titles = list(map(lambda s: s.title, morning))
print(titles)
print("Давтан тоглуулах горим:")
for song in islice(cycle(morning), 5):
print(" ", song.title)Нийт: 10 мин 55 сек
Хамгийн урт: Ээж минь
['Ээж минь', 'Миний нутаг', 'Талын салхи']
Давтан тоглуулах горим:
Ээж минь
Миний нутаг
Талын салхи
Ээж минь
Миний нутагМөр бүр өөр бүлгийн хэрэгсэл: generator expression ба sum() Бүлэг 2-оос, max ба key= Бүлэг 3-аас, map Хичээл 3.1-ээс, cycle ба islice Хичээл 2.4-өөс. cycle нь iterable-ийг хязгааргүй давтдаг itertools-ийн хэрэгсэл бөгөөд хөгжим тоглуулагчийн "давтах" товчийг дуурайж байна. islice хязгааргүй урсгалыг таван дуунд таслав. Бид эдгээрийн аль нэгд нь Playlist-ийн тухай нэг ч мөр бичээгүй. Тэдгээр Playlist-ийг list-ээс ялгах боломжгүй, учир нь Playlist list-тэй ижил протоколыг дагадаг.
Энэ бол бүлгийн гол санааны бүрэн илрэл. Хичээл 8.2-ын клубын гэрээ: заалтуудад гарын үсэг зурвал бүх байгууламж нээгдэнэ. Бүлэг 1-ийн iterator протокол, Бүлэг 2-ын generator, Бүлэг 8-ын container протокол нэг жижиг class-д нийлж, Python-ий бүх стандарт сан түүнтэй ажилладаг болсон.
Хүсвэл Playlist-д shuffled(seed=None) гэсэн generator method нэмж, random.Random(seed)-ээр доторх жагсаалтын хуулбарыг холиод нэг нэгээр нь yield хийдэг болгож болно. Seed өгвөл холилт давтагдах, Хичээл 3.4-ийн генераторын адил. Сонирхвол Жишээ 1-ийн үүрлэсэн давталтыг __iter__ нь return self буцаадаг, __next__-тэй хувилбар дээр туршиж, хэдэн хос гарахыг өөрийн нүдээр харж болно. Энэ нь generator хэлбэрийн давуу талыг хамгийн тод харуулна. Мөн __iter__-ийн биед print нэмж, sum(...), max(...), list(map(...)) мөрүүд тус бүр хэдэн удаа __iter__-ийг дуудаж байгааг тоолох нь trace-ийн "дуудлага бүрт шинэ машин" санааг батлах энгийн туршилт. Эдгээр бүгд санал бөгөөд дараагийн хичээлүүд үүнийг хийсэн гэж үзэхгүй.
TypeError: iter() returned non-iterator of type ‘list’. “iter() iterator биш, ‘list’ төрлийн объект буцаалаа” гэсэн утгатай. Хичээл 1.4-ийн танил алдаа, энэ удаа өөр шалтгаанаар. __iter__ дотор return self._songs гэж доторх list-ийг өөрийг нь буцаахад гарна. List бол iterable, iterator биш, Хичээл 1.2-ын гол ялгаа. __iter__ iterator буцаах ёстой: return iter(self._songs) эсвэл generator хэлбэр.
TypeError: iter() returned non-iterator of type ‘NoneType’. “iter() iterator биш, ‘NoneType’ буцаалаа” гэсэн утгатай. __iter__-ийн биед yield ч, return ч байхгүй үед гарна, жишээлбэл давталтыг бичсэн атлаа yield-ийн оронд print бичсэн. Тэр үед method энгийн функц болж None буцаана. Generator хэлбэрт yield заавал байх ёстой.
Давталтын явцад элемент алгасагдах. Мессежгүй, энэ хичээлийн хамгийн аюултай алдаа. Давталтын явцад доторх жагсаалтаас элемент устгахад гарна:
python
def remove_shorter_than(self, seconds):
for song in self._songs:
if song.seconds < seconds:
self._songs.remove(song)Хэрэв богино хоёр дуу дараалан байвал хоёр дахь нь устгагдахгүй үлдэнэ. Trace: доторх list_iterator байрлалаа тоогоор санадаг. Эхний богино дуу 0-р байрлалд байгаад устгагдахад үлдсэн бүх дуу нэг байрлалаар урагш шилжинэ. Хоёр дахь богино дуу одоо 0-р байрлалд, гэтэл iterator дараагийн алхамд 1-р байрлалыг харна. Хоёр дахь богино дууг хэн ч шалгаагүй. Python ямар ч алдаа өгөхгүй. Шийдэл нь давтаж байгаа жагсаалтаа өөрчлөхгүй байх: шинэ жагсаалт бүтээх, self._songs = [s for s in self._songs if s.seconds >= seconds], эсвэл хуулбар дээр давтах, for song in list(self._songs):.
Давталт дуусахгүй байх. Ижил гэр бүлийн алдаа: давталтын явцад жагсаалтад элемент нэмэхэд list_iterator шинэ элементүүдийг ч харж, давталт тэдгээрийг ч давтана. Хэрэв давталт бүр элемент нэмдэг бол давталт хэзээ ч дуусахгүй. Ctrl+C-ээр зогсоож, ижил шийдлийг хэрэглэ: хуулбар дээр давтах. dict-ийн хувьд Python илүү хамгаалалттай: давталтын явцад dict-ийн хэмжээ өөрчлөгдвөл “RuntimeError: dictionary changed size during iteration” гэсэн тодорхой алдаа шиднэ. List-д ийм хамгаалалт байхгүй.
Өөр давталтын method-ыг хаалтгүй дуудах. for song in morning.by_duration: гэж хаалтгүй бичихэд “TypeError: ‘method’ object is not iterable” гарна: “method объект давтагдах боломжгүй”. __iter__-ийг Python өөрөө дууддаг бол бусад давталтын method-ыг бид өөрсдөө дуудаж, generator авах ёстой. Хичээл 6.1-ийн "функц ба түүний дуудлага" ялгаа дахин нэг удаа.
__iter__-ийг generator функц болгоход, method-ын биед yield бичихэд, хайрцаг ба түгээгчийн бүх бүтэц автоматаар бий болно. for loop iter()-ээр __iter__-ийг дуудах бүрт биеийг ажиллуулахгүйгээр шинэ generator үүсч, тэр нь iterator-ийн шалгалтыг давж, байрлалаа хөлдсөн frame-д хадгалж, төгсгөлд нь StopIteration-ийг автоматаар шиднэ. Энэ нь Хичээл 1.4-ийн хоёр class-тай загварын гурван мөрийн хувилбар. Энгийн тохиолдолд return iter(self._songs) delegation ч хангалттай. Давталтад логик хэрэгтэй үед generator. Дуудлага бүрт шинэ машин үүсдэг тул нэг объект дээрх үүрлэсэн давталтууд бие биедээ саад болохгүй. __iter__ байхад Python хуучин sequence протоколыг хэрэглэхгүй. Өөр дарааллын давталтыг тусдаа generator method болгон, __reversed__-ийг reversed()-д зориулж нэмж болно. Протоколуудыг дагасан class sum, max, map, cycle, islice зэрэг стандарт сангийн бүх хэрэгсэлтэй нэмэлт ажилгүйгээр ажиллана. Давтаж байгаа жагсаалтаа өөрчлөх нь элемент алгасах эсвэл дуусахгүй давталт үүсгэдэг тул хуулбар дээр давтах эсвэл шинэ жагсаалт бүтээх хэрэгтэй.
Ингээд Бүлэг 8 дуусч, өөрийн class-ууд Python-ий бэлэн төрлүүдтэй тэгш эрхтэй боллоо. Дараагийн бүлэг өөр төрлийн асуултад шилжинэ: олон class, олон функцийг хэрхэн зохион байгуулах вэ? Програмчлалын түүхэнд дахин дахин гарч ирдэг асуудлуудад нэр өгөгдсөн, шалгагдсан шийдлүүд бий: design pattern. Дараагийн хичээлд бид pattern гэж юу болох, яагаад тэдгээрийг мэдэх нь хэрэгтэй, мөн яагаад Python-д ихэнх pattern-ууд бусад хэлнээс хамаагүй хөнгөн байдгийг шударгаар ярилцана. Функц утга, closure, __call__, protocol-ууд, энэ сургалтын бүх мэдлэг тэр хөнгөн байдлын шалтгаан болно.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.