3-qism · Professional Python·14-bob
Dekoratorlar va kontekst menejerlari
Dekorator qanday ishlaydi, functools.wraps, parametrli dekoratorlar (retry, timer, cache), ro'yxatga oluvchi dekoratorlar, klass dekoratorlari; with va kontekst menejerlari: __enter__/__exit__, contextlib, ExitStack.
AI/ML kodida @ belgisi har qadamda uchraydi: @torch.no_grad(),
@app.post("/chat") (FastAPI), @pytest.fixture, @dataclass,
@lru_cache, @tool (agent asboblari), @retry. Xuddi shunday with
ham: with open(...), with torch.no_grad():, with client.messages.stream(...) as s:.
Bu bobda bu ikki mexanizmni ichidan tushunamiz va o'zimiz yozamiz.
Dekoratorlar qanday ishlashini funksiyalar birinchi darajali obyekt ekanidan boshlab bosqichma-bosqich tushuntiradi.
1. Asos: funksiyalar — obyektlar#
8-bobdan eslaymiz: funksiyani o'zgaruvchiga saqlash, argument sifatida berish va boshqa funksiyadan qaytarish mumkin. Dekorator aynan shu uchinchi xususiyatga asoslangan.
def wrapper_factory(fn):
def wrapper(*args, **kwargs):
print(f"→ {fn.__name__} chaqirildi")
result = fn(*args, **kwargs)
print(f"← {fn.__name__} tugadi")
return result
return wrapper
def salom(ism):
return f"Salom, {ism}"
salom = wrapper_factory(salom) # funksiyani "o'rab" qayta tayinladik
salom("Aziz")→ salom chaqirildi
← salom tugadiDekorator — funksiyani qabul qilib, (odatda) yangi funksiya
qaytaradigan funksiya. @ sintaksisi — oxirgi qatorning qisqa yozuvi:
@wrapper_factory
def salom(ism):
return f"Salom, {ism}"
# aynan shu bilan bir xil:
# def salom(ism): ...
# salom = wrapper_factory(salom)Muhim: dekorator funksiya ta'riflanganda (modul import qilinganda)
bir marta ishlaydi; wrapper esa har chaqiruvda ishlaydi.
2. functools.wraps — metama'lumotni saqlash#
Yuqoridagi dekoratorda muammo bor:
salom.__name__ # 'wrapper' — asl nom yo'qoldi!
salom.__doc__ # None — docstring yo'qoldiBu log'larni, traceback'larni, help()ni va ko'p kutubxonalarni
(FastAPI, pytest — ular funksiya imzosini o'qiydi) buzadi. Yechim —
har doim functools.wraps:
import functools
import time
def timer(fn):
@functools.wraps(fn) # nom, docstring, imzo va h.k. ko'chiriladi
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return fn(*args, **kwargs)
finally:
elapsed = time.perf_counter() - start
print(f"{fn.__name__}: {elapsed:.3f}s")
return wrapper
@timer
def embed_batch(texts):
"""Matnlarni vektorlarga aylantiradi."""
time.sleep(0.1)
return [[0.0] * 4 for _ in texts]
embed_batch(["a", "b"]) # embed_batch: 0.105s
embed_batch.__name__ # 'embed_batch'
embed_batch.__wrapped__ # asl (o'ralmagan) funksiya — testlarda foydalitry/finally — funksiya xato bersa ham vaqt chiqariladi.
Dekorator shabloni#
Deyarli har bir dekorator shu shaklda bo'ladi — uni yodlab oling:
import functools
def my_decorator(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
# ... oldin
result = fn(*args, **kwargs)
# ... keyin
return result
return wrapper*args, **kwargs — o'ralgan funksiya qanday argumentlar qabul qilishidan
qat'i nazar, hammasini o'zgarishsiz uzatish uchun (8-bob).
3. Parametrli dekoratorlar#
@retry(times=3) — bu yerda retry(times=3) avval chaqiriladi va
dekorator qaytaradi. Demak uch qavat funksiya kerak:
import functools
import logging
import random
import time
log = logging.getLogger(__name__)
def retry(times: int = 3, exceptions: tuple[type[Exception], ...] = (Exception,),
base_delay: float = 1.0):
"""Xato bo'lsa qayta urinadi: exponential backoff + jitter."""
def decorator(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
for attempt in range(1, times + 1):
try:
return fn(*args, **kwargs)
except exceptions as e:
if attempt == times:
raise
delay = base_delay * 2 ** (attempt - 1) * random.uniform(0.5, 1.5)
log.warning("%s: %d-urinish muvaffaqiyatsiz (%r), %.1fs kutamiz",
fn.__name__, attempt, e, delay)
time.sleep(delay)
return wrapper
return decorator
@retry(times=5, exceptions=(ConnectionError, TimeoutError))
def fetch_embeddings(texts):
...Bu qavatlar closure'lar (8-bob) orqali bog'langan: wrapper ichida
times, exceptions va fn — hammasi tashqi funksiyalardan "eslab
qolingan".
4. Dekoratorlarni ustma-ust qo'yish#
@timer
@retry(times=3)
def call_api(): ...
# aynan: call_api = timer(retry(times=3)(call_api))Pastdagisi birinchi qo'llanadi (funksiyaga eng yaqin), lekin chaqiruvda
eng tashqisi — timer — birinchi ishlaydi. Bu misolda timer barcha
urinishlarning umumiy vaqtini o'lchaydi. Tartibni almashtirsangiz —
har bir urinish alohida o'lchanadi.
5. Funksiyani o'zgartirmaydigan dekoratorlar: ro'yxatga olish#
Dekorator yangi funksiya qaytarishi shart emas — u funksiyani "qayd qilib", o'zini qaytarishi mumkin. Bu plagin tizimlari va agent asboblari uchun asosiy naqsh:
import inspect
TOOLS: dict[str, dict] = {}
def tool(fn):
"""Funksiyani LLM agent uchun asbob sifatida ro'yxatga oladi."""
sig = inspect.signature(fn)
TOOLS[fn.__name__] = {
"fn": fn,
"description": inspect.getdoc(fn) or "",
"params": list(sig.parameters),
}
return fn # funksiya o'zgarmaydi
@tool
def get_weather(city: str) -> str:
"""Shahar uchun joriy ob-havoni qaytaradi."""
return f"{city}: +18°C, quyoshli"
@tool
def calculator(expression: str) -> str:
"""Oddiy arifmetik ifodani hisoblaydi."""
...
print(list(TOOLS)) # ['get_weather', 'calculator']
print(TOOLS["get_weather"]["description"]) # Shahar uchun joriy ob-havoni qaytaradi.FastAPI'dagi @app.get("/users"), Flask'dagi @app.route(...), pytest'dagi
@pytest.fixture, Anthropic SDK'dagi @beta_tool — hammasi shu g'oya:
funksiyani qandaydir ro'yxatga yozib qo'yish. Real agent tizimida
inspect.signature va type hint'lardan JSON Schema yasaladi va modelga
yuboriladi (23-bob).
6. Standart kutubxonadagi dekoratorlar#
| Dekorator | Nima qiladi | Bob |
|---|---|---|
@property, @x.setter | Metodni atributga aylantiradi | 12 |
@classmethod, @staticmethod | Metod turlari | 12 |
@functools.cache, @lru_cache(maxsize=N) | Natijalarni keshlash | 8 |
@functools.cached_property | Bir marta hisoblanadigan property | 12 |
@functools.wraps(fn) | Dekorator ichida metama'lumotni ko'chirish | 14 |
@functools.singledispatch | Argument tipiga qarab turli amalga oshirish | — |
@dataclass | Klass dekoratori: __init__, __repr__... | 13 |
@abstractmethod | Abstrakt metod | 13 |
@contextlib.contextmanager | Generatordan kontekst menejer | 14 |
@typing.override | "Bu metod ota klassnikini almashtiradi" (3.12+) | 15 |
Klass dekoratorlari#
Dekorator klassni ham qabul qilishi mumkin — @dataclass shunday:
klassni oladi, unga metodlar qo'shadi va qaytaradi. O'zingiz ham yozishingiz
mumkin, lekin amalda bu kamdan-kam kerak bo'ladi.
7. Kontekst menejerlari: with#
with — "kirish va chiqishda nimadir qilish kerak" bo'lgan holatlar
uchun. Eng muhimi — chiqish har doim bajariladi, xato bo'lsa ham:
with open("data.txt", encoding="utf-8") as f: # kirish: fayl ochiladi
data = f.read()
# chiqish: fayl yopiladi — hatto read() xato bergan bo'lsa hamKontekst menejerlar ishlatiladigan joylar:
| Vazifa | Misol |
|---|---|
| Resursni bo'shatish | Fayl, tarmoq ulanishi, ma'lumotlar bazasi |
| Qulf olish/qo'yish | with lock: (17-bob) |
| Vaqtincha holatni o'zgartirish | with torch.no_grad():, with np.errstate(...) |
| Tranzaksiya | with db.transaction(): — xato bo'lsa rollback |
| Oqimni boshqarish | with client.messages.stream(...) as s: |
| Vaqt o'lchash | with Timer(): |
Protokol: __enter__ va __exit__#
import time
class Timer:
def __init__(self, name: str = "blok") -> None:
self.name = name
def __enter__(self) -> "Timer":
self.start = time.perf_counter()
return self # `as` dan keyingi nomga shu bog'lanadi
def __exit__(self, exc_type, exc, tb) -> bool:
self.elapsed = time.perf_counter() - self.start
status = "xato bilan" if exc_type else "muvaffaqiyatli"
print(f"{self.name}: {self.elapsed:.3f}s ({status})")
return False # False — xato bo'lsa, u yuqoriga ko'tarilsin
with Timer("tokenizatsiya") as t:
tokens = [s.split() for s in ["a b c"] * 100_000]
print(t.elapsed)__exit__ uchta argument oladi: istisno tipi, obyekti va traceback
(xato bo'lmasa — uchalasi None). Agar __exit__ True qaytarsa,
istisno yutib yuboriladi — buni faqat ataylab qiling.
with ifodasi hisoblanadi
Timer("tokenizatsiya")— kontekst menejer obyekti yaratiladi.__enter__ chaqiriladi
Qaytgan qiymat
as tga bog'lanadi.Blok bajariladi
Xato bo'lishi yoki bo'lmasligi mumkin.
__exit__ chaqiriladi — HAR DOIM
Xato bo'lgan bo'lsa, uning ma'lumotlari bilan.
Trueqaytarsa — xato yutiladi.
8. contextlib.contextmanager — generatordan kontekst menejer#
Klass yozish ko'pincha ortiqcha. Generator bilan ancha qisqa:
import time
from contextlib import contextmanager
@contextmanager
def timed(name: str):
start = time.perf_counter()
try:
yield # ← with bloki shu yerda bajariladi
finally:
print(f"{name}: {time.perf_counter() - start:.2f}s")
with timed("ma'lumot yuklash"):
time.sleep(0.05)yieldgacha — __enter__, yielddan keyin — __exit__. yield x
qilsangiz, x as ga bog'lanadi. try/finally majburiy — aks holda
blokda xato bo'lsa, tozalash kodi ishlamaydi.
Amaliy misol: muhit o'zgaruvchilarini vaqtincha o'zgartirish#
import os
from contextlib import contextmanager
@contextmanager
def temp_env(**overrides: str):
old = {k: os.environ.get(k) for k in overrides}
os.environ.update(overrides)
try:
yield
finally:
for k, v in old.items():
if v is None:
os.environ.pop(k, None)
else:
os.environ[k] = v
with temp_env(MODEL="claude-sonnet-5-5", DEBUG="1"):
print(os.environ["MODEL"]) # claude-sonnet-5-5
print(os.environ.get("MODEL")) # None — avvalgi holat tiklandiTestlarda juda foydali (garchi pytest'da tayyor monkeypatch bor — 16-bob).
Amaliy misol: model'ni baholash rejimi#
PyTorch'da baholashda gradient hisoblashni o'chirish va modelni eval
rejimiga o'tkazish kerak, keyin esa avvalgi holatga qaytarish:
from contextlib import contextmanager
@contextmanager
def evaluating(model):
"""Model'ni vaqtincha eval rejimiga o'tkazadi, keyin avvalgi rejimni tiklaydi."""
was_training = model.training
model.eval()
try:
yield model
finally:
if was_training:
model.train()
# with evaluating(model), torch.no_grad():
# val_loss = compute_loss(model, val_loader)9. contextlibning boshqa vositalari#
from contextlib import suppress, ExitStack, nullcontext, closing
# suppress — ma'lum xatoni ataylab e'tiborsiz qoldirish
with suppress(FileNotFoundError):
os.remove("tmp.lock") # yo'q bo'lsa — mayli
# ExitStack — oldindan noma'lum sondagi kontekst menejerlar
with ExitStack() as stack:
files = [stack.enter_context(open(p, encoding="utf-8")) for p in paths]
... # chiqishda hammasi yopiladi
# nullcontext — "hech narsa qilmaydigan" menejer (shartli holatlar uchun)
ctx = timed("debug") if debug else nullcontext()
with ctx:
run()Bir nechta menejerni bitta withda:
with open("in.txt", encoding="utf-8") as src, open("out.txt", "w", encoding="utf-8") as dst:
dst.write(src.read().upper())
with ( # Python 3.10+: qavslar bilan ko'p qatorli
open("a.txt", encoding="utf-8") as a,
open("b.txt", encoding="utf-8") as b,
):
...10. Dekorator va kontekst menejer — qachon qaysi biri#
| Dekorator | Kontekst menejer | |
|---|---|---|
| Nimaga qo'llanadi | Butun funksiyaga, har chaqiruvda | Kod blokiga, bir marta |
| Qachon | Xatti-harakat funksiyaning "xususiyati" (retry, cache, log) | Xatti-harakat chaqiruvchiga bog'liq (bu yerda vaqtincha...) |
| Misol | @retry, @cache, @app.get | with open, with no_grad(), with timed() |
Ba'zilari ikkala usulda ham ishlaydi: torch.no_grad() — ham @torch.no_grad()
dekorator, ham with torch.no_grad():. contextlib.ContextDecorator
vorisi bo'lgan menejerlar shunday ishlaydi.
11. Amaliyot: LLM chaqiruvlari uchun "observability"#
Real AI ilovada har bir model chaqiruvini kuzatish kerak: qancha vaqt ketdi, muvaffaqiyatlimi, qancha token. Dekorator + kontekst menejer bilan:
import functools
import json
import logging
import time
from contextlib import contextmanager
log = logging.getLogger("llm")
@contextmanager
def span(name: str, **attrs):
"""Bitta amalni kuzatadi va natijani JSON qatori sifatida logga yozadi."""
record = {"span": name, **attrs}
start = time.perf_counter()
try:
yield record # chaqiruvchi record'ga ma'lumot qo'shishi mumkin
record["status"] = "ok"
except Exception as e:
record["status"] = "error"
record["error"] = repr(e)
raise
finally:
record["ms"] = round((time.perf_counter() - start) * 1000, 1)
log.info(json.dumps(record, ensure_ascii=False))
def traced(fn):
"""Funksiyaning har bir chaqiruvini span ichida bajaradi."""
@functools.wraps(fn)
def wrapper(*args, **kwargs):
with span(fn.__name__) as rec:
result = fn(*args, **kwargs)
if isinstance(result, dict) and "usage" in result:
rec["tokens"] = result["usage"]
return result
return wrapper
@traced
def fake_llm(prompt: str) -> dict:
time.sleep(0.02)
return {"text": prompt.upper(), "usage": {"input": len(prompt), "output": len(prompt)}}
logging.basicConfig(level=logging.INFO, format="%(message)s")
fake_llm("salom")
# {"span": "fake_llm", "tokens": {"input": 5, "output": 5}, "status": "ok", "ms": 20.3}Langfuse, LangSmith, OpenTelemetry kabi kuzatuv tizimlari aynan shu g'oyada ishlaydi — faqat loglarni maxsus serverga yuboradi.
Nima noto'g'ri ketishi mumkin#
| Xato | Oqibat | Tuzatish |
|---|---|---|
functools.wraps yo'q | Nom/docstring yo'qoladi; FastAPI/pytest buziladi | Har doim @functools.wraps(fn) |
wrapperda return unutilgan | Funksiya None qaytaradi | return fn(*args, **kwargs) |
@retry o'rniga @retry() (yoki aksincha) | G'alati TypeError | Parametrli dekoratorni qavs bilan |
| Hamma istisnoda retry | Bug'lar ham qayta-qayta urinadi | Faqat vaqtinchalik xatolar: tarmoq, 429, 5xx |
@contextmanagerda try/finally yo'q | Xatoda tozalash ishlamaydi | yieldni try/finally ichiga |
__exit__ tasodifan True qaytaradi | Xatolar jim yo'qoladi | False (yoki None) qaytaring |
| Dekoratorda og'ir ish ta'rif paytida | Import sekin | Og'ir ishni wrapper ichiga |
suppress(Exception) | Hamma xato yutiladi | Faqat aniq, kutilgan xato |
1. log_calls dekoratori
@log_calls dekoratorini yozing: har chaqiruvda funksiya nomi,
argumentlari va natijasini (yoki xatosini) logging orqali chiqarsin.
Uzun argumentlarni (100 belgidan uzun) qisqartirsin. Rekursiv funksiyada
(faktorial) sinab ko'ring.
Maslahatni ko'rsatish
functools.wraps; argumentlarni repr bilan: ', '.join([repr(a) for a in args] + [f'{k}={v!r}' for k, v in kwargs.items()]).
2. Rate limiter dekoratori
@rate_limit(calls=5, period=1.0) dekoratorini yozing: funksiya period
soniya ichida calls martadan ko'p chaqirilsa, keyingi chaqiruv kerakli
vaqtgacha kutsin (xato bermasin). 12 marta chaqirib, umumiy vaqtni o'lchang.
Maslahatni ko'rsatish
Closure ichida deque(maxlen=calls) — oxirgi chaqiruvlar vaqti; to'lgan bo'lsa, eng eskisidan period o'tguncha time.sleep().
3. Disk keshi
@disk_cache(cache_dir) dekoratorini yozing: natijani JSON fayl sifatida
saqlasin va keyingi chaqiruvlarda fayldan o'qisin (argumentlar va natija
JSON'ga mos deb hisoblang). Bu qimmat LLM chaqiruvlarini ishlab chiqish
paytida qayta-qayta to'lamaslik uchun juda foydali. Atomik yozishni
unutmang (11-bob).
Maslahatni ko'rsatish
Kalit: hashlib.sha256(json.dumps([fn.__name__, args, kwargs], sort_keys=True).encode()).hexdigest(); fayl: cache_dir / f'{key}.json'.
4. Kontekst menejer: ishchi papka
@contextmanager bilan working_directory(path) yozing: blok ichida
joriy papkani path ga o'zgartirsin (os.chdir), chiqishda avvalgisiga
qaytarsin — xato bo'lsa ham. Keyin xuddi shuni __enter__/__exit__
li klass sifatida yozing. (Python 3.11+ da tayyor contextlib.chdir bor
— yozib bo'lgach, solishtiring.)
- 01Dekorator — funksiyani olib, funksiya qaytaradigan funksiya; @d — f = d(f) ning qisqa yozuvi.
- 02Dekorator ta'rif paytida bir marta, wrapper har chaqiruvda ishlaydi.
- 03Har doim functools.wraps; wrapper(*args, **kwargs) va return fn(*args, **kwargs).
- 04Parametrli dekorator — uch qavat: sozlamalar → decorator → wrapper.
- 05Ro'yxatga oluvchi dekoratorlar (fn ni o'zini qaytaradi) — routing, plaginlar, agent asboblari.
- 06with — kirish/chiqish; __exit__ har doim ishlaydi; True qaytarsa xato yutiladi.
- 07@contextmanager: yield'gacha — kirish, keyin — chiqish; try/finally majburiy.
- 08suppress, ExitStack, nullcontext — contextlib'ning foydali vositalari.
Mustahkamlash
Test va amaliy masalalar
Avval testni ishlang — har javobdan keyin izoh chiqadi. Keyin masalalarni shu yerning o'zida yechib, brauzerdagi haqiqiy Python bilan avtomatik tekshiring. Natijalaringiz kurs progressi sahifasida jamlanadi.
Bilimni tekshirish
Bob bo'yicha test
12 ta savol · har javobdan keyin to'g'ri yoki xatoligi va izoh ko'rsatiladi · oxirida natija foizda.
Amaliy masalalar
Kodni yozing va «Tekshirish»ni bosing (yoki Ctrl/⌘ + Enter). Kodingiz shu brauzerda saqlanib turadi.
01count_calls dekoratori
@count_calls — funksiya necha marta chaqirilganini wrapper.calls atributida sanasin; functools.wraps bilan nom va docstring saqlansin; argumentlar va natija o'zgarishsiz uzatilsin.
02Parametrli retry
@retry(times=3, exceptions=(ConnectionError,)) — xato bo'lsa times martagacha qayta urinsin (kutishsiz), oxirida xatoni ko'tarsin; ro'yxatda yo'q xato — darhol. functools.wraps ishlating.
03temp_env kontekst menejeri
temp_env(env, **overrides) — env lug'atiga (amalda os.environ) qiymatlarni vaqtincha qo'ysin va blokdan chiqishda — xato bo'lsa ham — avvalgi holatni tiklasin (oldin yo'q bo'lgan kalitlar o'chirilsin). @contextmanager ishlating.