Ачааллж байна...
Бүлэг 8-ын туршид бид нэг санааг олон удаа давтсан: Python объектыг "хэн бэ" гэдгээр нь биш, "юу хийж чаддаг вэ" гэдгээр нь таньдаг. Хичээл 8.2-т Playlist list-ээс удамшаагүй ч __len__ ба __getitem__-тэй байсан тул len(), for loop, reversed() бүгд түүнийг хүлээн авсан. Хичээл 8.3-т __call__-тэй объект функц хүлээдэг бүх газарт ажилласан. Бүлэг 9-ийн стратегиуд, loader-ууд, сонсогчид нийтлэг эцэг class-гүйгээр ижил гэрээг дагасан. Энэ зарчмыг бид duck typing гэж нэрлэсэн: "нугас шиг алхаж, нугас шиг дуугарвал түүнийг нугас гэж үзнэ".
Гэвч тэр бүх гэрээ бичигдээгүй байсан. Хичээл 9.2-т "стратеги бүр нэг Order хүлээн авч, бүхэл тоо буцаана" гэсэн дүрмийг бид тайлбар хэлбэрээр л хэлсэн. Хэрэв хэн нэгэн параметргүй стратеги бичвэл алдаа зөвхөн ажиллах үед гарсан. Өмнөх хичээлийн Жишээ 2-т бид Sized гэсэн төрөлтэй танилцсан: “__len__ method-той аливаа зүйл”. Энэ бол duck typing-ийн гэрээг type hint болгосон анхны жишээ байв.
Энэ хичээлд бид ийм гэрээг өөрсдөө бичиж сурна: Protocol. Protocol бол "энэ method-уудтай, ийм гарын үсэгтэй аливаа объект" гэсэн төрөл. Энэ санааг англиар structural typing буюу "бүтцийн төрөлжилт" гэж нэрлэдэг: объект ямар class-аас удамшсанаар биш, ямар бүтэцтэйгээр нь шалгагдана. Бүлэг 8-ын duck typing ба Бүлэг 10-ын type hint энд нийлнэ. Бид Drawable буюу "зурагдах боломжтой" гэсэн протокол тодорхойлж, түүнийг удамшилгүйгээр хангадаг class-ууд бичнэ, mypy тэдгээрийг хэрхэн шалгадгийг trace-ээр мөрдөнө. Дараа нь сонгодог ABC буюу хийсвэр эцэг class-тай шударгаар харьцуулж, Бүлэг 9-ийн pattern-уудад Protocol нэмнэ.
Ажлын зарын хоёр хэлбэрийг төсөөлөөд үзээрэй. Эхний зар: "Жолооч хэрэгтэй. Заавал манай компанийн жолоочийн сургуулийг төгссөн байх." Энэ зар нэр дэвшигчийн гарал үүслийг шаардаж байна. Хэрэв таныг өөр сургуульд маш сайн сургасан, хорин жилийн туршлагатай бол ч энэ зард тэнцэхгүй. Энэ бол удамшилд суурилсан төрөлжилт: объект тодорхой эцэг class-аас удамшсан байх ёстой.
Хоёр дахь зар: "Жолооч хэрэгтэй. B ангиллын үнэмлэхтэй, механик хурдны хайрцагтай машин барьж чаддаг байх." Энэ зар зөвхөн чадварыг шаардаж байна. Хаана сурсан нь хамаагүй. Үнэмлэх ба ур чадвар байвал та тэнцэнэ. Энэ бол Protocol: "ингэж ажилладаг л бол чи манайх".
Хоёр дахь зарын нэг чухал давуу тал: ажил олгогч зарыг бичихээс өмнө сургасан хүмүүсийг ч хүлээн авч чадна. Хорин жилийн өмнө өөр хотод сурсан жолооч энэ компанийн тухай сонсож ч байгаагүй ч шаардлагыг хангана. Python-д энэ нь асар чухал: Protocol таны бичээгүй, өөрчилж чадахгүй class-уудыг ч, стандарт сангийн, гадны сангийн class-уудыг, хүлээн авч чадна, тэдгээр шаардлагатай method-уудтай л бол.
Терминал дээр энгийн текстэн зураг гаргадаг жижиг систем бүтээе. Зурагдах зүйл бүр өөрийгөө мөрүүдийн жагсаалт болгон "зурж" чаддаг байх ёстой. Гэрээг Protocol-оор бичье:
python
from collections.abc import Iterable
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> list[str]: ...
def render_all(items: Iterable[Drawable]) -> str:
blocks = ["\n".join(item.draw()) for item in items]
return "\n\n".join(blocks)Drawable нь typing.Protocol-оос удамшсан class хэлбэрээр бичигдсэн. Түүний бие ганц method-ын гарын үсэг: draw нь self-ээс өөр юу ч хүлээн авахгүй, string-үүдийн жагсаалт буцаана. Биеийн оронд ... бичигдсэн нь Python-ий "энд юу ч байхгүй" гэсэн тэмдэглэгээ: Protocol method-ын хэрэгжилтийг биш, хэлбэрийг л тодорхойлно. render_all нь Drawable-уудын iterable хүлээн авч, тус бүрийг зурж, хоосон мөрөөр тусгаарлан нэгтгэнэ. Өмнөх хичээлийн Iterable энд Protocol-тэй нийлсэн.
Одоо гэрээг хангадаг хоёр class. Тэдгээр Drawable-ийг огт дурдахгүйг анзаараарай:
python
class Rectangle:
def __init__(self, width: int, height: int) -> None:
self.width = width
self.height = height
def draw(self) -> list[str]:
top = "+" + "-" * self.width + "+"
middle = "|" + " " * self.width + "|"
return [top] + [middle] * self.height + [top]
class Label:
def __init__(self, text: str) -> None:
self.text = text
def draw(self) -> list[str]:
line = "=" * (len(self.text) + 4)
return [line, f" {self.text} ", line]
print(render_all([Rectangle(6, 2), Label("Сайн байна уу")]))+------+
| |
| |
+------+
=================
Сайн байна уу
=================Rectangle ба Label хоёулаа энгийн class, Drawable-аас удамшаагүй, түүнийг импортлоогүй ч. Тэд ердөө draw method-той бөгөөд гарын үсэг нь гэрээтэй таарч байна. Энэ нь хангалттай: render_all тэдгээрийг хүлээн авсан, mypy ч хүлээн авна. Хоёр дахь зар: үнэмлэх байгаа л бол хаана сурсан нь хамаагүй.
Trace: mypy бүтцийг хэрхэн шалгадаг вэ
Өмнөх хичээлийн адил энд нуугдмал төлөв нь type checker-ийн шинжилгээнд байна. Ажиллах үед Drawable огт хэрэглэгдэхгүй: render_all ердөө item.draw()-ийг дууддаг. mypy render_all([Rectangle(6, 2), Label("Сайн байна уу")]) мөрийг шалгахдаа:
Алхам 1 (гэрээг унших). mypy Drawable-ийн гишүүдийг жагсаана: draw(self) -> list[str]. Энэ бол шаардлагын жагсаалт.
Алхам 2 (Rectangle-ийг шалгах). Rectangle-д draw нэртэй method байгаа эсэх? Тийм. Түүний гарын үсэг гэрээтэй таарч байна уу? Self-ээс өөр параметргүй, list[str] буцаана. Таарч байна. Дүгнэлт: Rectangle нь Drawable. Rectangle-ийн эцэг class-уудыг огт хараагүйг анзаараарай.
Алхам 3 (Label-ийг шалгах). Ижил шалгалт, мөн тэнцэнэ.
Алхам 4 (жагсаалтыг шалгах). Жагсаалтын бүх элемент Drawable тул тэр Iterable[Drawable]-д тохирно. Дуудлага зөвшөөрөгдөнө.
Одоо гэрээг зөрчих хоёр class нэмье:
python
class Circle:
def __init__(self, radius: int) -> None:
self.radius = radius
class Banner:
def draw(self) -> str:
return "*** ТАНИЛЦУУЛГА ***"
render_all([Circle(3)])
render_all([Banner()])Алхам 5 (Circle). draw нэртэй method байхгүй. Дүгнэлт: Circle нь Drawable биш. mypy дуудлагыг алдаа гэж тэмдэглэж, Circle-д ямар гишүүн дутуу байгааг нэрээр нь хэлнэ.
Алхам 6 (Banner). draw method бий, гэвч буцаах төрөл нь str, гэрээ list[str] шаардаж байна. Дүгнэлт: нэр таарсан ч гарын үсэг таарахгүй тул Banner нь Drawable биш.
Ажиллах үед энэ хоёр дуудлага юу болох вэ? Circle-тэй дуудлага “AttributeError: ‘Circle’ object has no attribute ‘draw’” гэсэн алдаагаар унана. Banner-тэй дуудлага бүр ч хачин: "\n".join("*** ТАНИЛЦУУЛГА ***") нь string-ийг тэмдэгт тэмдэгтээр нь давтаж, тэмдэгт бүрийг тусдаа мөрөнд хэвлэнэ. Алдаа огт гарахгүй, ердөө буруу гаралт. Энэ бол Protocol-ийн жинхэнэ үнэ цэнэ: Banner шиг чимээгүй алдааг програм ажиллахаас өмнө илрүүлэх. mypy-ийн бодит гаралтыг Хичээл 10.4-т мөр мөрөөр нь уншина.
Жишээ 1: Protocol ба ABC — хоёр зар
Python-д гэрээ тодорхойлох өөр нэг, илүү хуучин арга бий: ABC, “Abstract Base Class” буюу хийсвэр эцэг class. Хичээл 9.2-ын ShippingStrategy-ийг санаарай: тэр NotImplementedError шиддэг cost method-той эцэг class байсан. abc модуль үүнийг албан ёсны болгодог:
python
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def draw(self) -> list[str]: ...
def show(self) -> None:
print("\n".join(self.draw()))
class Square(Shape):
def __init__(self, size: int) -> None:
self.size = size
def draw(self) -> list[str]:
return ["#" * self.size] * self.size
class Broken(Shape):
pass
Square(3).show()
Broken()###
###
###
Traceback (most recent call last):
...
TypeError: Can't instantiate abstract class Broken without an implementation for abstract method 'draw'Shape ABC-ээс удамшиж, draw-ийг @abstractmethod decorator-оор тэмдэглэсэн. Square Shape-ээс удамшиж, draw-ийг хэрэгжүүлсэн. Broken удамшсан ч draw-ийг хэрэгжүүлээгүй, тиймээс түүний объект үүсгэх оролдлого ажиллах үед шууд унасан: “draw хийсвэр method-ын хэрэгжилтгүйгээр хийсвэр class Broken-ийн объект үүсгэх боломжгүй”. Мөн Shape энгийн method show-г агуулж, удамшсан бүх class-д нийтлэг хэрэгжилт өгсөн.
Хоёр аргыг шударгаар харьцуулъя. ABC бол эхний зар: удамшлыг шаарддаг, ажиллах үед хамгаалдаг, нийтлэг кодыг хуваалцах боломж олгодог. Хэрэв таны class-ууд нэг гэр бүлийн гишүүд бөгөөд тэдгээрт нийтлэг хэрэгжилт хэрэгтэй, жишээлбэл show() шиг, бол ABC зүйтэй. Protocol бол хоёр дахь зар: удамшил шаарддаггүй, mypy-ээр шалгагддаг, нийтлэг код хуваалцдаггүй. Таны бичээгүй class-уудыг хүлээн авах, эсвэл хамааралгүй class-ууд нэг чадварыг хуваалцах үед Protocol зүйтэй. Python-ий стандарт сан хоёуланг нь хэрэглэдэг. Энэ сургалтын бүх жишээнүүд, Бүлэг 8-ын протоколуудаас Бүлэг 9-ийн pattern-ууд хүртэл, duck typing дээр тулгуурласан тул Protocol тэдгээрт илүү байгалийн.
Жишээ 2: стандарт сангийн протоколууд
Та аль хэдийн олон протоколтой ажилласан. collections.abc модулийн Iterable, Iterator, Sized, Container, Callable зэрэг төрлүүдийг mypy бүтцээр нь шалгадаг. Тэгэхээр тэд протоколууд:
python
from collections.abc import Container, Iterable, Sized
def describe(collection: Sized) -> str:
return f"{len(collection)} элементтэй"
def has_admin(users: Container[str]) -> bool:
return "admin" in usersdescribe нь __len__-тэй аливаа зүйлийг хүлээн авна: list, dict, str, Хичээл 8.2-ын Playlist, өмнөх хичээлийн Stack. has_admin нь __contains__-тэй аливаа зүйлийг: list, set, dict-ийн түлхүүрүүд, Playlist. Бүлэг 8-ын протокол бүр collections.abc-д нэртэй төрөлтэй:
__iter__ → Iterable; __iter__ ба __next__ → Iterator; __len__ → Sized; __contains__ → Container; __call__ → Callable.
Тэгэхээр Бүлэг 8-ын гэрээнүүд аль хэдийн type hint хэлбэрээр бэлэн байжээ. Өөрийн Protocol-ийг бичих шаардлага зөвхөн таны програмын өөрийн гэрээнд, жишээлбэл draw эсвэл load шиг method-уудад, гарна. Өмнөх хичээлийн сургамжийг дахин санаарай: параметрт хамгийн бага хэрэгтэй протоколыг сонго. describe нь len()-ээс өөр юу ч хэрэглэдэггүй тул list биш, Sized.
Жишээ 3: Бүлэг 9-ийн гэрээнүүдийг бичих
Одоо Бүлэг 9-ийн гурван pattern-ийн бичигдээгүй гэрээнүүдийг төрөл болгоё.
Strategy (Хичээл 9.2). Стратеги бүр Order хүлээн авч int буцаадаг функц. Энгийн функцүүдийн гэрээг Callable-оор бичнэ:
python
from collections.abc import Callable
ShippingStrategy = Callable[[Order], int]
SHIPPING_METHODS: dict[str, ShippingStrategy] = {}
def shipping_cost(order: Order, method: str) -> int:
strategy = SHIPPING_METHODS.get(method)
if strategy is None:
raise ValueError(f"Танигдаагүй хүргэлт '{method}'")
return strategy(order)Callable[[Order], int] нь "нэг Order argument авч, int буцаадаг дуудагдаж чаддаг аливаа зүйл" гэсэн утгатай. Хичээл 9.2-ын Түгээмэл алдаанд параметргүй pickup_cost() ажиллах үед TypeError өгч байсан. Одоо mypy түүнийг бүртгэлд нэмэх мөчид, програм ажиллахаас өмнө илрүүлнэ. Энгийн функцүүд, closure-ууд, __call__-тэй объектууд бүгд энэ төрөлд тохирно, Хичээл 8.3-ын "дуудагдаж чаддаг юу ч".
Factory (Хичээл 9.3). Loader бүр load() method-той объект. Энд Protocol тохирно:
python
from collections.abc import Iterator
from typing import Protocol, TypedDict
class SaleRow(TypedDict):
product: str
amount: int
class Loader(Protocol):
def load(self) -> Iterator[SaleRow]: ...Одоо create_loader-ийн буцаах hint -> Loader болно. CsvLoader, JsonLoader Loader-ээс удамшихгүй хэвээр. Хичээл 9.3-т бид "гэрээ нь гаралтын хэлбэрийг ч хамардаг" гэж тэмдэглэж, CsvLoader-т amount-ийг int болгох шаардлагатай байсан. Одоо тэр шаардлага бичигдсэн: Iterator[SaleRow], SaleRow-ийн amount int. Хэрэв хэн нэгэн amount-ийг string хэвээр үлдээсэн loader бичвэл mypy анхааруулна. Энэ нь Хичээл 10.1-ийн TypedDict, Хичээл 10.2-ын Iterator ба энэ хичээлийн Protocol гуравуулаа нэг мөрөнд нийлсэн нь.
Observer (Хичээл 9.4). Сонсогч бүр Event хүлээн авдаг дуудагдаж чаддаг зүйл. Handler = Callable[[Event], object]. Буцаах төрөл object нь "юу ч буцааж болно, бид түүнийг хэрэглэхгүй" гэсэн утгатай, өмнөх хичээлийн Жишээ 3-ын сургамж: list-ийн append None буцаадаг, бусад сонсогчид өөр зүйл буцааж болно, event bus тэдгээрийг үл тоодог.
Жишээ 4: ажиллах үеийн шалгалт — runtime_checkable
Protocol mypy-д зориулагдсан. Харин заримдаа ажиллах үед "энэ объект протоколыг хангаж байна уу" гэж асуух шаардлага гарна. isinstance-ийг шууд хэрэглэвэл:
python
isinstance(Rectangle(2, 2), Drawable)TypeError: Instance and class checks can only be used with @runtime_checkable protocols“Instance ба class шалгалтыг зөвхөн @runtime_checkable протоколуудад хэрэглэж болно” гэсэн утгатай. Protocol-ийг decorator-оор тэмдэглэвэл isinstance ажиллана:
python
from typing import Protocol, runtime_checkable
@runtime_checkable
class Drawable(Protocol):
def draw(self) -> list[str]: ...
print(isinstance(Rectangle(2, 2), Drawable))
print(isinstance(Circle(3), Drawable))
print(isinstance(Banner(), Drawable))True
False
TrueСүүлийн мөрийг анхааралтай хараарай: Banner True гарсан. Гэтэл Trace-ийн Алхам 6-д mypy Banner-ийг гарын үсэг таарахгүй гэж татгалзсан. Шалтгаан нь ажиллах үеийн шалгалт зөвхөн нэрийг шалгадаг: “draw гэсэн attribute байна уу”. Гарын үсэг, буцаах төрлийг шалгадаггүй, учир нь Хичээл 10.1-ийн дагуу ажиллах үед төрлийн мэдээлэл ашиглагддаггүй. Тэгэхээр runtime_checkable бол бүдүүн шүүлтүүр: "огт хамааралгүй объект биш үү" гэдгийг л баталгаажуулна. Нарийн шалгалт mypy-ийн ажил. Энэ хязгаарлалтыг Python-ий баримт бичиг ч тодорхой анхааруулдаг.
Хүсвэл Хичээл 9.3-ын loader-уудад Loader протоколыг нэмж, create_loader(...) -> Loader гэж hint өгөөд, санаатайгаар load-ийн оронд read гэж нэрлэсэн loader бичээд VS Code юу харуулахыг ажиглаарай. Сонирхвол Хичээл 10.2-ын longest функцэд Sized-ийн оронд өөрийн HasDuration протоколыг, seconds: int attribute-тай гэрээг бичиж, Song-уудын жагсаалтаас хамгийн урт дууг олох функц бүтээж болно: Protocol method төдийгүй attribute ч тодорхойлж чадна. Мөн Shape ABC-д draw-ийг хэрэгжүүлэхгүй Broken class-ийг Drawable протокол дээр туршиж, "ажиллах үеийн хамгаалалт" ба “mypy-ийн хамгаалалт” хоёрын ялгааг өөрийн нүдээр харьцуулах нь хичээлийн гол санааг нэгтгэнэ. Эдгээр бүгд санал бөгөөд дараагийн хичээлүүд үүнийг хийсэн гэж үзэхгүй.
AttributeError: ‘Circle’ object has no attribute ‘draw’. “Circle объектод ‘draw’ attribute байхгүй” гэсэн утгатай. Протоколыг хангаагүй объект ажиллах үед протокол хүлээдэг газар хүрэхэд гарна. Protocol hint байгаа ч Python өөрөө юу ч шалгадаггүй тул алдаа гэрээг анх хэрэглэсэн мөрөнд гарна. mypy ажиллуулсан бол энэ дуудлага аль хэдийн тэмдэглэгдсэн байх байсан.
Буруу үр дүн алдаагүйгээр гарах. Мессежгүй, Banner-ийн тохиолдол. Нэр таарсан ч гарын үсэг таарахгүй method ажиллах үед алдаа гаргахгүйгээр буруу ажиллаж болно. Энэ бол Protocol hint бичих хамгийн хүчтэй шалтгаан: mypy гарын үсгийг шалгадаг, Python шалгадаггүй.
TypeError: Instance and class checks can only be used with @runtime_checkable protocols. “Instance ба class шалгалтыг зөвхөн @runtime_checkable протоколуудад хэрэглэж болно” гэсэн утгатай. Энгийн Protocol-ийг isinstance-д өгөхөд гарна. Шийдэл нь @runtime_checkable нэмэх, гэхдээ Жишээ 4-ийн хязгаарлалтыг санаарай: тэр зөвхөн нэрийг шалгана.
TypeError: Protocols cannot be instantiated. "Протоколуудын объект үүсгэх боломжгүй" гэсэн утгатай. Drawable() гэж Protocol-ийн объект үүсгэх гэж оролдоход гарна. Protocol бол гэрээ, ажилтан биш: тэр хэлбэрийг тодорхойлдог ч ажил хийдэггүй. Объектыг гэрээг хангадаг Rectangle шиг энгийн class-аас үүсгэнэ.
TypeError: Can’t instantiate abstract class Broken without an implementation for abstract method ‘draw’. “draw хийсвэр method-ын хэрэгжилтгүйгээр хийсвэр class Broken-ийн объект үүсгэх боломжгүй” гэсэн утгатай. Python-ий хуучин хувилбарууд арай өөр хэлбэрээр бичдэг. Энэ бол ABC-ийн алдаа: удамшсан class бүх хийсвэр method-ыг хэрэгжүүлээгүй. Protocol-оос ялгаатай нь ABC ажиллах үед хамгаалдаг. Шийдэл нь дутуу method-ыг хэрэгжүүлэх. Хэрэв мессеж дэх method-ын нэр таны бичсэнтэй таарахгүй мэт харагдвал үсгийн алдааг шалгаарай: def drwa(self) гэвэл draw хэрэгжээгүй хэвээр.
Protocol бол duck typing-ийн гэрээг type hint болгон бичих арга: method-ууд ба attribute-уудын гарын үсгийг тодорхойлдог class, typing.Protocol-оос удамшсан, method-уудын бие нь .... Гэрээг хангадаг class Protocol-оос удамших, түүнийг импортлох шаардлагагүй: зөв нэр ба гарын үсэгтэй гишүүдтэй байхад хангалттай. Энэ бол structural typing. mypy шалгахдаа эцэг class-уудыг биш, бүтцийг тулгадаг: нэр байхгүй эсвэл гарын үсэг таарахгүй бол татгалзана. Python өөрөө ажиллах үед Protocol-ийг шалгадаггүй, тиймээс нэр таарсан ч гарын үсэг буруу объект чимээгүй буруу ажиллаж болно. ABC удамшил шаарддаг, ажиллах үед хамгаалдаг, нийтлэг кодыг хуваалцдаг. Protocol удамшилгүй, таны бичээгүй class-уудыг ч хүлээн авдаг. collections.abc-ийн Iterable, Iterator, Sized, Container, Callable нь Бүлэг 8-ын протоколуудын бэлэн type hint хэлбэр. Бүлэг 9-ийн гэрээнүүдийг Callable alias эсвэл Protocol-оор бичиж болно. @runtime_checkable isinstance-ийг боломжтой болгодог ч зөвхөн нэрийг шалгана.
Энэ бүлгийн гурван хичээлд бид олон hint бичиж, тэр болгонд “mypy үүнийг илрүүлнэ” гэж амласан. Дараагийн хичээлд тэр амлалтыг биелүүлнэ. Бид mypy-г суулгаж, энэ бүлгийн кодон дээр ажиллуулж, бодит алдааны мессежүүдийг Монгол хэлээр мөр мөрөөр нь уншиж, засна. Мөн type hint хамгийн их ашиг өгдөг нөхцлийг, том кодыг дахин бүтэцлэх үеийг харж, hint-ийг төсөлд хэрхэн аажмаар нэвтрүүлэх шударга зөвлөмж өгнө.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.