Ачааллж байна...
Өмнөх хичээлд бид decorator-ын МЕХАНИКИЙГ ойлгосон: @decorator нь func = decorator(func) -тэй ижил, стандарт загвар нь *args, **kwargs -аар дамжуулж, return ur_dun -аар буцаана.
Одоо өөрөө бодит decorator бичнэ.
Гэхдээ би нэг зүйлийг эхнээс нь тодруулъя: энэ хичээлд бид ЗӨВХӨН НЭГ decorator бичнэ.
Яагаад ганцхан? Учир нь decorator-ын синтакс нь энгийн, гэхдээ түүнийг ХЭЗЭЭ ашиглах вэ гэдэг нь илүү чухал. Нэг сайн жишээг гүнзгий ойлгох нь арван жишээг гүехэн харахаас илүү юм.
Тэр нэг жишээ бол цаг хэмжигч (timing decorator) — функц хэдэн секунд ажилласныг хэмждэг. Тэр нь:
Практикт ҮНЭХЭЭР хэрэгтэй (аль код удаан ажиллаж байгааг олох)
Decorator-ын гол давуу талыг тод харуулдаг
Богино, ойлгомжтой
Хичээлийн төгсгөлд бид түүнийг Бүлэг 5-ийн set/list хурдны туршилтад ашиглаж, decorator нь яагаад ЛОГИК ба ХЭМЖИЛТИЙГ салгах гоё арга болохыг харна.
Эхлээд decorator-гүйгээр хийж үзье.
Танд удаан ажилладаг функц байна. Хэр удаан вэ?
python
import time
def udaan_ajil(hemjee: int) -> int:
niit = 0
for i in range(hemjee):
niit += i * i
return niit
ekhelsen = time.time()
ur_dun = udaan_ajil(10_000_000)
duussan = time.time()
print(f"Үр дүн: {ur_dun}")
print(f"Хугацаа: {duussan - ekhelsen:.4f} секунд")Үр дүн: 333333283333335000000
Хугацаа: 1.2847 секундАжиллаж байна. Гэвч энэ шийдэл нь ХЭДЭН асуудалтай.
Асуудал 1: гурван нэмэлт мөр. ekhelsen, duussan, print — эдгээр нь жинхэнэ ажилтай ямар ч холбоогүй.
Асуудал 2: ДАВХАРДАЛ. Хэрэв та арван функц хэмжихийг хүсвэл — гурван мөрийг арван удаа бичих хэрэгтэй.
Асуудал 3: түр зуурын код. Хэмжилт дуусахад та тэдгээр мөрийг устгах хэрэгтэй. Дараа нь дахин хэмжих хэрэгтэй болбол — дахин бичнэ.
Асуудал 4 (хамгийн муу): функцийн ДОТОР бичвэл бүр дор.
python
def udaan_ajil(hemjee: int) -> int:
ekhelsen = time.time() # хэмжилт
niit = 0
for i in range(hemjee):
niit += i * i
duussan = time.time() # хэмжилт
print(f"Хугацаа: {duussan - ekhelsen:.4f} секунд") # хэмжилт
return niitОдоо функц нь ХОЁР ажил хийж байна: тооцоолол БА хэмжилт. Тэдгээр нь холилдсон.
Хэрэв та хэмжилтийг унтраахыг хүсвэл — функцийн кодыг ЗАСАХ хэрэгтэй. Хэрэв бусад функцэд ч хэмжилт хэрэгтэй бол — ижил кодыг хуулах хэрэгтэй.
Энэ бол Бүлэг 7.3-ын сургамжтай ижил санаа: нэг зүйл нэг л газарт байх ёстой.
Гүйлтийн тэмцээн төсөөлье.
Буруу арга: гүйгч бүр өөрийн гартаа секундомер барьж, гүйж эхлэхдээ дарж, дуусахад дарж, дараа нь хугацаагаа хэлнэ.
Энэ нь ажиллана. Гэвч гүйгч нь одоо ХОЁР ажил хийж байна: гүйх БА цаг хэмжих. Тэр нь гүйхэд саад болно. Мөн гүйгч бүр секундомер сурах хэрэгтэй.
Зөв арга: гадуур зогсох шүүгч байна. Гүйгч эхлэхэд тэр дарж, дуусахад тэр дарна. Гүйгч нь зөвхөн ГҮЙНЭ — тэр цагийн талаар юу ч мэдэхгүй.
Decorator бол тэр шүүгч юм. Тэр нь функцийн ГАДУУР зогсож, түүний ажлыг хэмжинэ. Функц нь өөрийн ажлыг л хийнэ.
python
import time
import functools
def khugatsaa_khemjikh(func):
@functools.wraps(func)
def dotood(*args, **kwargs):
ekhelsen = time.time()
ur_dun = func(*args, **kwargs)
duussan = time.time()
print(f"[{func.__name__}] {duussan - ekhelsen:.4f} секунд")
return ur_dun
return dotood
@khugatsaa_khemjikh
def udaan_ajil(hemjee: int) -> int:
niit = 0
for i in range(hemjee):
niit += i * i
return niit
ur_dun = udaan_ajil(10_000_000)
print(f"Үр дүн: {ur_dun}")[udaan_ajil] 1.2691 секунд
Үр дүн: 333333283333335000000udaan_ajil -ийн код нь ЦЭВЭР. Тэнд цагийн талаар ганц ч мөр байхгүй. Тэр нь зөвхөн тооцоолол хийж байна.
Гэвч хугацаа хэмжигдлээ.
Decorator-ыг мөр мөрөөр нь задалъя. Бүлэг 10.2-ын стандарт загвар яг ажиллаж байна:
def khugatsaa_khemjikh(func): — функц хүлээж авна.
@functools.wraps(func) — эх функцийн нэр, тайлбарыг хамгаална (Бүлэг 10.2). Энэ нь чухал, учир нь бид доор func.__name__ -ыг ашиглана.
def dotood(*args, **kwargs): — ЮУ Ч хүлээж авна (Бүлэг 3.3).
ekhelsen = time.time() — ӨМНӨХ ажил.
ur_dun = func(*args, **kwargs) — эх функцийг дуудна, бүх аргументыг ЗАДЛАЖ дамжуулна.
duussan = time.time() — ДАРААХ ажил.
print(...) — тайлагнана. func.__name__ нь эх функцийн нэрийг өгнө (Бүлэг 3.3-т товч дурдсан).
return ur_dun — буцаалтыг дамжуулна. ЭНЭ МӨРИЙГ МАРТАЖ БОЛОХГҮЙ.
return dotood — боолтыг буцаана, ХААЛТГҮЙ.
Одоо decorator-ын жинхэнэ хүчийг харъя.
python
import time
import functools
def khugatsaa_khemjikh(func):
@functools.wraps(func)
def dotood(*args, **kwargs):
ekhelsen = time.time()
ur_dun = func(*args, **kwargs)
duussan = time.time()
print(f"[{func.__name__:<20}] {duussan - ekhelsen:.4f} секунд")
return ur_dun
return dotood
@khugatsaa_khemjikh
def loop_niilber(hemjee: int) -> int:
niit = 0
for i in range(hemjee):
niit += i
return niit
@khugatsaa_khemjikh
def comprehension_niilber(hemjee: int) -> int:
return sum(i for i in range(hemjee))
@khugatsaa_khemjikh
def built_in_niilber(hemjee: int) -> int:
return sum(range(hemjee))
HEMJEE = 10_000_000
a = loop_niilber(HEMJEE)
b = comprehension_niilber(HEMJEE)
c = built_in_niilber(HEMJEE)
print()
print(f"Гурвуулаа ижил үр дүн: {a == b == c}")[loop_niilber ] 0.5821 секунд
[comprehension_niilber] 0.4103 секунд
[built_in_niilber ] 0.1247 секунд
Гурвуулаа ижил үр дүн: TrueГурван функц, нэг decorator, ямар ч давхардал байхгүй.
Мөн бид сонирхолтой зүйл олж мэдлээ: sum(range(hemjee)) нь loop-оос дөрөв дахин хурдан. Built-in функцууд нь C хэл дээр бичигдсэн тул ихэвчлэн Python-ы loop-оос хурдан байдаг.
Хэрэв decorator байгаагүй бол — та гурван функц бүрд гурван мөр нэмэх байсан. Есөн мөр давхардал.
Одоо хэмжилтийг УНТРААХ хэрэгтэй болбол — зөвхөн @khugatsaa_khemjikh мөрүүдийг устгана. Функцийн код НЭГ Ч мөр өөрчлөгдөхгүй.
Энэ бол "бэлгийг боох цаас" зүйрлэлийн бодит биелэл: боолтыг авахад бэлэг нь яг хэвээрээ үлдэнэ.
Одоо буцаж хараарай. Бүлэг 5.2-т бид set ба list-ийн хурдыг харьцуулсан. Тэр код ийм байсан:
python
import time
hemjee = 100_000
davtalt = 10_000
tootsuud_list: list[int] = list(range(hemjee))
tootsuud_set: set[int] = set(range(hemjee))
ekhelsen = time.time()
for i in range(davtalt):
_ = (hemjee - 1) in tootsuud_list
list_khugatsaa = time.time() - ekhelsen
ekhelsen = time.time()
for i in range(davtalt):
_ = (hemjee - 1) in tootsuud_set
set_khugatsaa = time.time() - ekhelsen
print(f"List: {list_khugatsaa:.4f} секунд")
print(f"Set: {set_khugatsaa:.4f} секунд")Тэр код нь ажилласан, гэхдээ эмх замбараагүй. Цаг хэмжих логик нь туршилтын логиктой холилдсон. ekhelsen хувьсагч хоёр удаа давтагдсан. Хэмжилтийн код нь жинхэнэ ажлаас илүү олон мөр эзэлсэн.
Одоо decorator-аар:
python
import time
import functools
def khugatsaa_khemjikh(func):
@functools.wraps(func)
def dotood(*args, **kwargs):
ekhelsen = time.time()
ur_dun = func(*args, **kwargs)
duussan = time.time()
print(f"[{func.__name__:<12}] {duussan - ekhelsen:.4f} секунд")
return ur_dun
return dotood
HEMJEE = 100_000
DAVTALT = 10_000
tootsuud_list: list[int] = list(range(HEMJEE))
tootsuud_set: set[int] = set(range(HEMJEE))
khaij_bui = HEMJEE - 1
@khugatsaa_khemjikh
def list_dээr_khaikh() -> None:
for _ in range(DAVTALT):
_ = khaij_bui in tootsuud_list
@khugatsaa_khemjikh
def set_dээr_khaikh() -> None:
for _ in range(DAVTALT):
_ = khaij_bui in tootsuud_set
list_dээr_khaikh()
set_dээr_khaikh()[list_dээr_khaikh] 4.8213 секунд
[set_dээr_khaikh ] 0.0009 секундИжил үр дүн, ХАМААГҮЙ цэвэрхэн код.
Одоо туршилтын функц бүр нь ЗӨВХӨН туршилтыг агуулж байна. Хэмжилт нь бүрэн салсан.
Мөн шинэ туршилт нэмэх нь маш амархан:
python
@khugatsaa_khemjikh
def dict_dээr_khaikh() -> None:
tootsuud_dict = {i: True for i in range(HEMJEE)}
for _ in range(DAVTALT):
_ = khaij_bui in tootsuud_dictНэг мөр (@khugatsaa_khemjikh) — хэмжилт бэлэн.
Энэ бол decorator-ын жинхэнэ ашиг тус: ХӨНДЛӨН огтлолцох ажлыг салгах.
"Хөндлөн огтлолцох" гэдэг нь: цаг хэмжих, лог бичих, эрх шалгах, кэш хийх — эдгээр нь БҮХ функцэд хэрэгтэй байж болох ажил, гэхдээ тэдгээр нь функцийн ЖИНХЭНЭ ажилтай ямар ч холбоогүй.
Decorator нь тэдгээрийг гадуур байлгадаг.
Ижил загвараар өөр decorator бичье. Энэ нь функцийн дуудалтыг бүртгэнэ.
python
import functools
def burtgekh(func):
@functools.wraps(func)
def dotood(*args, **kwargs):
args_str = ", ".join(repr(a) for a in args)
kwargs_str = ", ".join(f"{k}={v!r}" for k, v in kwargs.items())
buk = ", ".join(kh for kh in [args_str, kwargs_str] if kh)
print(f"→ {func.__name__}({buk})")
ur_dun = func(*args, **kwargs)
print(f"← {func.__name__} = {ur_dun!r}")
return ur_dun
return dotood
@burtgekh
def niilber(a: int, b: int) -> int:
return a + b
@burtgekh
def mendchilekh(ner: str, mendchilgee: str = "Сайн байна уу") -> str:
return f"{mendchilgee}, {ner}!"
niilber(5, 3)
print()
mendchilekh("Болд")
print()
mendchilekh("Сараа", mendchilgee="Өглөөний мэнд")→ niilber(5, 3)
← niilber = 8
→ mendchilekh('Болд')
← mendchilekh = 'Сайн байна уу, Болд!'
→ mendchilekh('Сараа', mendchilgee='Өглөөний мэнд')
← mendchilekh = 'Өглөөний мэнд, Сараа!'Дуудалт бүр бүртгэгдэж байна — ямар аргументтай, ямар үр дүн.
Энэ бол дебаг хийхэд маш хэрэгтэй. Программ буруу ажиллаж байвал — та зөвхөн @burtgekh нэмээд, юу болж байгааг ХАРНА.
Кодыг задалъя. Хамгийн сонирхолтой хэсэг нь *args ба **kwargs -ыг форматлах явдал.
args_str = ", ".join(repr(a) for a in args) — args нь TUPLE (Бүлэг 3.3). Түүний элемент бүрийг repr() -ээр форматлаж, таслалаар холбож байна. repr нь string-д хашилт нэмнэ (Бүлэг 8.1).
kwargs_str = ", ".join(f"{k}={v!r}" for k, v in kwargs.items()) — kwargs нь DICTIONARY (Бүлэг 3.3). .items() -ээр key-value хосыг авч (Бүлэг 2.2), key=value хэлбэрээр форматлаж байна. !r нь repr (Бүлэг 8.1).
buk = ", ".join(kh for kh in [args_str, kwargs_str] if kh) — хоёр string-ийг холбоно, гэхдээ ХООСОН нэгийг нь алгасна. if kh нь хоосон string-ийг шүүнэ.
Хэдэн бүлгийн мэдлэг нэг функцэд нийллээ: *args/**kwargs (3.3), comprehension (2.1), .items() (2.2), repr (8.1), join (Курс 1).
Decorator-уудыг ДАВХАРЛАЖ болно:
python
@khugatsaa_khemjikh
@burtgekh
def niilber(a: int, b: int) -> int:
return a + b
niilber(5, 3)→ niilber(5, 3)
← niilber = 8
[niilber ] 0.0000 секундХоёр боолт. Бэлгийг эхлээд нэг цаасаар, дараа нь өөр цаасаар боосон.
Дараалал нь ЧУХАЛ. Уншихдаа ДООРООС ДЭЭШ уншаарай:
python
@khugatsaa_khemjikh # 2. дараа нь ЭНЭ боолт
@burtgekh # 1. эхлээд ЭНЭ боолт
def niilber(a, b): ...Тэр нь дараах кодтой ижил:
python
niilber = khugatsaa_khemjikh(burtgekh(niilber))Дотоод боолт (burtgekh) нь эх функцтэй ойр. Гадна боолт (khugatsaa_khemjikh) нь бүхнийг тойрдог.
Тиймээс цаг хэмжигч нь БҮХ зүйлийг (лог бичихийг ч оруулаад) хэмжиж байна.
Хэрэв дарааллыг сольвол:
python
@burtgekh
@khugatsaa_khemjikh
def niilber(a: int, b: int) -> int:
return a + b
niilber(5, 3)→ niilber(5, 3)
[niilber ] 0.0000 секунд
← niilber = 8Одоо цаг хэмжилт нь лог ДОТОР гарч байна. Ялгаа нь жижиг, гэхдээ бодит.
Практик зөвлөгөө: хоёр, гурваас олон decorator давхарлах нь кодыг ойлгомжгүй болгодог. Болгоомжтой хандаарай.
Одоо чухал зөвлөгөө. Decorator нь хүчирхэг, гэхдээ бүх зүйлд хэрэглэх ЁСГҮЙ.
Decorator бич:
Ажил нь ОЛОН функцэд давтагдана (цаг хэмжих, лог бичих, эрх шалгах, кэш хийх)
Ажил нь функцийн ЖИНХЭНЭ ЗОРИЛГОТОЙ ХОЛБООГҮЙ (хөндлөн огтлолцох)
Ажлыг АСААХ/УНТРААХ боломжтой байх ёстой (нэг мөр нэмэх/устгах)
Decorator БҮҮ бич:
Ажил нь зөвхөн НЭГ функцэд хэрэгтэй → зүгээр л функц дотор бич
Ажил нь функцийн ЛОГИКИЙН хэсэг → тэр нь функцэд харьяалагдана
Та зөвхөн "гоё харагдана" гэж бодож байвал → тодорхой байдал үргэлж илүү чухал
Хамгийн чухал анхааруулга: decorator нь кодыг НУУДАГ.
python
@khugatsaa_khemjikh
@burtgekh
@keshlekh
@erkh_shalgakh
def niilber(a: int, b: int) -> int:
return a + bЭнэ функцийг дуудахад ЮУ болох вэ? Дөрвөн боолт ажиллана. Тэдгээрийн аль нь эхлээд? Аль нь юу хийдэг? Уншигч нь ДӨРВӨН decorator-ын кодыг уншиж, дараалал нь юу болохыг бодох хэрэгтэй.
Бүлэг 7.4-ийн "yo-yo problem" -ыг санаж байна уу? Тэнд гүн inheritance гинж нь кодыг ойлгомжгүй болгож байсан.
Decorator ч ижил асуудалтай. Хэт олон боолт нь бэлгийг олохыг хэцүү болгодог.
Практик дүрэм: нэг, ихдээ хоёр decorator. Гурав нь хязгаар.
Хэрэв сонирхвол: khugatsaa_khemjikh decorator-ыг өөрөө бичээд, өөрийн Курс 1-ийн ямар нэг программын функцууд дээр хэрэглэж, аль нь удаан ажиллаж байгааг олж үзээрэй. Тэр туршилт нь decorator-ын бодит хэрэглээг мэдрүүлнэ.
Мөн хүсвэл: @khugatsaa_khemjikh -ыг ТУРШИЖ, дараа нь УСТГААД, функцийн код НЭГ Ч мөр өөрчлөгдөөгүйг батлаарай. Бэлэг нь боолтоос үл хамааран хэвээрээ.
Мөн хүсвэл: return ur_dun -ыг зориудаар устгаад, niilber(5, 3) дуудаад None гарахыг хараарай. Тэр алдаа нь маш түгээмэл тул нэг удаа өөрөө үүсгэсэн нь дээр.
1. return ur_dun мартах
python
def dotood(*args, **kwargs):
ekhelsen = time.time()
func(*args, **kwargs) # буцаалт хаягдлаа
print(f"...")python
print(niilber(5, 3))NoneХамгийн түгээмэл алдаа. Бүлэг 10.2-оос дахин: боолт нь буцаалтыг ЗААВАЛ дамжуулах ёстой.
2. func.__name__ нь dotood гэж гарах
python
def khugatsaa_khemjikh(func):
def dotood(*args, **kwargs): # @functools.wraps БАЙХГҮЙ
...
return dotoodЭнэ тохиолдолд func.__name__ нь ЗӨВ ажиллана (учир нь func нь эх функц). Гэвч niilber.__name__ нь "dotood" болно.
Хэрэв та decorator дотор func.__name__ ашиглаж байгаа бол — асуудалгүй. Гэхдээ ГАДНААС niilber.__name__ шалгах бол — @functools.wraps(func) хэрэгтэй.
3. Эх функцийг ХОЁР удаа дуудах
python
def dotood(*args, **kwargs):
ekhelsen = time.time()
func(*args, **kwargs) # нэг удаа
duussan = time.time()
print(f"...")
return func(*args, **kwargs) # ХОЁР ДАХЬ удаа!Функц ХОЁР удаа ажиллана! Хэрэв тэр нь файлд бичдэг, эсвэл өгөгдөл өөрчилдөг бол — аймшигтай.
Засвар: ur_dun = func(...) гэж НЭГ УДАА дуудаж, хадгал.
4. Дуудалт дотор од мартах
python
ur_dun = func(args, kwargs) # од БАЙХГҮЙTypeError: niilber() takes 2 positional arguments but ...args (tuple) ба kwargs (dict) нь хоёр аргумент болж дамжина. Од нь ЗАДАЛНА (Бүлэг 3.3).
5. Хэт олон decorator давхарлах
python
@a
@b
@c
@d
def func(): ...Кодыг ойлгомжгүй болгоно. Нэг, ихдээ хоёр.
Decorator-ын гол ашиг тус: ХӨНДЛӨН ОГТЛОЛЦОХ ажлыг ЛОГИКоос салгах. Цаг хэмжих, лог бичих, эрх шалгах — эдгээр нь функцийн жинхэнэ ажилтай холбоогүй.
Цаг хэмжигчийн стандарт загвар:
python
def khugatsaa_khemjikh(func):
@functools.wraps(func)
def dotood(*args, **kwargs):
ekhelsen = time.time()
ur_dun = func(*args, **kwargs)
duussan = time.time()
print(f"[{func.__name__}] {duussan - ekhelsen:.4f} секунд")
return ur_dun
return dotoodФункцийн код НЭГ Ч мөр өөрчлөгдөхгүй. Бэлэг нь боолтоос үл хамааран хэвээрээ.
Нэг decorator, олон функц. Давхардал байхгүй. Асаах/унтраах нь нэг мөр.
return ur_dun -ыг МАРТАЖ БОЛОХГҮЙ — хамгийн түгээмэл алдаа.
Эх функцийг ЗӨВХӨН НЭГ УДАА дууд, үр дүнг хувьсагчид хадгал.
Decorator-уудыг ДАВХАРЛАЖ болно. Уншихдаа ДООРООС ДЭЭШ. @a @b def f() нь f = a(b(f)) -тэй ижил.
Хэзээ бичих: ажил нь олон функцэд давтагдана, функцийн зорилготой холбоогүй, асаах/унтраах боломжтой байх ёстой.
Хэзээ БҮҮ бич: зөвхөн нэг функцэд хэрэгтэй, ажил нь логикийн хэсэг, зөвхөн "гоё харагдана" гэж бодож байвал.
Хэт олон decorator нь "yo-yo problem" үүсгэнэ (Бүлэг 7.4-тэй ижил). Нэг, ихдээ хоёр.
Дараагийн хичээлээр Бүлэг 10 дуусна. Тэнд бид framework-үүд decorator-ыг хэрхэн ашигладгийг харна. Та FastAPI-гийн @app.get("/") гэсэн мөрийг АНХ УДАА харах болно — мөн тэр нь илбэ БИШ гэдгийг мэдэх болно. Тэр нь зөвхөн функцийг бүртгэж, "энэ хаяг дээр хүсэлт ирвэл, энэ функцийг дууд" гэж хэлж байна.
Тэр хичээлээр та FastAPI-гийн код УНШИЖ чаддаг болно. Ажиллуулах нь Бүлэг 15-ын capstone-ы ажил — гэхдээ ойлгох нь дараагийн хичээлд эхэлнэ.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.