Ачааллж байна...
Өмнөх хичээлд Money арифметик ба харьцуулалтын протоколыг дагаж, тоо шиг ажилладаг болсон: нэмэгддэг, үржигддэг, эрэмбэлэгддэг. Энэ хичээлд өөр төрлийн объектыг, бусад зүйлсийг агуулдаг объектыг судална. Python-д list, tuple, dict, string зэрэг олон бэлэн “container” буюу "сав" байдаг. Тэдгээр бүгдэд нийтлэг хэдэн үйлдэл бий: x[0] гэж индексээр хандах, len(x) гэж хэмжээ асуух, "a" in x гэж агуулж байгаа эсэхийг шалгах. Эдгээр үйлдлүүдийг өөрийн class-д өгөх протоколыг container протокол гэж нэрлэдэг.
Бид Playlist буюу дууны жагсаалт бүтээнэ. Энэ нь дуунуудыг агуулдаг бөгөөд гурван тусгай method-оор list шиг мэдрэгддэг болно: __len__ нь len()-д хариулна, __getitem__ нь playlist[0] ба playlist[1:3]-д хариулна, __contains__ нь in-д хариулна. Гурав нь бүгд Бүлэг 1-ээс хойш танил механизмаар ажилладаг: Python-ий суурь үйлдэл далдуур тодорхой нэртэй method дууддаг.
Энэ хичээлийн нэг гэнэтийн бэлэг бий. Бид __iter__ бичихгүй. Гэвч Playlist for loop-д орно, reversed()-тэй ч ажиллана. Бүлэг 1-д бид iterable-ийг __iter__-тэй объект гэж тодорхойлсон. Энэ тодорхойлолт үнэн боловч бүрэн биш байсан: Python-д хуучин, илүү энгийн бас нэг давталтын зам бий. Энэ хичээлд бид түүнийг trace-ээр нээж, яагаад тэр нь хангалттай биш, Хичээл 8.4-т __iter__ бичих шаардлага яагаад гарахыг ойлгоно.
Протоколыг гэрээ гэж ойлгож болно: "ингэж ажилладаг л бол чи манайх". Спорт клубыг төсөөлөөд үзээрэй. Клуб гишүүддээ олон төрлийн байгууламж санал болгодог: усан сан, фитнесс, хувцас солих өрөө. Клуб таныг хаанаас ирсэн, хэн гэдгийг асуудаггүй. Тэр зөвхөн гэрээний гурван заалтад гарын үсэг зурсан эсэхийг шалгана. Гарын үсэг зурмагц бүх байгууламж танд нээгдэнэ.
Python-ий бэлэн функцүүд бол тэр байгууламжууд: len(), индексээр хандах, in, for loop, reversed(), if нөхцөл. Тэд таны class list-ээс удамшсан эсэхийг огт асуудаггүй. Тэд зөвхөн шаардлагатай method-ууд байгаа эсэхийг хардаг. Энэ зарчмыг Python нийгэмлэг duck typing гэж нэрлэдэг. "Нугас шиг алхаж, нугас шиг дуугарвал түүнийг нугас гэж үзнэ". Хэн гэдэг биш, юу хийж чаддаг нь чухал.
Гэрээний нэг сонирхолтой нарийн ширийн зүйл: зарим заалт бусдыг давхар хамардаг. Клуб "та усан санд орох эрхтэй бол хувцас солих өрөөнд ч орох эрхтэй" гэж үзэж болно. Python-д ч мөн адил: __getitem__ ба __len__ байгаа бол Python for loop-ийг тэр хоёрын тусламжтайгаар өөрөө зохион байгуулж чадна. Энэ бол хичээлийн гэнэтийн бэлэг.
Эхлээд дууг төлөөлөх жижиг class хэрэгтэй. Хичээл 7.4-ийн мэдлэгээр frozen dataclass:
python
from dataclasses import dataclass
@dataclass(frozen=True)
class Song:
title: str
artist: str
seconds: int
def __str__(self):
minutes, secs = divmod(self.seconds, 60)
return f"{self.title} ({self.artist}) {minutes}:{secs:02d}"Дуу бол утга шинжтэй, өөрчлөгдөх шаардлагагүй өгөгдөл тул frozen. divmod() нь хуваалт ба үлдэгдлийг зэрэг буцаадаг суурь функц: 245 секунд бол 4 минут 5 секунд. {secs:02d} нь секундийг хоёр оронтой, тэгээр дүүргэж бичнэ: 5 нь “05” болно.
Одоо Playlist. Эхлээд зөвхөн нэр ба дуунуудыг хадгалъя:
python
class Playlist:
def __init__(self, name, songs=None):
self.name = name
self._songs = [] if songs is None else list(songs)
def add(self, song):
self._songs.append(song)
morning = Playlist("Өглөөний")
morning.add(Song("Ээж минь", "Б.Шарав", 245))
morning.add(Song("Миний нутаг", "Д.Жаргалсайхан", 198))
morning.add(Song("Талын салхи", "Н.Жанцанноров", 212))
print(len(morning))Traceback (most recent call last):
...
TypeError: object of type 'Playlist' has no len()__init__ нь Хичээл 7.3-ын Жишээ 4-ийн None загварыг хэрэглэж байна: өөрчлөгдөж болох анхдагч утгын урхиас зайлсхийж, өгсөн жагсаалтын хуулбарыг авна. _songs нэрний эхэн дэх доогуур зураас Хичээл 7.4-ийн адил: "дотоод хадгалалт, гаднаас шууд хандах зориулалтгүй". Хэрэглэгч Playlist-тэй ажиллахдаа _songs-ийг биш, бидний санал болгох үйлдлүүдийг хэрэглэх ёстой.
len() ажилласангүй. Мессеж: “Playlist төрлийн объектод len() байхгүй”. Клубын гэрээнд гарын үсэг хараахан зураагүй байна.
Жишээ 1: __len__ — хэмжээ ба үнэн худал
Эхний заалт:
python
def __len__(self):
return len(self._songs)Ердөө нэг мөр: Playlist-ийн урт нь доторх list-ийн урт. Энэ бол delegation буюу "даалгах" загвар: Playlist ажлыг өөрөө хийхгүй, доторх list-д даалгадаг. Одоо:
python
print(len(morning))
empty = Playlist("Хоосон")
print(len(empty))
if morning:
print("Өглөөний жагсаалтад дуу байна")
if not empty:
print("Хоосон жагсаалт хоосон байна")3
0
Өглөөний жагсаалтад дуу байна
Хоосон жагсаалт хоосон байнаlen() ажиллалаа. Гэвч сүүлийн хоёр мөр нэмэлт бэлэг: if нөхцөл ч ажиллаж байна. Python-д объектыг if-д өгөхөд тэр үнэн эсвэл худал эсэхийг шийдэхийн тулд эхлээд __bool__ method хайдаг. Олдохгүй бол __len__-ийг хайж, 0 бол худал, бусад бол үнэн гэж үзнэ. Энэ бол list-ийн зан чанар: хоосон list худал. Бид __bool__ бичээгүй ч Playlist энэ дүрмийг автоматаар авсан. Гэрээний нэг заалт хоёр байгууламжийг нээсэн.
Жишээ 2: __getitem__ — индекс ба зүсэлт
Хоёр дахь заалт:
python
def __getitem__(self, index):
return self._songs[index]Мөн delegation. morning[0] гэж бичихэд Python morning.__getitem__(0)-ийг дуудаж, тэр доторх list-ээс 0-р элементийг авна:
python
print(morning[0])
print(morning[-1])
print(morning[5])Ээж минь (Б.Шарав) 4:05
Талын салхи (Н.Жанцанноров) 3:32
Traceback (most recent call last):
...
IndexError: list index out of rangeЭерэг индекс, сөрөг индекс хоёулаа ажилласан, учир нь бид индексийг list-д шууд дамжуулсан бөгөөд list сөрөг индексийг мэддэг. Хүрээнээс гарсан индекс IndexError өгсөн: “list-ийн индекс хүрээнээс гарлаа”. Мессеж “list” гэж хэлж байгааг анзаараарай: хэрэглэгч Playlist-тэй ажиллаж байгаа ч алдаа доторх хадгалалтын тухай ярьж байна. Энэ нь жижиг гоо зүйн асуудал бөгөөд Түгээмэл алдаанууд хэсэгт засварыг харна.
Одоо зүсэлт:
python
first_two = morning[0:2]
print(type(first_two).__name__)
print(first_two)list
[Song(title='Ээж минь', artist='Б.Шарав', seconds=245), Song(title='Миний нутаг', artist='Д.Жаргалсайхан', seconds=198)]Зүсэлт ажилласан ч энгийн list буцаасан, Playlist биш. Trace: morning[0:2] гэж бичихэд Python __getitem__-д тоо биш, slice объект өгнө: slice(0, 2, None). Энэ бол эхлэл, төгсгөл, алхмыг агуулсан жижиг объект. Бидний __getitem__ түүнийг list-д дамжуулсан, list зүсэлтийг list хэлбэрээр буцаасан. Хэрэв хэрэглэгч зүсэлтийн дараа first_two.name гэж бичвэл алдаа гарна. Бэлэн төрлүүд зүсэлтийг өөрийн төрлөөр буцаадаг: string-ийн зүсэлт string, tuple-ийнх tuple. Playlist ч тэгэх ёстой:
python
def __getitem__(self, index):
if isinstance(index, slice):
return type(self)(f"{self.name} (хэсэг)", self._songs[index])
return self._songs[index]Одоо slice ирвэл доторх list-ээс хэсгийг авч, түүгээр шинэ Playlist үүсгэнэ. Хичээл 8.1-ийн адил type(self) ашигласан тул удамшсан class-ийн зүсэлт ч зөв төрөлтэй байна. Тоо ирвэл өмнөх шигээ ганц дууг буцаана.
python
first_two = morning[:2]
print(type(first_two).__name__, "|", first_two.name, "|", len(first_two))Playlist | Өглөөний (хэсэг) | 2Жишээ 3: __contains__ — "агуулж байна уу" гэдгийн өөрийн утга
Гурав дахь заалт. Хэрэглэгч "Миний нутаг" дуу жагсаалтад байгаа эсэхийг мэдэхийг хүснэ. Дууны объект биш, нэрийг нь бичих нь байгалийн, мөн том жижиг үсэг ялгахгүй байх нь найрсаг:
python
def __contains__(self, item):
if isinstance(item, str):
wanted = item.casefold()
return any(song.title.casefold() == wanted for song in self._songs)
return item in self._songs"дуу" in playlist гэж бичихэд Python playlist.__contains__("дуу")-г дуудна. Бидний хувилбар хоёр төрлийн асуултад хариулна. String ирвэл нэрээр хайна: casefold() нь string-ийг том жижиг үсгийн ялгаагүй харьцуулахад зориулсан хэлбэрт оруулдаг, lower()-ээс илүү бүрэн, олон хэлний онцлогийг тооцдог. Дараа нь Хичээл 2.2-ын any() ба generator expression: тохирох дуу олдмогц зогсоно. Song объект ирвэл доторх list-ээс шууд хайна. frozen dataclass-ийн __eq__ талбаруудаар харьцуулдаг тул ижил утгатай дуу олдоно.
python
print("Миний нутаг" in morning)
print("МИНИЙ НУТАГ" in morning)
print(Song("Ээж минь", "Б.Шарав", 245) in morning)
print("Хөх тэнгэр" in morning)
print("Хөх тэнгэр" not in morning)True
True
True
False
TrueБүгд хүлээсэн ёсоор ажилласан. not in нь __contains__-ийн үр дүнг эсрэгээр нь эргүүлдэг тул тусдаа method шаардлагагүй. Энэ жишээ __contains__-ийг бичих жинхэнэ шалтгааныг харуулж байна: "агуулж байна уу" гэдэгт өөрийн утгыг өгөх. Хэрэв бид __contains__ бичээгүй бол in ажилласаар байх байсан, гэхдээ нэрээр хайж чадахгүй. Яагаад ажилласаар байх байсныг дараагийн жишээ тайлбарлана.
Жишээ 4: гэнэтийн бэлэг — for loop __iter__-гүйгээр
Бид Playlist-д __iter__ бичээгүй. Гэвч:
python
for song in morning:
print(song)
print("---")
for song in reversed(morning):
print(song.title)Ээж минь (Б.Шарав) 4:05
Миний нутаг (Д.Жаргалсайхан) 3:18
Талын салхи (Н.Жанцанноров) 3:32
---
Талын салхи
Миний нутаг
Ээж миньХоёулаа ажилласан. Бүлэг 1-ийн мэдлэгээр for loop iter()-ийг дууддаг бөгөөд iter() __iter__ хайдаг. Playlist-д __iter__ байхгүй. Юу болсон бэ? __getitem__-д чагнуур тавьж trace хийе:
python
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]python
for song in morning:
print(song.title) [__getitem__(0)]
Ээж минь
[__getitem__(1)]
Миний нутаг
[__getitem__(2)]
Талын салхи
[__getitem__(3)]Алхам 1. for loop iter(morning)-ийг дуудна. iter() Playlist-д __iter__ хайгаад олохгүй.
Алхам 2. iter() бууж өгөхгүй. Тэр Playlist-д __getitem__ байгааг хараад тусгай түгээгч үүсгэнэ. Энэ түгээгчийн дотор ердөө нэг тоолуур: 0.
Алхам 3. for loop next() дуудна. Түгээгч morning.__getitem__(0)-ийг дуудаж, "Ээж минь"-ийг буцаана, тоолуурыг 1 болгоно.
Алхам 4-5. 1, 2 индексүүд ижил замаар.
Алхам 6. Түгээгч __getitem__(3)-ийг дуудна. Доторх list IndexError шиднэ. Түгээгч энэ IndexError-ийг барьж, StopIteration болгон хувиргана. for loop чимээгүй зогсоно.
Энэ бол Python-ий хуучин sequence протокол: __iter__ бүхий орчин үеийн протокол бий болохоос өмнөх давталтын арга. Python хуучин кодтой нийцтэй байхын тулд түүнийг одоо ч хадгалсаар байна. Тэгэхээр Хичээл 1.2-ын iterable-ийн тодорхойлолтыг бүрэн болгоё: iterable бол __iter__-тэй эсвэл 0-ээс эхэлсэн бүхэл тоон индекстэй __getitem__-тэй объект. reversed() ч мөн адил: __reversed__ байхгүй бол __len__ ба __getitem__-ийг ашиглаж, сүүлийн индексээс эхлэн буурч хандана.
Тэгвэл __contains__-гүй бол in яагаад ажиллах байсан бэ? in эхлээд __contains__ хайдаг, олдохгүй бол объектыг давтаж, элемент бүрийг ==-оор харьцуулдаг. Давталт энэ хуучин протоколоор явагдана. Гэрээний заалтууд бие биенийгээ хамардаг.
Энэ бэлэг сайхан ч шударга анхааруулга хэрэгтэй. Хуучин протокол зөвхөн 0, 1, 2 гэх мэт бүхэл тоон индексээр ажилладаг. Хэрэв таны container dict шиг түлхүүрээр хандагддаг бол энэ зам огт буруу ажиллана. Мөн давталтын логикийг индексээр хязгаарладаг: файлаас мөр урсгах, generator-ийн адил хэмнэлттэй давтах боломжгүй. Орчин үеийн Python кодонд давтагдах ёстой class __iter__-ийг тодорхой бичдэг бөгөөд Хичээл 8.4-т бид Playlist-д үүнийг generator-ийн тусламжтайгаар нэмнэ.
Жишээ 5: яагаад list-ээс удамшихгүй вэ
Энэ хичээлийн туршид асуулт төрсөн байж магадгүй: Playlist-ийг шууд class Playlist(list): гэж list-ээс удамшуулбал бүх зүйл бэлэн биш гэж үү? Тийм, индекс, len, in, for, зүсэлт бүгд бэлэн болно. Гэвч list-ийн бүх method-ууд ч бас ирнэ: append, insert, extend, sort, clear, remove. Хэрэглэгч playlist.append(5) гэж тоо нэмж, playlist.sort() гэж дуунуудыг харьцуулах гэж оролдож болно. Бидний дүрэм, жишээлбэл "зөвхөн Song нэмэгдэнэ", "давхардсан дуу хориотой", тэдгээрийг хамгаалж чадахгүй.
Бидний сонгосон хэлбэрийг composition буюу "бүрдүүлэлт" гэж нэрлэдэг: Playlist list агуулдаг, list биш. Тэр зөвхөн бидний сонгосон үйлдлүүдийг гадагш нээдэг, бусдыг нь нуудаг. add() method-д шалгалт нэмэх нь энгийн:
python
def add(self, song):
if not isinstance(song, Song):
raise TypeError(f"Зөвхөн Song нэмэгдэнэ, {type(song).__name__} ирлээ")
if song in self._songs:
raise ValueError(f"'{song.title}' аль хэдийн жагсаалтад байна")
self._songs.append(song)Composition ба удамшлын сонголт Бүлэг 9-ийн design pattern-уудын гол сэдвүүдийн нэг бөгөөд ерөнхий зөвлөмж нь: “is-a” (Playlist бол list) биш, “has-a” (Playlist list агуулдаг) харилцаа байвал composition-ийг сонго.
Хүсвэл Playlist-д __setitem__ нэмж, morning[1] = Song(...) гэж дуу солих боломжтой болгож болно. Тэнд ч add()-ийн шалгалтыг хэрэглэх үү? Сонирхвол __delitem__ нэмж del morning[0] гэж дуу устгах боломж олгож болно. Мөн Жишээ 4-ийн чагнууртай хувилбарт "Талын салхи" in morning-ийг __contains__-тэй ба түүнийг түр арилгаж ажиллуулаад, __getitem__ хэдэн удаа дуудагдахыг харьцуулах нь гэрээний заалтууд хэрхэн бие биенээ хамардгийг өөрийн нүдээр харах сонирхолтой туршилт. Эдгээр бүгд санал бөгөөд дараагийн хичээлүүд үүнийг хийсэн гэж үзэхгүй.
TypeError: ‘Playlist’ object is not subscriptable. “Playlist объектод индексээр хандах боломжгүй” гэсэн утгатай. __getitem__ байхгүй, эсвэл нэр нь буруу бичигдсэн үед playlist[0] гэхэд гарна. Хичээл 2.1-ийн generator-ийн ижил алдааг санаарай: тэнд generator индекс дэмждэггүй байсан. Энд бид __getitem__ бичиж дэмжлэг нэмж болно.
TypeError: object of type ‘Playlist’ has no len(). “Playlist төрлийн объектод len() байхгүй” гэсэн утгатай. __len__ байхгүй үед гарна. Хичээлийн эхэнд бид үүнийг харсан.
TypeError: ‘float’ object cannot be interpreted as an integer. “float объектыг бүхэл тоо гэж тайлбарлах боломжгүй” гэсэн утгатай. __len__ бүхэл тооноос өөр зүйл буцаахад гарна, жишээлбэл нийт хугацааг минутаар return total_seconds / 60 гэж буцаах. Python len()-ийн үр дүн үргэлж 0 эсвэл түүнээс их бүхэл тоо байхыг шаарддаг. Сөрөг тоо буцаавал “ValueError: len() should return >= 0” гарна. Хэрэв нийт хугацаа хэрэгтэй бол тусдаа method эсвэл property бич, __len__-ийг зөвхөн тоо ширхэгт үлдээ.
AttributeError: ‘list’ object has no attribute ‘name’. “list объектод ‘name’ attribute байхгүй” гэсэн утгатай. __getitem__ slice-ийг тусад нь шийддэггүй үед зүсэлтийн үр дүнг Playlist гэж хэрэглэхэд гарна: morning[:2].name. Жишээ 2-ын isinstance(index, slice) шалгалт үүнийг засна.
IndexError: list index out of range. “list-ийн индекс хүрээнээс гарлаа” гэсэн утгатай. Хүрээнээс гадуурх индекс өгсөн үед гарна. Энэ нь зөв зан чанар, гэхдээ мессеж доторх хадгалалтыг ил гаргадаг. Хэрэв илүү тодорхой мессеж хүсвэл try/except-ээр барьж, өөрийн IndexError-ийг шидэж болно: “Playlist-д 3 дуу байна, 5-р индекс байхгүй”. Чухал: заавал IndexError шидэх ёстой, өөр төрлийн алдаа биш. Жишээ 4-ийн хуучин давталтын протокол зөвхөн IndexError-ийг "дууслаа" гэсэн дохио гэж таньдаг. Хэрэв __getitem__ хүрээнээс гарахад ValueError шидвэл for loop зогсохын оронд тэр алдаагаар унана.
Container протокол нь өөрийн class-ийг list, dict шиг агуулагч болгодог. __len__ нь len()-д хариулж, бүхэл, сөрөг биш тоо буцаах ёстой. Мөн __bool__ байхгүй бол if нөхцөлд хоосон эсэхийг шийднэ. __getitem__ нь obj[i]-д хариулдаг. Тоо эсвэл slice объект хүлээн авах бөгөөд зүсэлтийг өөрийн төрлөөр, type(self)(...)-ээр буцаах нь бэлэн төрлүүдийн зан чанартай нийцнэ. __contains__ нь in ба not in-д хариулж, "агуулж байна уу" гэдэгт өөрийн утга, жишээлбэл нэрээр том жижиг үсэг ялгахгүй хайх, өгөх боломж олгоно. Энэ бүгдийг доторх list-д даалгах delegation загвараар хэдэн мөрөнд бичиж болно. Python-ий хуучин sequence протоколын ачаар __getitem__ ба __len__ бүхий объект __iter__-гүйгээр ч for loop, reversed(), in-д ажилладаг: iter() 0-ээс эхлэн индексээр хандаж, IndexError-ийг StopIteration болгоно. Гэвч энэ зам зөвхөн бүхэл тоон индекстэй сав-д тохирох тул орчин үеийн кодонд __iter__ бичдэг. Playlist list-ээс удамшихгүй, list агуулдаг: composition нь гадагш нээх үйлдлүүдийг бидэнд хянах боломж олгодог.
Энэ хичээлд Playlist агуулагч болсон. Дараагийн хичээлд илүү гайхмаар протоколтой танилцана: __call__. Энэ method-той объектыг функц шиг хаалтаар дуудаж болно: validator(value). Объект функц шиг ажиллах нь хэзээ хэрэгтэй вэ? Бид тохируулсан шалгагч бүтээж, түүнийг Бүлэг 4-ийн closure-тэй харьцуулж, мөн Бүлэг 5-ын decorator-уудтай гүн холбоог нь харна: Хичээл 5.4-т бид decorator бол "дуудагдаж чаддаг юу ч" гэж хэлсэн. __call__ бол яг энэ "дуудагдаж чаддаг" гэдгийн протокол.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.