Ачааллж байна...
Энэ бүлгийн өмнөх хоёр хичээлд бид объектуудыг Python-ий суурь үйлдлүүдэд нэгтгэсэн: Money +, <-д хариулдаг болсон, Playlist [], len(), in-д хариулдаг болсон. Энэ хичээл хамгийн суурь үйлдлүүдийн нэгийг авч үзнэ: дуудах. Функцийн нэрийн ард хаалт бичиж, greet("Бат") гэж дуудахыг та Course 1-ээс хойш өдөр бүр хийж ирсэн. Тэр хаалт бол бас нэг operator бөгөөд түүнд харгалзах тусгай method нь __call__. Энэ method-той объектыг функц шиг дуудаж болно.
Энэ нь эхлээд хачин санагдаж магадгүй: объект функц шиг ажиллах нь юунд хэрэгтэй вэ? Үнэндээ та аль хэдийн олон "дуудагдаж чаддаг объект"-той ажилласан. Хичээл 3.3-ын partial функц биш, partial объект байсан. Хичээл 5.4-т lru_cache-ийн wrapper C объект байсан бөгөөд cache_info method-той. property, classmethod, dataclass бүгд класс байсан бөгөөд класс өөрөө дуудагдаж чаддаг. Хичээл 5.4-т бид decorator-ыг "дуудагдаж чаддаг юу ч" гэж тодорхойлсон. __call__ бол тэр "дуудагдаж чаддаг" гэдгийн протокол.
Энэ хичээлийн гол асуулт нь хэзээ. Бүлэг 4-т бид closure-оор тохируулсан функц бүтээж сурсан. __call__-тэй объект ижил ажлыг хийж чаддаг. Хоёрын аль нь хэзээ илүү вэ? Бид тохируулсан шалгагч, validator бүтээж, түүнийг closure хувилбартай зэрэгцүүлж, __call__ жинхэнэ давуу тал өгөх тохиолдлуудыг шударгаар тодорхойлно. Хичээлийн төгсгөлд class-аар бичсэн decorator-ыг бүтээж, Бүлэг 5-ын мэдлэгтэй холбоно.
Баримт бичгийн тамгыг төсөөлөөд үзээрэй. Тамга хийлгэхдээ та түүнд байгууллагын нэр, огноо, гарын үсгийн загварыг тохируулж өгнө. Дараа нь тамга хэрэглэх нь маш энгийн: цаасан дээр тавиад дарна. Дарах бүрт тамга урьдчилан тохируулсан мэдээллээ цаасан дээр үлдээнэ. Хэрэглэгч тамгын дотор юу байгааг мэдэх шаардлагагүй, зөвхөн "дарах" гэсэн ганц үйлдлийг мэднэ.
Гэвч тамга бол зүгээр нэг үйлдэл биш, биет зүйл. Та түүнийг гартаа барьж, дээр нь бичсэн зүйлийг харж, "энэ тамга хэдэн удаа хэрэглэгдсэн бэ?" гэсэн тоолуурыг шалгаж, шаардлагатай бол огноог солих боломжтой. Энэ нь ердийн гар хүрээгүй үйлдлээс ялгаатай.
__call__-тэй объект бол тэр тамга. Үүсгэх үед тохиргоо авна. Дуудах бүрт, хаалт бичих бүрт, тохиргоогоор ажиллана. Гэвч тэр бүрэн эрхт объект хэвээр: attribute-уудтай, бусад method-уудтай, уншигдахуйц тайлбартай. Бүлэг 4-ийн closure-оор бүтээсэн функц ч мөн тохиргоотой тамга, гэхдээ тэр тохиргоо үүргэвчинд нуугдсан, гаднаас __closure__-ийг нээж байж л хардаг. Энэ ялгаа хичээлийн бүх сонголтыг тодорхойлно.
Тоо тодорхой хүрээнд байгаа эсэхийг шалгадаг шалгагч бүтээе. Хүрээг үүсгэх үед өгнө, дараа нь утга бүрийг шалгагчийг дуудаж шалгана:
python
class RangeValidator:
def __init__(self, minimum, maximum):
self.minimum = minimum
self.maximum = maximum
def __call__(self, value):
if not self.minimum <= value <= self.maximum:
raise ValueError(
f"{value} нь {self.minimum}-{self.maximum} хүрээнээс гадуур"
)
return value
def __repr__(self):
return f"RangeValidator({self.minimum}, {self.maximum})"
score_check = RangeValidator(0, 100)
print(score_check(95))
print(score_check)
print(callable(score_check))
score_check(130)95
RangeValidator(0, 100)
True
Traceback (most recent call last):
...
ValueError: 130 нь 0-100 хүрээнээс гадуур__init__ тохиргоог self дээр хадгална, Хичээл 6.2-ын Timer шиг. __call__ бол гол хэсэг: тэр self-ээс гадна нэг argument value авч, шалгаад, хүрээнд байвал утгыг өөрчлөлтгүй буцаана, гадуур байвал ValueError шиднэ. Утгыг буцаасан нь шалгагчийг өөр илэрхийллийн дунд хэрэглэх боломж олгоно: self.score = score_check(score).
score_check(95) гэж объектын нэрийн ард хаалт бичсэн. callable() бол Хичээл 6.4-т бидний ашигласан суурь функц: объект дуудагдаж чадах эсэхийг хэлдэг. __call__ байгаа тул True.
Trace: объектыг дуудах нь
Алхам 1 (RangeValidator(0, 100)). Класс дуудагдаж, шинэ объект үүсч, __init__ ажиллана. Объектод: minimum = 0, maximum = 100.
Алхам 2 (score_check(95)). Python score_check-ийн араас хаалтыг харна. Тэр score_check-ийн class RangeValidator-т __call__ хайж олно. Дараа нь RangeValidator.__call__(score_check, 95) гэж дуудна: Хичээл 7.2-ын энгийн method-ын адил объект self болон орно.
Алхам 3 (__call__-ийн бие). self.minimum ба self.maximum-ийг объектоос уншина: 0 ба 100. 0 <= 95 <= 100 үнэн, 95 буцна.
Алхам 4 (score_check(130)). Ижил зам, нөхцөл худал, ValueError.
Тэгэхээр объект(argument) нь объект.__call__(argument)-ийн товчлол, a + b нь a.__add__(b)-ийн товчлолтой яг адил. Энэ бол хаалт operator-ын протокол.
Үүнийг мэдсэний дараа Python-ий нэг гүн баримт тодорхой болно: энгийн функц ч __call__-тэй объект.
python
def greet(name):
return f"Сайн байна уу, {name}"
print(hasattr(greet, "__call__"))
print(greet.__call__("Бат"))
print(callable(len), callable(RangeValidator), callable(5))True
Сайн байна уу, Бат
True True FalseФункц бол Python-д __call__ method-той объект. len шиг бэлэн функц ч мөн. RangeValidator класс ч callable: түүнийг дуудахад шинэ объект үүсдэг. Тоо 5 callable биш. Тэгэхээр Python-д "функц" гэдэг нь онцгой зүйл биш, дуудагдаж чаддаг объектуудын олон төрлийн нэг л юм. Классыг дуудахад ард нь юу болдгийн нарийн механизм, класс өөрөө ямар класстай вэ гэсэн асуулт, metaclass гэдэг сэдвийн нэг хэсэг бөгөөд сургалтын эхэнд хэлсэнчлэн энэ сургалтын хүрээнээс гадуур, сүүлийн хичээлд нэрлэгдэх болно.
Жишээ 1: closure уу, __call__ уу
Бүлэг 4-ийн мэдлэгээр ижил шалгагчийг closure-оор бичье:
python
def make_range_validator(minimum, maximum):
def validate(value):
if not minimum <= value <= maximum:
raise ValueError(f"{value} нь {minimum}-{maximum} хүрээнээс гадуур")
return value
return validate
score_check_fn = make_range_validator(0, 100)
print(score_check_fn(95))
print(score_check_fn)95
<function make_range_validator.<locals>.validate at 0x7f1c2a3b4d60>Ажиллагаа ижил. Хоёр хувилбарыг хэрэглэх тал нь ч ижил: хоёулаа score_check(95) гэж дуудагдана, хоёулаа map-д, sorted-ийн key-д өгөгдөж болно, хоёулаа callable. Хэрэглэгчийн хувьд ялгаагүй. Энэ бол duck typing-ийн нэг илрэл: функц хүлээдэг газарт ямар ч callable ажиллана.
Ялгаа нь объектыг гаднаас харахад гарна. Closure хувилбарыг хэвлэхэд ердөө “make_range_validator-ийн дотоод validate функц”, хүрээний тухай юу ч алга. Тохиргоог мэдэхийн тулд __closure__-ийг нээх ёстой. Объект хувилбар RangeValidator(0, 100) гэж тодорхой хэлнэ, тохиргоог score_check.minimum гэж шууд уншиж болно. Debug хийхэд, log-д, тест бичихэд энэ ил тод байдал маш хэрэгтэй.
Шударга сонголтын дүрэм: нэг үйлдэл, жижиг тохиргоо, дотоод төлөв харах шаардлагагүй бол closure илүү товч. Тохиргоо эсвэл төлөв гаднаас харагдах ёстой, нэмэлт method хэрэгтэй, эсвэл төлөв дуудлагаас дуудлагад өөрчлөгддөг бол __call__-тэй class. Хичээл 4.2-т бид closure олон функц буцааж эхэлбэл class бичих цаг болсныг дохио гэж хэлсэн. __call__ бол тэр шилжилтийн гүүр: class болсон ч функц шиг дуудагдсаар байна.
Жишээ 2: төлөвтэй шалгагч
__call__-ийн давуу тал хамгийн тод харагдах тохиолдол бол дуудлагын хооронд өөрчлөгддөг төлөв. Олон утга шалгах явцад татгалзсан утгуудыг цуглуулж, дараа нь тайлан гаргадаг шалгагч хэрэгтэй гэж бодъё:
python
class PhoneValidator:
def __init__(self, length=8):
self.length = length
self.checked = 0
self.rejected = []
def __call__(self, phone):
self.checked += 1
cleaned = phone.replace(" ", "").replace("-", "")
if not cleaned.isdigit() or len(cleaned) != self.length:
self.rejected.append(phone)
return None
return cleaned
def report(self):
good = self.checked - len(self.rejected)
return f"Шалгасан: {self.checked}, зөв: {good}, буруу: {self.rejected}"
check_phone = PhoneValidator()
raw = ["9911-2233", "88 77 66 55", "1234", "9911abcd", "70112233"]
valid = [p for p in map(check_phone, raw) if p is not None]
print(valid)
print(check_phone.report())['99112233', '88776655', '70112233']
Шалгасан: 5, зөв: 3, буруу: ['1234', '9911abcd']PhoneValidator монгол утасны дугаарыг шалгадаг: зай, зураасыг арилгаад, бүх тэмдэгт цифр, яг 8 оронтой эсэхийг шалгана. Энэ удаа алдаа шидэхийн оронд буруу дугаарт None буцааж, түүнийг rejected жагсаалтад хадгална. Дуудлага бүрт checked нэмэгдэнэ.
Объектыг map-д шууд өгсөнийг анзаараарай: Хичээл 3.1-ийн "бэлэн нэртэй функцтэй map" дүрэм энд ч хүчинтэй, учир нь check_phone callable. Дараа нь шалгалт дууссаны дараа report() method-оор тайлан гаргасан. Энэ method closure-д байхгүй: closure ганц функц, нэмэлт үйлдлүүд үгүй.
Closure-оор ижил зүйлийг хийх боломжтой ч: checked-д nonlocal хэрэгтэй, rejected-ийг гадагш гаргахын тулд Хичээл 4.2-ын Жишээ 3-ын адил хэд хэдэн функц буцаах хэрэгтэй. Энэ бол яг “class бичих цаг” гэсэн дохио. Мөн Хичээл 7.3-ын сургамжийг анзаараарай: rejected жагсаалт __init__ дотор үүссэн тул шалгагч бүр өөрийн жагсаалттай.
Жишээ 3: callable-уудыг нийлүүлэх
Callable объект өөр callable-уудыг агуулж, тэдгээрийг нэг дор дуудаж болно. Ингэснээр жижиг шалгагчуудаас том шалгагч бүтээнэ:
python
class AllOf:
def __init__(self, *validators):
self.validators = validators
def __call__(self, value):
for validator in self.validators:
value = validator(value)
return value
def __repr__(self):
inner = ", ".join(repr(v) for v in self.validators)
return f"AllOf({inner})"
def must_be_int(value):
if not isinstance(value, int):
raise TypeError(f"Бүхэл тоо хүлээсэн, {type(value).__name__} ирлээ")
return value
age_check = AllOf(must_be_int, RangeValidator(16, 99))
print(age_check)
print(age_check(25))
age_check(12)AllOf(<function must_be_int at 0x7f1c2a3b5e40>, RangeValidator(16, 99))
25
Traceback (most recent call last):
...
ValueError: 12 нь 16-99 хүрээнээс гадуурAllOf хэдэн ч шалгагч хүлээн авч, дуудагдахад тэдгээрийг дарааллаар нь дуудна. Шалгагч бүрийн буцаасан утга дараагийнхын оролт болно. Энэ бол Хичээл 2.3-ын pipeline-ий санаа, гэхдээ өгөгдлийн урсгалын оронд шалгалтын гинж. AllOf-ийн дотор энгийн функц must_be_int ба объект RangeValidator хоёр холилдсон, AllOf тэдгээрийг ялгахгүй, хоёуланг нь ердөө дууддаг. Duck typing бүрэн хүчээрээ: "дуудагдаж чаддаг бол чи манайх". Мөн AllOf өөрөө callable тул түүнийг өөр AllOf-д хийж, гинжийг гинжинд нийлүүлж болно. Энэ нь Бүлэг 9-ийн design pattern-уудын нэг загварын урьдал.
Жишээ 4: class-аар бичсэн decorator
Одоо Бүлэг 5-тай холбоос. Decorator бол функц хүлээн авч, callable буцаадаг callable. Энэ тодорхойлолтод "функц буцаадаг" гэж бичигдээгүй. Тэгэхээр decorator __call__-тэй объект буцааж болно. Хичээл 4.2-т бид Fibonacci-ийн дуудлагыг closure тоолуураар тоолж, тоог нь __closure__[0].cell_contents-ээр уншиж байсан. Тэр эвгүй байсан. Class-аар:
python
from functools import update_wrapper
class CountCalls:
def __init__(self, func):
self.func = func
self.calls = 0
update_wrapper(self, func)
def __call__(self, *args, **kwargs):
self.calls += 1
return self.func(*args, **kwargs)
@CountCalls
def fib(n):
"""n-р Fibonacci тоо."""
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
print(fib(20))
print("Дуудлага:", fib.calls)
print(fib.__name__, "|", fib.__doc__)6765
Дуудлага: 21891
fib | n-р Fibonacci тоо.Хичээл 5.1-ийн томьёогоор @CountCalls def fib нь fib = CountCalls(fib). CountCalls бол класс, түүнийг дуудахад шинэ объект үүсч, __init__ анхны fib-ийг self.func-д хадгална. fib нэр одоо тэр объектыг заана. fib(20) дуудагдахад объектын __call__ ажиллаж, тоолуурыг нэмээд, анхны функцийг дуудна. Рекурсив дуудлагууд Хичээл 4.3-ын нэрийн хавтангийн дүрмээр fib нэрээр явдаг тул бүгд объектоор дамжиж тоологдоно. Тоо 21,891, Хичээл 3.3 ба 4.2-ын тоотой таарч байна.
Ялгаа нь сүүлийн хоёр мөрөнд. fib.calls гэж тоог шууд уншсан, closure-ийн хайрцгийг нээх шаардлагагүй. update_wrapper бол Хичээл 5.2-т дурдсан wraps-ийн үндсэн функц: wraps бол decorator хэлбэр, update_wrapper бол энгийн функц хэлбэр. Тэр анхны функцийн нэр, тайлбарыг объект дээр хуулсан тул fib нэр ба docstring хадгалагдсан. Class-аар бичсэн decorator нь decorator-т өөрийн гэсэн ил төлөв, нэмэлт method, жишээлбэл тоолуурыг тэглэх reset(), хэрэгтэй үед тохиромжтой.
Нэг шударга анхааруулга: энэ хэлбэрийн decorator энгийн функцэд сайн ажилладаг ч class-ийн method-ыг боовол ажиллахгүй. Шалтгаан нь Хичээл 7.2-ын “bound method” механизм: Python функцийг объекттой холбодог ч энгийн объектыг холбодоггүй, тиймээс self дамжихгүй. Үүнийг засах нь Хичээл 5.4-т нэрлэсэн descriptor протоколыг шаарддаг, энэ сургалтын хүрээнээс гадуур. Method-уудыг боох шаардлагатай бол Бүлэг 5-ын функц хэлбэрийн decorator хэрэглэ.
Хүсвэл RangeValidator-т __eq__ нэмж, ижил хүрээтэй хоёр шалгагчийг тэнцүү гэж үздэг болгож болно. Эсвэл түүнийг frozen dataclass болгож, __call__-ийг нэмэх нь Бүлэг 7-ын мэдлэгтэй нэгтгэх энгийн арга. Сонирхвол CountCalls-д reset() method нэмж, fib(10)-ийн дуудлагыг тоолоод, тэглээд, fib(15)-ийнхыг тоолж болно. Мөн CountCalls-ийг Хичээл 4.3-ын make_memo-тэй давхарлаж, Хичээл 5.4-ийн дүрмээр аль давхарга юуг тоолохыг урьдчилан таамаглаад шалгах нь гурван бүлгийн мэдлэгийг нэгтгэсэн сайн туршилт. Эдгээр бүгд санал бөгөөд дараагийн хичээлүүд үүнийг хийсэн гэж үзэхгүй.
TypeError: ‘RangeValidator’ object is not callable. “RangeValidator объектыг дуудах боломжгүй” гэсэн утгатай. __call__ method байхгүй, эсвэл нэр нь буруу бичигдсэн, жишээлбэл __call эсвэл call, үед объектыг хаалтаар дуудахад гарна. Мөн буруу хувьсагчийг дуудсанаас ч гарч болно: хэрэв score = score_check(95) гэсний дараа санамсаргүй score(80) гэвэл “‘int’ object is not callable” гарна.
TypeError: RangeValidator.init() missing 1 required positional argument: ‘maximum’. “RangeValidator.init()-д шаардлагатай ‘maximum’ argument дутуу байна” гэсэн утгатай. Класс ба объектыг андуурахад гарна: шалгагчийг үүсгэхийн оронд RangeValidator(95) гэж классыг шууд утгаар дуудсан. Хичээл 5.3 ба 6.3-ын "хоёр шат" энд ч хүчинтэй: эхлээд классыг тохиргоогоор дуудаж объект авна, дараа нь объектыг утгаар дуудна. Нэг мөрөнд бол RangeValidator(0, 100)(95).
TypeError: RangeValidator.call() missing 1 required positional argument: ‘value’. “RangeValidator.call()-д шаардлагатай ‘value’ argument дутуу байна” гэсэн утгатай. Объектыг argument-гүйгээр дуудсан үед гарна: score_check(). Мессеж нь __call__-ийг нэрээр нь дурдаж байгаа нь trace-ийн Алхам 2-ыг баталж байна: хаалт __call__ руу хөрвүүлэгдсэн.
TypeError: Student.greet() missing 1 required positional argument: ‘self’. “Student.greet()-д шаардлагатай ‘self’ argument дутуу байна” гэсэн утгатай. Жишээ 4-ийн анхааруулга: CountCalls шиг class-аар бичсэн decorator-ыг class-ийн method дээр хэрэглээд, method-ыг объектоор дуудахад гарна. Объект self болж дамжихгүй тул анхны method self-гүйгээр дуудагдана. Шийдэл: method-уудад функц хэлбэрийн decorator, Хичээл 5.2-ын загвар.
callable() хэт өргөн хариулах. Энэ нь алдааны мессеж биш, логикийн урхи. Хэрэв та "энэ argument функц мөн үү" гэдгийг callable(x)-ээр шалгавал класс ч, __call__-тэй объект ч, bound method ч True өгнө. Ихэнх тохиолдолд энэ нь яг хүссэн зүйл: duck typing-ийн утга нь "функц мөн үү" биш, "дуудаж болох уу" гэдгийг асуух. Хэрэв жинхэнэ функц шаардлагатай бол, энэ нь ховор, inspect.isfunction() хэрэглэ.
__call__ method бүхий объектыг функц шиг дуудаж болно: obj(x) нь obj.__call__(x)-ийн товчлол. Python-д энгийн функцүүд ч, классууд ч, partial, lru_cache-ийн wrapper ч бүгд callable объектууд бөгөөд callable() тэдгээрийг танина. Функц хүлээдэг бүх газар, map, key=, decorator, шалгагчийн гинж, ямар ч callable хүлээн авна. __call__-тэй class ба closure хоёр ижил ажлыг хийж чадна. Нэг үйлдэл, жижиг, нуугдмал тохиргоонд closure товч. Тохиргоо эсвэл төлөв гаднаас харагдах, дуудлагын хооронд өөрчлөгдөх, эсвэл нэмэлт method хэрэгтэй бол __call__-тэй class. Callable объектууд бусад callable-уудыг агуулж, нийлмэл шалгагч зэргийг бүтээж болно. Class-аар бичсэн decorator нь update_wrapper-ээр нэр, тайлбарыг хадгалж, fib.calls шиг ил төлөв өгдөг. Гэвч энэ хэлбэр class-ийн method-уудад descriptor протоколгүйгээр ажиллахгүй.
Энэ бүлгийн гурван хичээлд бид Money-г тоо шиг, Playlist-ийг list шиг, шалгагчийг функц шиг ажилладаг болгосон. Дараагийн хичээл бүлгийг дугуйлж, Бүлэг 1 рүү буцна. Хичээл 8.2-т Playlist for loop-д хуучин sequence протоколоор орсон, гэхдээ бид тэр замын хязгаарлалтыг тэмдэглэсэн. Дараагийн хичээлд Playlist-д жинхэнэ __iter__ нэмнэ, бас Бүлэг 1-ийн хоёр class-тай загварыг биш, Бүлэг 2-ын yield-ийг ашиглан гурван мөрөнд. Бүлэг 1-ийн iterator протокол, Бүлэг 2-ын generator, Бүлэг 8-ын container протокол нэг жижиг class-д нийлж, протоколууд хэрхэн бие биетэйгээ нийлдэгийг нүдээр харна.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.