Ачааллж байна...
Өмнөх хичээлд бид decorator-ыг closure-ийн бүрэн мэдлэгтэйгээр бүтээж, тодорхойлох ба дуудах цагийг ялгасан. Тэр хичээлийн туршид нэг асуудал хэд хэдэн удаа бидний хажуугаар өнгөрсөн. add-ийг хэвлэхэд log_calls.<locals>.wrapper гарч ирсэн. Алдааны мессеж дуудсан функцийн нэрээр биш, wrapper-ийн нэрээр гарсан. Хичээл 4.3-т fib.__name__ нь “memoized” болсон. Бид эдгээрийг тэмдэглээд цааш явсан. Энэ хичээлд тэдгээрийг гол асуудал болгон авч үзнэ.
Асуудлын мөн чанар нь Хичээл 4.3-ын нэрийн хавтан солих мөчид оршино. add = log_calls(add) хийгдэхэд add нэр анхны функцийг биш, wrapper-ийг заадаг болно. wrapper бол өөрийн гэсэн нэр, тайлбар, гарын үсэгтэй тусдаа функц. Тэгэхээр гаднаас add-ийн талаар асуусан бүх асуулт, тухайлбал "чиний нэр хэн бэ", "чи юу хийдэг вэ", "ямар argument авдаг вэ", wrapper-ийн хариултыг авна. Функц боодолдоо хувийн мэдээллээ алддаг.
Энэ алдагдал гоо зүйн асуудал мэт санагдаж магадгүй ч бодит програмд ноцтой үр дагавартай. help() функц буруу мэдээлэл харуулна. Функцийн нэрээр ажилладаг бүртгэлийн системүүд чимээгүй эвдэрнэ. Course 3-ын FastAPI зэрэг функцийн гарын үсгийг уншиж ажилладаг хэрэгслүүд буруу ажиллана. Энэ хичээлд бид алдагдлыг бүрэн харуулж, дараа нь стандарт сангийн functools.wraps-ээр засна. wraps бол decorator бичих мэргэжлийн стандарт: Python-ий бэлэн decorator-ууд бүгд түүнийг эсвэл түүний үндсэн механизмыг ашигладаг, бид ч одооноос эхлэн decorator бүртээ хэрэглэнэ.
Бэлэг боох цаас руугаа буцъя, гэхдээ одоо номын дэлгүүрт. Дэлгүүрийн тавиур дээр ном бүр нүүрэн дээрээ нэр, зохиогч, товч тайлбартай. Худалдан авагч тавиураас ном сонгохдоо эдгээрийг уншдаг. Одоо дэлгүүрийн ажилтан бүх номыг бэлгийн цаасаар боож тавиурт буцааж тавилаа гэж төсөөлөөд үзээрэй. Ном бүр дотроо бүрэн бүтэн ч гаднаас бүгд ижил цэцэгтэй цаас харагдана. Худалдан авагч "энэ ямар ном бэ?" гэж асуухад хариулт нь "цэцэгтэй цаас". Зуун ном тавиур дээр бүгд нэг нэртэй болжээ.
Шийдэл нь энгийн бөгөөд ухаалаг дэлгүүрүүд үүнийг хийдэг: номыг боосны дараа боодлын гадна талд номын нүүрний шошгыг наах. Нэр, зохиогч, тайлбар бүгд боодол дээр харагдана. Худалдан авагч номыг задлалгүйгээр юу болохыг мэднэ. Ном дотор хэвээр, боодол хэвээр, зөвхөн боодол одоо өөрийнхөө биш, доторх номын мэдээллийг харуулж байна. Мөн шошгон дээр жижиг тэмдэглэл бий: "доторх анхны номыг үзэхийг хүсвэл энд".
functools.wraps бол яг тэр шошго наагч. Энэ нь анхны функцийн нэр, тайлбар болон бусад мэдээллийг wrapper дээр хуулж, мөн анхны функц рүү заасан тэмдэглэл үлдээдэг.
Өмнөх хичээлийн log_calls-ийг хялбаршуулж, тайлбартай функцэд хэрэглэе. Функцийн биеийн эхэн дэх гурван давхар хашилттай string бол Course 2-оос танил docstring: функцийн тайлбар бөгөөд __doc__ attribute-д хадгалагдаж, help()-д харагддаг.
python
def log_calls(func):
def wrapper(*args, **kwargs):
print(f"-> {func.__name__} дуудагдлаа")
return func(*args, **kwargs)
return wrapper
def add(a, b):
"""Хоёр тоог нэмж, нийлбэрийг буцаана."""
return a + b
print("Боохоос өмнө:")
print(" нэр:", add.__name__)
print(" тайлбар:", add.__doc__)
add = log_calls(add)
print("Боосны дараа:")
print(" нэр:", add.__name__)
print(" тайлбар:", add.__doc__)Боохоос өмнө:
нэр: add
тайлбар: Хоёр тоог нэмж, нийлбэрийг буцаана.
Боосны дараа:
нэр: wrapper
тайлбар: NoneБид энд @-ийн оронд гараар add = log_calls(add) бичсэн. Ингэснээр нэрийн хавтан солигдохоос өмнө ба дараах байдлыг харьцуулах боломжтой болсон. Боохоос өмнө add өөрийн нэр, тайлбартай. Боосны дараа нэр нь “wrapper”, тайлбар нь None. wrapper-т docstring бичээгүй тул тайлбар хоосон.
Одоо Python-ий суурь тусламжийн хэрэгсэл help()-ийг ажиллуулъя. Хөгжүүлэгчид функц юу хийдгийг, ямар argument авдгийг мэдэхийн тулд interactive горимд түүнийг байнга хэрэглэдэг.
python
help(add)Help on function wrapper in module __main__:
wrapper(*args, **kwargs)help() “wrapper гэдэг функцийн тусламж” гэж эхэлж байна. Гарын үсэг нь (*args, **kwargs), тайлбар байхгүй. add хоёр argument a ба b авдаг гэдэг, мөн хоёр тоог нэмдэг гэдэг мэдээлэл бүрэн алга болсон. Хэрэв энэ функцийг өөр хөгжүүлэгч ашиглах гэж help(add) гэвэл тэр огт хэрэгтэй мэдээлэл олохгүй.
Гарын үсгийг шууд асуух стандарт хэрэгсэл бий: inspect модулийн signature().
python
import inspect
print(inspect.signature(add))(*args, **kwargs)Энэ алдагдал help()-ээс ч илүү ноцтой, учир нь олон хэрэгсэл яг энэ функцээр функцийн гарын үсгийг уншиж, түүн дээр тулгуурлан ажилладаг. Course 3-т та FastAPI endpoint бичихдээ функцийн argument-уудын нэр, төрлийг зарласан бөгөөд FastAPI тэдгээрийг уншиж, URL-ийн параметр болгон хувиргасан. Хэрэв endpoint функцийг wraps-гүй decorator-оор боовол FastAPI a, b-ийн оронд args ба kwargs гэсэн хоёр хачин параметр харж, хүсэлтүүд ойлгомжгүй алдаагаар унана.
Одоо засвар. wrapper-ийн def мөрийн дээр нэг мөр нэмнэ:
python
from functools import wraps
def log_calls(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"-> {func.__name__} дуудагдлаа")
return func(*args, **kwargs)
return wrapper
@log_calls
def add(a, b):
"""Хоёр тоог нэмж, нийлбэрийг буцаана."""
return a + b
print("нэр:", add.__name__)
print("тайлбар:", add.__doc__)
print("гарын үсэг:", inspect.signature(add))
print("анхны функц:", add.__wrapped__)
help(add)нэр: add
тайлбар: Хоёр тоог нэмж, нийлбэрийг буцаана.
гарын үсэг: (a, b)
анхны функц: <function add at 0x7f2e1c3d4a40>
Help on function add in module __main__:
add(a, b)
Хоёр тоог нэмж, нийлбэрийг буцаана.Бүх мэдээлэл сэргэсэн. Нэр нь add, тайлбар нь анхных, гарын үсэг (a, b), help() бүрэн, зөв мэдээлэл харуулж байна. Мөн шинэ attribute гарч ирсэн: __wrapped__. Энэ нь зүйрлэлийн "доторх анхны номыг үзэхийг хүсвэл энд" тэмдэглэл. wraps анхны функцийг wrapper-ийн __wrapped__ attribute-д хадгалдаг. inspect.signature энэ attribute-ыг дагаж анхны функцийн гарын үсгийг олсон тул (a, b) гарсан.
Trace: wraps яг юу хийдэг вэ
@wraps(func) мөрөнд хаалттай argument бий гэдгийг анзаараарай. Өмнөх хичээлийн төгсгөлд бид decorator-ыг хаалттай бичвэл алдаа гардаг гэж хэлсэн. wraps өөр ангилалд багтдаг: тэр argument-тай decorator. Бид түүнийг дараагийн хичээлд өөрсдөө бүтээнэ. Одоохондоо түүний ажиллагааг Хичээл 5.1-ийн “@d нь f = d(f)” томьёогоор задалъя. @wraps(func) нь wrapper = wraps(func)(wrapper) гэсэн утгатай. Тодорхойлох цагт, log_calls дуудагдах үед:
Алхам 1 (log_calls(add) эхлэх). log_calls-ийн гэрт func = анхны add.
Алхам 2 (def wrapper ажиллах). wrapper функц объект төрнө. Энэ мөчид түүний attribute-ууд өөрийнх: __name__ = “wrapper”, __doc__ = None, __qualname__ = “log_calls.<locals>.wrapper”. Үүргэвчинд func хайрцаг.
Алхам 3 (wraps(func) дуудагдах). wraps-д анхны add өгөгдөнө. Тэр шинэ decorator буцаана, тэр decorator өөрийн үүргэвчиндээ анхны add-ийг барьж байна. Бүлэг 4-ийн closure энд ч ажиллаж байна.
Алхам 4 (тэр decorator wrapper-т хэрэглэгдэх). Decorator анхны add-аас attribute-уудыг уншаад wrapper дээр бичнэ: __name__ = “add”, __doc__ = "Хоёр тоог нэмж...", __qualname__ = “add”, __module__, мөн type hint-үүд байвал __annotations__. Дараа нь __wrapped__ = анхны add-ийг нэмнэ. Эцэст нь ижил wrapper объектыг буцаана.
Алхам 5 (log_calls wrapper-ийг буцаах). Нэрийн хавтан ердийнхөөрөө солигдоно: add нэр wrapper-ийг заана.
Алхам 4-ийн сүүлийн хэсэг чухал: wraps шинэ давхарга нэмдэггүй. Тэр wrapper-ийг өөр функцээр солихгүй, ижил функц объектын шошгыг л солино. Дуудах цагт юу ч өөрчлөгдөхгүй: wrapper өмнөх шигээ ажиллана, гүйцэтгэлд нэмэлт ачаалал огт байхгүй. Бүх ажил тодорхойлох цагт нэг удаа хийгдэнэ.
Нэг шударга тэмдэглэл: wraps шошгыг солих боловч wrapper-ийн кодыг солихгүй. Тиймээс traceback-д frame нь “in wrapper” гэж харагдсаар байна. Traceback функцийн шошгыг биш, кодын нэрийг хэвлэдэг. Энэ бол хэвийн бөгөөд хэрэгтэй ч: traceback-д “in wrapper” харагдвал тэр нь боодлын доторх мөр гэдгийг та мэдэж байна.
Жишээ 1: нэрээр бүртгэх систем — чимээгүй эвдрэл
help() ба signature нь хөгжүүлэгчийн тав тухтай холбоотой бол дараагийн жишээ програмын зан чанарт шууд нөлөөлнө. Олон програм функцүүдийг нэрээр нь бүртгэж, дараа нь нэрээр нь дууддаг. Жишээлбэл, командын мөрийн хэрэгсэл “add”, “list”, “delete” гэсэн командыг харгалзах функцэд холбодог.
python
commands = {}
def register(func):
commands[func.__name__] = func
return func
def log_calls(func):
def wrapper(*args, **kwargs):
print(f"-> {func.__name__} ажиллаж байна")
return func(*args, **kwargs)
return wrapper
@register
@log_calls
def backup():
return "нөөцлөлт дууслаа"
@register
@log_calls
def cleanup():
return "цэвэрлэгээ дууслаа"
print(list(commands))
print(commands["wrapper"]())['wrapper']
-> cleanup ажиллаж байна
цэвэрлэгээ дууслааregister бол өөр нэг жижиг decorator: функцийг өөрчлөхгүйгээр буцаана, зөвхөн замдаа түүнийг commands dict-д нэрээр нь бүртгэнэ. Энд хоёр decorator давхарласан. Тэдгээрийн дарааллыг Хичээл 5.4-т бүрэн судална. Одоохондоо доорх log_calls эхэлж боож, дараа нь register боосон функцийг бүртгэнэ гэж ойлгоход хангалттай.
Үр дүнг хараарай. Бид хоёр команд бүртгэсэн, гэвч dict-д ганцхан түлхүүр байна: “wrapper”. Trace: backup-ийг log_calls боож, wrapper буцаана. Түүний нэр “wrapper”. register түүнийг commands[“wrapper”]-т бүртгэнэ. Дараа нь cleanup-ийг боож, өөр wrapper буцаана, түүний нэр ч “wrapper”. register түүнийг commands[“wrapper”]-т бүртгэж, backup-ийг дарж бичнэ. backup команд бүртгэлээс чимээгүй алга болсон, Python ямар ч анхааруулга өгөөгүй. Хэрэглэгч “backup” команд өгвөл KeyError гарна, эсвэл бүр муугаар “wrapper” гэж дуудвал backup-ийн оронд cleanup ажиллана.
Засвар нь log_calls-д @wraps(func) нэмэх:
python
def log_calls(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"-> {func.__name__} ажиллаж байна")
return func(*args, **kwargs)
return wrapperИжил бүртгэлийн кодыг дахин ажиллуулахад:
['backup', 'cleanup']Хоёр команд өөр өөрийн нэрээр бүртгэгдсэн. Энэ жишээ wraps-ийг заавал хэрэглэх ёстой хамгийн хүчтэй шалтгааныг харуулж байна: та decorator бичихдээ таны боосон функцийг дараа нь ямар хэрэгсэл, ямар аргаар хэрэглэхийг мэдэхгүй. wraps-гүй decorator хамгийн гэнэтийн газарт эвдрэл үүсгэж болно.
Жишээ 2: wrapped — боодлыг тойрох
__wrapped__ нь зөвхөн inspect-д зориулагдаагүй, бидэнд ч хэрэгтэй. Заримдаа боосон функцийн анхны хувилбарыг боодолгүйгээр дуудах шаардлага гарна. Хамгийн түгээмэл тохиолдол бол тест: таны тест бүртгэл, хэмжилт, кэшгүйгээр зөвхөн анхны логикийг шалгах ёстой байж болно.
python
@log_calls
def add(a, b):
"""Хоёр тоог нэмж, нийлбэрийг буцаана."""
return a + b
print(add(2, 3))
print(add.__wrapped__(2, 3))-> add ажиллаж байна
5
5Эхний дуудлага боодлоор дамжиж бүртгэл хэвлэв. Хоёр дахь нь __wrapped__-аар анхны функц руу шууд очсон тул бүртгэлгүй. Python-ий бэлэн decorator-ууд ч ижил стандартыг дагадаг. Хичээл 3.3-ын lru_cache-тай fib-д бас __wrapped__ бий:
python
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
"""n-р Fibonacci тоог буцаана."""
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
print(fib.__name__, "|", fib.__doc__)
print(fib.__wrapped__)fib | n-р Fibonacci тоог буцаана.
<function fib at 0x7f2e1c3d5b20>lru_cache нэр, тайлбарыг хадгалж, __wrapped__-ийг тохируулсан. Учир нь lru_cache дотроо wraps-ийн үндэс болох functools.update_wrapper функцийг ашигладаг. Тэгэхээр бидний бичих decorator-ууд одоо стандарт сангийнхтай ижил түвшинд ажиллах болно. Нэг анхааруулга: fib.__wrapped__(30) нь анхны функцийг дуудна, гэвч түүний доторх рекурсив дуудлагууд fib нэрээр явдаг тул кэштэй хувилбар руу буцаж орно. Хичээл 4.3-ын нэрийн хавтангийн trace энд ч хүчинтэй.
Жишээ 3: мэргэжлийн загвар
Одоо Хичээл 5.1-ийн гурван decorator-ыг мэргэжлийн стандартаар дахин бичье. Энэ нь цаашид бичих бүх decorator-ын загвар болно.
python
import time
from functools import wraps
def log_calls(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"-> {func.__name__} args={args} kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"<- {func.__name__} буцаалаа: {result!r}")
return result
return wrapper
def timed(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__}: {elapsed:.4f} секунд")
return result
return wrapper
def memo(func):
cache = {}
@wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapperГурвуулаа ижил араг ястай: гадна функц func авна, дотоод wrapper @wraps(func)-ээр шошгологдсон, wrapper анхны функцийг дуудаж үр дүнг буцаана, гадна функц wrapper-ийг буцаана. Ялгаа нь зөвхөн боодлын нэмэлт ажилд. memo-д анхаарах зүйл: cache wrapper-ийн гадна, memo-ийн гэрт байрлаж, wrapper-ийн үүргэвчинд орсон. @wraps(func) cache-ийн мөрийн дараа, яг def wrapper-ийн өмнө бичигдсэн. wraps үргэлж def wrapper-ийн яг дээр байх ёстой, учир нь тэр wrapper функцийг шошголдог.
Энэ араг ясыг цээжлэх нь зүйтэй. Дүрэм нь энгийн: wraps-гүй decorator бичихгүй. Ганцхан мөр, гүйцэтгэлд ямар ч зардалгүй, олон чимээгүй эвдрэлээс сэргийлдэг.
Хүсвэл Хичээл 4.3-ын make_memo-д @wraps(func) нэмж, @make_memo-той fib-ийн __name__ одоо “memoized” биш “fib” болж байгааг шалгаарай. Сонирхвол Жишээ 1-ийн бүртгэлийн системд гурав дахь команд нэмж, эхлээд wraps-гүй, дараа нь wraps-тай хувилбарт commands dict-ийн агуулгыг харьцуулж болно. Мөн type hint бүхий функц, тухайлбал def add(a: int, b: int) -> int:-ийг wraps-тай болон wraps-гүй decorator-оор боож, add.__annotations__-ийг харьцуулах нь wraps юу хуулдгийг бүрэн харуулна. Эдгээр бүгд санал бөгөөд дараагийн хичээлүүд үүнийг хийсэн гэж үзэхгүй.
NameError: name ‘wraps’ is not defined. “wraps гэдэг нэр тодорхойлогдоогүй” гэсэн утгатай. Импорт мартсан. Шийдэл: файлын эхэнд from functools import wraps. Хэрэв import functools гэж модулиар нь импортлосон бол @functools.wraps(func) гэж бичнэ. Хоёр хэлбэр хоёулаа түгээмэл. Энэ алдаа decorator-ыг хэрэглэх үед биш, decorator-оор функцийг боох тодорхойлох цагт гарна.
TypeError: update_wrapper() got multiple values for argument ‘wrapped’. “update_wrapper()-ийн ‘wrapped’ argument-д олон утга ирлээ” гэсэн утгатай. Энэ хачин мессеж @wraps(func)-ийн оронд @wraps гэж хаалтгүй бичсэн үед гарна. Хаалтгүй бол wraps wrapper-ийг анхны функц гэж ойлгож, огт өөр зүйл буцаадаг. Тэр буруу зүйл таны функцийн нэрэнд оноогдож, функцийг дуудах үед дотоод update_wrapper функц таны argument-уудыг хүлээн аваад будилна. Мессеж update_wrapper-ийн тухай ярьж байгаа тул анх харахад холбоогүй мэт санагдана. wraps-ийн тухай алдаа гарвал хамгийн түрүүнд (func) бичсэн эсэхийг шалгаарай.
NameError: name ‘func’ is not defined. “func гэдэг нэр тодорхойлогдоогүй” гэсэн утгатай. @wraps(func)-ийг буруу газарт, жишээлбэл гадна функцийн дээр бичсэн үед гарна:
python
@wraps(func)
def log_calls(func):
...Энэ мөр ажиллах үед func хараахан байхгүй: func бол log_calls-ийн argument, тэр зөвхөн log_calls дуудагдах үед л гэрт нь бий болно. wraps-ийн зорилго бол дотоод wrapper-ийг шошголох тул тэр log_calls-ийн дотор, def wrapper-ийн яг дээр байх ёстой.
help() эсвэл name буруу хэвээр байх. wraps нэмсэн атлаа нэр “wrapper” хэвээр байвал хоёр түгээмэл шалтгаан бий. Нэгдүгээрт, decorator-ыг өөрчилсний дараа interactive горимыг дахин эхлүүлээгүй: хуучин тодорхойлолт санах ойд хэвээр. Хоёрдугаарт, олон давхар decorator-ын аль нэгд л wraps байна. Давхарга бүр өөрийн wraps-тэй байх ёстой, нэг давхарга ч шошгогүй бол түүний дээрх бүх давхарга буруу нэр харна. Энэ бол Жишээ 1-ийн бүртгэлийн системийн эвдрэлийн шалтгаан: register зөв ажиллаж байсан, асуудал доод давхарга log_calls-д байсан.
Decorator нэрийн хавтанг солиход функц өөрийн нэр, тайлбар, гарын үсгээ алддаг: __name__ нь “wrapper”, __doc__ нь None, help() ба inspect.signature нь (*args, **kwargs) харуулдаг. Энэ алдагдал help()-ийг эвдэж, функцийн гарын үсгийг уншдаг FastAPI зэрэг хэрэгслүүдийг буруу ажиллуулж, нэрээр бүртгэдэг системд функцүүдийг чимээгүй дарж бичүүлдэг. functools.wraps нь def wrapper-ийн яг дээр @wraps(func) гэж бичигдэж, тодорхойлох цагт анхны функцийн нэр, тайлбар, module, annotation-уудыг wrapper дээр хуулж, __wrapped__-д анхны функцийг хадгалдаг. Тэр шинэ давхарга нэмдэггүй, дуудах цагийн зардалгүй. __wrapped__ нь боодлыг тойрч анхны функцийг дуудахад, тухайлбал тестэд хэрэгтэй. lru_cache зэрэг стандарт decorator-ууд ижил стандартыг дагадаг. Дүрэм: wraps-гүй decorator бичихгүй.
Энэ хичээлд бид @wraps(func) гэж хаалт, argument-тай decorator-ыг хэрэглэсэн, мөн Хичээл 3.3-т @lru_cache(maxsize=None)-ийг. Хоёулаа decorator-ын нэг илүү давхарга бүхий хэлбэр бөгөөд түүнийг бүтээх цаг ирлээ. Дараагийн хичээлд бид @retry(times=3) гэсэн decorator бичнэ: алдаа гарвал функцийг гурван удаа хүртэл дахин оролддог. Энэ нь гурван давхар функцтэй бүтэц шаардах бөгөөд энэ бүлгийн хамгийн хэцүү хичээл гэдгийг урьдчилан хэлье. Бид давхарга бүрийг маш удаан, trace-ээр нэг нэгээр нь бүтээнэ.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.