Ачааллж байна...
Энэ бүлгийн нэр бол Протоколууд ба operator. Өмнөх бүлгүүдэд бид протоколтой хоёр удаа уулзсан. Бүлэг 1-д iterator протокол: __iter__ ба __next__-ийг бичвэл таны объект for loop-д ордог болсон. Бүлэг 6-д context manager протокол: __enter__ ба __exit__-ийг бичвэл with-д ордог болсон. Энэ бүлэг тэр санааг Python-ий бусад суурь үйлдлүүдэд өргөтгөнө: нэмэх, харьцуулах, индексээр хандах, in-ээр асуух, функц мэт дуудах. Бүлэгийн төгсгөлд таны class Python-ий бэлэн төрлүүдээс ялгагдахгүй байгалийн мэт ажиллах болно.
Энэ хичээлийн сэдэв нь operator overloading: +, -, *, < зэрэг тэмдгүүдийг өөрийн class-д утгатай болгох. Өмнөх хичээлийн Money-г санаарай. Хоёр мөнгөн дүнг нэмэхийн тулд одоо Money(a.amount + b.amount) гэж бичих хэрэгтэй. Энэ хичээлийн төгсгөлд a + b, price * 3, sum(prices), sorted(prices) бүгд байгалийн мэт ажиллана. Mеханизм нь бидэнд аль хэдийн танил: a + b гэж бичихэд Python далдуур a.__add__(b)-ийг дууддаг, яг len(x) нь x.__len__()-ийг дууддагтай адил.
Энэ хичээлийн хамгийн чухал хэсэг бол нэг тусгай утга: NotImplemented. Таны class "энэ үйлдлийг би хийж чадахгүй" гэж Python-д хэрхэн шударгаар хэлэх ёстой вэ? Алдаа шидэх үү, NotImplemented буцаах уу? Энэ хоёрын ялгаа нь Python-ий operator системийн хамгийн ухаалаг хэсэг болох "нөгөө талаас асуух" механизмыг нээж, мөн яагаад sum() таны class-тай ажиллахгүй байж болохыг тайлбарлана. Бид Python-ий шийдвэрийн дарааллыг trace-ээр алхам алхмаар дагана.
Валют солих цэгт хоёр ажилтан хажуу хажуудаа сууж байна гэж төсөөлөөд үзээрэй. Нэг нь төгрөгтэй ажилладаг, нөгөө нь долларын мэргэжилтэн. Үйлчлүүлэгч ирж, хоёр ширхэг зүйлийг тавиад "эдгээрийг нийлүүлж өгөөч" гэнэ. Дүрэм ёсоор эхлээд зүүн талын зүйлийн ажилтан асуулгыг хүлээн авна.
Хэрэв тэр ажилтан ажлыг хийж чадвал хийнэ: хоёр төгрөгийн дүнг нийлүүлж өгнө. Хэрэв тэр "энэ миний мэдлэгийн хүрээнээс гадуур" гэвэл тэр алдаа гаргаж байгаа биш, зүгээр л эелдгээр "би биш" гэж хэлж байна. Тэгэхэд асуулга баруун талын зүйлийн ажилтан руу шилжинэ: "чи эдгээрийг нийлүүлж чадах уу?" Хэрэв хоёр дахь ажилтан чадвал ажил хийгдэнэ. Хэрэв хоёулаа "би биш" гэвэл л цэгийн менежер үйлчлүүлэгчид "уучлаарай, эдгээрийг нийлүүлэх боломжгүй" гэж албан ёсоор татгалзана.
Энэ дүрмийн гол утга нь "би биш" гэсэн хариулт алдаа биш, харин бусдад боломж олгох эелдэг хариулт юм. Хэрэв эхний ажилтан "би биш"-ийн оронд шууд "боломжгүй!" гэж хашгирвал хоёр дахь ажилтан хэзээ ч асуулга сонсохгүй, хэдийгээр тэр чадах байсан ч.
Python-д a + b бичихэд яг ийм дараалал явагддаг. Зүүн талын объект __add__-ээр асуулгыг хүлээн авна. "Би биш" гэсэн хариулт бол NotImplemented. Дараа нь баруун талын объектоос __radd__-ээр асууна. Хоёулаа татгалзвал Python TypeError шиднэ.
Өмнөх хичээлийн frozen Money-д __add__ нэмье:
python
from dataclasses import dataclass
@dataclass(frozen=True)
class Money:
amount: int
currency: str = "MNT"
def __add__(self, other):
if not isinstance(other, Money):
return NotImplemented
if other.currency != self.currency:
raise ValueError(
f"Өөр валют нэмэх боломжгүй: {self.currency} ба {other.currency}"
)
return type(self)(self.amount + other.amount, self.currency)
def __str__(self):
return f"{self.amount:,} {self.currency}"
book = Money(25000)
pen = Money(1500)
total = book + pen
print(total)
print(repr(total))26,500 MNT
Money(amount=26500, currency='MNT')book + pen ажилласан. __add__-ийг мөр мөрөөр нь уншъя. Хоёр argument бий: self бол + тэмдгийн зүүн талын объект, other бол баруун талынх. Эхлээд other нь Money мөн эсэхийг isinstance()-ээр шалгана. Биш бол NotImplemented буцаана. Энэ мөрийн утгыг Жишээ 1-д бүрэн харна. Хэрэв Money боловч өөр валют бол ValueError шиднэ. Эцэст нь нийлбэрээр шинэ Money үүсгэж буцаана.
Шинэ объектыг Money(...) биш, type(self)(...) гэж үүсгэсэн. Хичээл 7.1-ийн cls сургамжийг санаарай: class-ийн нэрийг хатуу бичвэл удамшсан class-ийн объектуудыг нэмэхэд эцэг class-ийн объект гарна. type(self) нь self-ийн жинхэнэ class-ийг буцаадаг тул удамшсан class-д ч зөв ажиллана.
Мөн frozen-ийн утгыг анзаараарай: book + pen нь book-ийг ч, pen-ийг ч өөрчлөөгүй, шинэ объект буцаасан. Бүхэл тоонууд ч ингэж ажилладаг: 2 + 3 нь 2-ыг өөрчлөхгүй, шинэ 5 үүсгэнэ. Утга шинжтэй class-ийн operator-ууд үргэлж шинэ объект буцаах ёстой.
Шинэ __str__ method нэмсэн. Course 2-оос танил: print() үүнийг хэрэглэж хүнд ойлгомжтой бичлэг гаргана, харин repr() dataclass-ийн үүсгэсэн __repr__-ийг хэрэглэж хөгжүүлэгчид зориулсан бичлэг гаргана. Мөнгөн дүн бүхэл тоо байгааг анхаар: мөнгийг үргэлж бүхэл MNT-ээр хадгалах нь 0.1 + 0.2 гэх мэт бутархай тооны нарийвчлалын асуудлаас зайлсхийх хамгийн найдвартай арга.
Trace: a + b дотор Python юу хийдэг вэ
book + pen гэж бичихэд Python дараах дарааллыг гүйцэтгэнэ:
Алхам 1. Python зүүн талын объект book-ийн class-д __add__ байгаа эсэхийг хайна. Олдлоо.
Алхам 2. book.__add__(pen) дуудна. self = book, other = pen.
Алхам 3. __add__ дотор: pen бол Money, ижил валют, шинэ Money(26500) буцна.
Алхам 4. Буцсан утга NotImplemented биш тул Python үүнийг book + pen-ийн үр дүн гэж үзэж, total-д онооно.
Энэ бол хамгийн энгийн зам. Одоо NotImplemented-ийн зам.
Жишээ 1: NotImplemented ба "нөгөө талаас асуух"
Money-д энгийн тоо нэмж үзье:
python
print(book + 500)Traceback (most recent call last):
File "C:\python-gunzgii\money.py", line 25, in <module>
print(book + 500)
~~~~~^~~~~
TypeError: unsupported operand type(s) for +: 'Money' and 'int'Мессеж: “+ үйлдэлд дэмжигдэхгүй төрлүүд: ‘Money’ ба ‘int’”. ~~~~~^~~~~ тэмдгүүд тэр мөрийн аль хэсэг, + үйлдэл, асуудалтай байгааг заан харуулж байна. Энэ алдааг бид өөрсдөө шидээгүй. Trace хийе:
Алхам 1. Python book-ийн __add__-ийг дуудна: book.__add__(500).
Алхам 2. 500 бол Money биш тул __add__ NotImplemented буцаана. Шидэхгүй, буцаана. Энэ бол зүйрлэлийн "би биш".
Алхам 3. Python NotImplemented-ийг хараад, асуулгыг баруун тал руу шилжүүлнэ. Баруун талын объект 500-ийн class int-д __radd__ байгаа эсэхийг хайна. “r” нь “reflected” буюу "тусгалт": баруун талын объект зүүн талын объекттой нэмэгдэх асуулга. int-д __radd__ бий.
Алхам 4. (500).__radd__(book) дуудагдана. int Money-ийн тухай юу ч мэдэхгүй тул тэр ч NotImplemented буцаана.
Алхам 5. Хоёр тал хоёулаа татгалзсан. Python өөрөө TypeError шидэж, хоёр төрлийн нэрийг мессежинд бичнэ.
Тэгэхээр NotImplemented нь алдаа биш, тусгай утга: Python-ий operator системд "энэ хослолыг би мэдэхгүй, нөгөө талаас асуу" гэсэн дохио. Яагаад өөрсдөө raise TypeError хийх биш, NotImplemented буцаах ёстой вэ? Учир нь ингэснээр бусад class бидний class-тай ажиллах боломжтой үлдэнэ. Жишээлбэл хэн нэгэн хожим Discount гэсэн class бичиж, түүнд __radd__ тодорхойлж book + Discount(10)-ийг дэмжихийг хүсч болно. Хэрэв бидний __add__ TypeError шиддэг байсан бол Python Discount-оос хэзээ ч асуухгүй. NotImplemented буцаах нь хаалгыг нээлттэй үлдээдэг.
Харин валютын шалгалтанд бид ValueError шидсэн. Энэ ялгаа санаатай. 500 бол бидний мэдэхгүй төрөл: өөр хэн нэгэн мэдэж магадгүй, тиймээс "би биш". Харин доллартай Money бол бидний сайн мэддэг төрөл, бид түүнийг нэмэх нь буруу гэдгийг тодорхой мэдэж байна, өөр хэн ч засч чадахгүй. Дүрэм: төрөл танихгүй бол NotImplemented буцаа, төрөл таньсан ч утга нь зохисгүй бол алдаа шид.
Жишээ 2: sum() ба __radd__
Одоо худалдан авалтын сагсны нийт дүнг sum()-ээр бодъё:
python
cart = [Money(25000), Money(1500), Money(8000)]
print(sum(cart))Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for +: 'int' and 'Money'Мессеж дэх дарааллыг анзаараарай: ‘int’ ба ‘Money’, эсрэг дарааллаар. sum() дотроо 0-ээс эхэлж, элемент бүрийг нэмдэг: 0 + cart[0], дараа нь үр дүн дээр + cart[1], гэх мэт. Хамгийн эхний нэмэлт 0 + Money(25000). Trace: Python int-ийн __add__-ийг дуудна, int Money-г мэдэхгүй тул NotImplemented. Python Money-ийн __radd__-ийг хайна. Бидний class-д __radd__ байхгүй тул TypeError.
Шийдэл нь __radd__ нэмэх. Бидэнд Money-д дурын int нэмэх шаардлагагүй, зөвхөн sum()-ийн эхлэлийн 0-ийг хүлээн авах хэрэгтэй:
python
def __radd__(self, other):
if other == 0:
return self
return NotImplemented__radd__ дуудагдах үед self бол баруун талын объект, Money, other бол зүүн талынх, 0. “0 нэмэх нь юу ч өөрчлөхгүй” тул self-ийг өөрчлөлтгүй буцаана. Бусад бүх зүйлд NotImplemented. Одоо:
python
print(sum(cart))34,500 MNTTrace: 0 + Money(25000) нь int-ийн __add__-ээс NotImplemented авч, Money-ийн __radd__-ийг дуудаж, Money(25000) буцаана. Дараагийн нэмэлтүүд Money + Money, ердийн __add__. Энэ trace NotImplemented-ийн "нөгөө талаас асуух" механизм яг хэрэгтэй үедээ ажилласныг харуулж байна: int Money-г мэдэхгүй ч Money int-ийн 0-ийг мэддэг.
Өөр шийдэл нь sum()-д эхлэлийн утга өгөх: sum(cart, Money(0)). Энэ нь __radd__-гүйгээр ажиллана, учир нь эхний нэмэлт Money(0) + cart[0] болно. Хоёр шийдэл хоёулаа зөв. __radd__ нь хэрэглэгч эхлэлийн утгыг мартсан ч ажилладаг тул илүү найрсаг.
Жишээ 3: үржүүлэх ба хоёр тал
Нэг барааг хэд хэдэн ширхэгээр авахад үнийг тоогоор үржүүлэх хэрэгтэй. __mul__ нэмье:
python
def __mul__(self, factor):
if not isinstance(factor, int):
return NotImplemented
return type(self)(self.amount * factor, self.currency)
__rmul__ = __mul____mul__ нь Money(1500) * 4 хэлбэрийг дэмжинэ. Харин хүмүүс 4 * Money(1500) гэж ч бичдэг. Trace: int-ийн __mul__ Money-г мэдэхгүй, NotImplemented. Python Money-ийн __rmul__-ийг хайна. Үржүүлэх нь хоёр талдаа ижил үр дүнтэй тул __rmul__-ийг тусад нь бичих шаардлагагүй: __rmul__ = __mul__ мөр ижил функцийг хоёр дахь нэрээр class-д оноож байна. Хичээл 7.3-ын class attribute: class-ийн биед оноосон аливаа нэр, функц ч бай, class-ийн гишүүн болно.
python
pen = Money(1500)
print(pen * 4)
print(4 * pen)
print(pen * 2.5)6,000 MNT
6,000 MNT
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for *: 'Money' and 'float'Хоёр хэлбэр хоёулаа ажилласан. Бутархай тоогоор үржүүлэхэд бид NotImplemented буцаасан тул TypeError гарсан. Энэ бол санаатай дизайн: мөнгийг бүхэл MNT-ээр хадгалдаг тул 2.5 дахин үржүүлэх нь бутархай мөнгө үүсгэнэ. Хэрэв хөнгөлөлт зэрэгт бутархай үржүүлэх хэрэгтэй бол тусгай method, жишээлбэл apply_rate(0.9), бичиж, тэр дотор дугуйлах дүрмийг тодорхой хэрэглэх нь зүйтэй.
__sub__ (хасах), __truediv__ (хуваах), __neg__ (сөрөг тэмдэг) зэрэг бусад арифметик operator-ууд яг ижил загвараар бичигддэг. Python-ий баримт бичигт бүх operator ба түүнд харгалзах method-уудын бүрэн жагсаалт бий.
Жишээ 4: харьцуулалт ба эрэмбэлэлт
Үнэ хоёрын аль нь хямд вэ? Сагсыг үнээр нь эрэмбэлэх. Эдгээрт < хэрэгтэй, тэр __lt__-ийг дууддаг (“less than”, "бага"):
python
def __lt__(self, other):
if not isinstance(other, Money):
return NotImplemented
if other.currency != self.currency:
raise ValueError("Өөр валютыг харьцуулах боломжгүй")
return self.amount < other.amountpython
cart = [Money(25000), Money(1500), Money(8000)]
print(Money(1500) < Money(8000))
print([str(m) for m in sorted(cart)])
print(max(cart))
print(Money(8000) > Money(1500))True
['1,500 MNT', '8,000 MNT', '25,000 MNT']
25,000 MNT
Truesorted() ба max() бидний Money-тэй ажилласан, учир нь тэд дотроо < харьцуулалт хэрэглэдэг. Хичээл 3.2-т бид dict-уудыг key-гүйгээр эрэмбэлэхэд “‘<’ not supported between instances of ‘dict’ and ‘dict’” алдаа гарсныг харсан. Одоо Money өөрөө <-ийг мэддэг тул key шаардлагагүй.
Сүүлийн мөр сонирхолтой: бид __gt__ (“greater than”) бичээгүй ч > ажилласан. Шалтгаан нь мөн тусгалт: a > b нь b < a-тай тэнцүү тул Python a-ийн __gt__-ийг олохгүй үед b-ийн __lt__-ийг дуудна. Гэвч <= ажиллахгүй:
python
print(Money(1500) <= Money(8000))TypeError: '<=' not supported between instances of 'Money' and 'Money'<=-ийн тусгал нь >=, тэр ч бичигдээгүй. Зургаан харьцуулалтыг (<, <=, >, >=, ==, !=) бүгдийг гараар бичих уйтгартай. functools модульд бэлэн шийдэл бий: total_ordering class decorator. Тэр __eq__ ба нэг харьцуулалт, жишээлбэл __lt__ байгаа class-д үлдсэн бүх харьцуулалтыг нэмж үүсгэдэг:
python
from dataclasses import dataclass
from functools import total_ordering
@total_ordering
@dataclass(frozen=True)
class Money:
amount: int
currency: str = "MNT"
def __lt__(self, other):
...Decorator-уудын дараалал чухал, Хичээл 5.4-ийн мэдлэг: доод decorator эхэлж ажилладаг. dataclass эхэлж ажиллаж __eq__-ийг үүсгэнэ, дараа нь total_ordering тэр __eq__ ба бидний __lt__-ийг ашиглан үлдсэн харьцуулалтуудыг нэмнэ. Дарааллыг солих нь total_ordering ажиллах үед __eq__ хараахан байхгүй гэсэн үг.
Шударга тэмдэглэл: dataclass өөрөө order=True тохиргоотой бөгөөд тэр талбаруудын tuple-ийг харьцуулж бүх харьцуулалтыг үүсгэдэг. Хичээл 3.2-ын tuple key-тэй ижил. Бид түүнийг энд хэрэглээгүй, учир нь tuple харьцуулалт дүнгийн дараа валютыг харьцуулна: Money(100, “USD”) < Money(100, “MNT”) нь “MNT” < “USD” гэсэн string харьцуулалтаар шийдэгдэж, утгагүй үр дүн гаргана. Өөрийн __lt__ нь өөр валютыг шууд татгалзаж чадна. Энгийн тохиолдолд, жишээлбэл огноо эсвэл хувилбарын дугаар, order=True хангалттай.
Хүсвэл Money-д __sub__ нэмж, хоёр дүнгийн зөрүүг гаргадаг болгож болно. Үр дүн сөрөг гарвал юу хийх вэ гэдгийг өөрөө шийдээрэй: зөвшөөрөх үү, алдаа шидэх үү? Сонирхвол __neg__ нэмж -Money(5000) гэж бичих боломжтой болгож болно. Энэ нь нэг argument-тай, зөвхөн self авдаг operator. Мөн Жишээ 1-ийн trace-ийг өөрийн нүдээр харахын тулд __add__ ба __radd__-ийн эхэнд print нэмж, book + pen, 0 + book, book + 500 гурвыг ажиллуулаад аль method хэдэн удаа дуудагдаж байгааг ажиглах нь хамгийн тод туршилт. Эдгээр бүгд санал бөгөөд дараагийн хичээлүүд үүнийг хийсэн гэж үзэхгүй.
TypeError: unsupported operand type(s) for +: ‘Money’ and ‘int’. “+ үйлдэлд дэмжигдэхгүй төрлүүд: ‘Money’ ба ‘int’” гэсэн утгатай. Хоёр тал хоёулаа NotImplemented буцаасан, эсвэл class-д тэр operator-ын method огт байхгүй үед гарна. Мессеж дэх төрлүүдийн дараалал зүүнээс баруун тийш: ‘int’ and ‘Money’ гэвэл тоо зүүн талд байсан гэсэн үг бөгөөд таны class __radd__ шаардлагатай байж магадгүй. Хэрэв энэ хослолыг дэмжих ёстой бол харгалзах method-ыг нэм. Хэрэв санаатайгаар дэмжихгүй бол энэ алдаа зөв зан чанар.
TypeError: ‘<=’ not supported between instances of ‘Money’ and ‘Money’. “‘<=’ нь Money ба Money төрлийн объектуудын хооронд дэмжигдэхгүй” гэсэн утгатай. Зөвхөн __lt__ тодорхойлсон class-д <= эсвэл >= хэрэглэхэд гарна. Шийдэл: @total_ordering нэмэх, эсвэл энгийн тохиолдолд dataclass-ийн order=True.
TypeError: exceptions must derive from BaseException. “Exception-ууд BaseException-ээс удамшсан байх ёстой” гэсэн утгатай. NotImplemented-ийг буцаахын оронд шидэхэд гарна: raise NotImplemented. NotImplemented бол exception биш, тусгай утга, түүнийг шидэх боломжгүй. Үүнийг ихэвчлэн нэр нь төстэй NotImplementedError-тэй андуурснаас болдог. Энэ хоёр огт өөр. NotImplementedError бол жинхэнэ exception: "энэ method-ыг удамшсан class бичих ёстой, энд хараахан хэрэгжээгүй" гэсэн утгатай. NotImplemented бол operator method-ын буцаах утга: "энэ хослолыг би мэдэхгүй". Operator method-д үргэлж return NotImplemented.
ValueError: Өөр валют нэмэх боломжгүй: MNT ба USD. Энэ бол бидний өөрсдийн бичсэн алдааны мессеж бөгөөд зөв ажиллаж байна: төгрөг ба долларыг санамсаргүй нийлүүлэхээс хамгаалж байна. Хэрэв таны програмд валют хөрвүүлэх шаардлагатай бол тусгай функц, жишээлбэл convert(money, "MNT", rate), бичиж, хөрвүүлсний дараа нэмнэ. + тэмдгийн дотор ханш нуух нь хэрэглэгчийг төөрөгдүүлнэ.
a + b None буцаах. Мессежгүй алдаа. Operator method дотор return мартсан үед гарна. __add__ шинэ Money-г тооцоолсон ч буцаагаагүй бол Python None-ыг нийлбэр гэж хүлээн авна, учир нь None нь NotImplemented биш. Дараа нь total.amount хандахад “‘NoneType’ object has no attribute ‘amount’” гарна, жинхэнэ шалтгаанаас хол газарт. Operator method бүр үр дүнгээ эсвэл NotImplemented-ийг заавал буцаах ёстой.
Operator overloading нь +, *, < зэрэг тэмдгүүдийг өөрийн class-д утгатай болгох арга: a + b нь a.__add__(b), a * b нь a.__mul__(b), a < b нь a.__lt__(b)-ийг дууддаг. Утга шинжтэй class-ийн operator-ууд объектыг өөрчлөхгүй, type(self)(...)-ээр шинэ объект буцаана. Хэрэв method нөгөө объектын төрлийг танихгүй бол NotImplemented буцаана: энэ бол алдаа биш, Python-д нөгөө талаас, __radd__, __rmul__ гэх мэт тусгалт method-аар асуухыг хэлсэн дохио. Хоёр тал хоёулаа татгалзвал л Python TypeError шиднэ. Төрөл таньсан ч утга нь зохисгүй бол, жишээлбэл өөр валют, өөрсдөө тодорхой алдаа шиднэ. sum() 0-ээс эхэлдэг тул __radd__-ийн 0-ийг хүлээн авдаг хувилбар эсвэл эхлэлийн утга хэрэгтэй. __lt__ байхад sorted, min, max ажилладаг. > тусгалтаар ажиллах ч <= ажиллахгүй, үүнийг total_ordering эсвэл dataclass-ийн order=True шийднэ.
Энэ хичээлд Money арифметик ба харьцуулалтын протоколыг дагаж, тоо шиг ажилладаг болсон. Дараагийн хичээлд өөр төрлийн протоколыг судална: container протокол. List ба dict-ийн адил playlist[0] гэж индексээр хандах, len(playlist) гэж урт асуух, "дуу" in playlist гэж агуулж байгаа эсэхийг шалгах. Бид Playlist class бүтээж, __getitem__, __len__, __contains__ гурван method-оор түүнийг Python-ий бэлэн жагсаалт мэт мэдрэгддэг болгоно. Мөн нэг гэнэтийн бэлэг бий: тэр гурвыг бичсний дараа Playlist for loop-д ч орох болно, хэдийгээр бид __iter__ бичээгүй байсан ч.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.