aidoc.uz

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.

▶ Video dars· Corey Schafer· 30:27· ingliz tilida
Python Tutorial: Decorators - Dynamically Alter The Functionality Of Your Functions

Dekoratorlar qanday ishlashini funksiyalar birinchi darajali obyekt ekanidan boshlab bosqichma-bosqich tushuntiradi.

YouTube'da ochish ↗

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 tugadi

Dekorator — 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)
def salom
asl funksiya yaratildi
wrapper_factory(salom)
dekorator chaqirildi — BIR MARTA, ta'rif paytida
salom = wrapper
nom endi o'ramga ishora qiladi
salom("Aziz")
wrapper ishlaydi → ichida asl fn

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'qoldi

Bu 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 foydali

try/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):
    ...
retry(times=5, ...)
sozlamalarni qabul qiladi → decorator qaytaradi
1
decorator(fn)
funksiyani qabul qiladi → wrapper qaytaradi
2
wrapper(*args, **kwargs)
har chaqiruvda ishlaydi
3

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#

DekoratorNima qiladiBob
@property, @x.setterMetodni atributga aylantiradi12
@classmethod, @staticmethodMetod turlari12
@functools.cache, @lru_cache(maxsize=N)Natijalarni keshlash8
@functools.cached_propertyBir marta hisoblanadigan property12
@functools.wraps(fn)Dekorator ichida metama'lumotni ko'chirish14
@functools.singledispatchArgument tipiga qarab turli amalga oshirish—
@dataclassKlass dekoratori: __init__, __repr__...13
@abstractmethodAbstrakt metod13
@contextlib.contextmanagerGeneratordan kontekst menejer14
@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 ham

Kontekst menejerlar ishlatiladigan joylar:

VazifaMisol
Resursni bo'shatishFayl, tarmoq ulanishi, ma'lumotlar bazasi
Qulf olish/qo'yishwith lock: (17-bob)
Vaqtincha holatni o'zgartirishwith torch.no_grad():, with np.errstate(...)
Tranzaksiyawith db.transaction(): — xato bo'lsa rollback
Oqimni boshqarishwith client.messages.stream(...) as s:
Vaqt o'lchashwith 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.

  1. with ifodasi hisoblanadi

    Timer("tokenizatsiya") — kontekst menejer obyekti yaratiladi.

  2. __enter__ chaqiriladi

    Qaytgan qiymat as t ga bog'lanadi.

  3. Blok bajariladi

    Xato bo'lishi yoki bo'lmasligi mumkin.

  4. __exit__ chaqiriladi — HAR DOIM

    Xato bo'lgan bo'lsa, uning ma'lumotlari bilan. True qaytarsa — 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 tiklandi

Testlarda 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#

DekoratorKontekst menejer
Nimaga qo'llanadiButun funksiyaga, har chaqiruvdaKod blokiga, bir marta
QachonXatti-harakat funksiyaning "xususiyati" (retry, cache, log)Xatti-harakat chaqiruvchiga bog'liq (bu yerda vaqtincha...)
Misol@retry, @cache, @app.getwith 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#

XatoOqibatTuzatish
functools.wraps yo'qNom/docstring yo'qoladi; FastAPI/pytest buziladiHar doim @functools.wraps(fn)
wrapperda return unutilganFunksiya None qaytaradireturn fn(*args, **kwargs)
@retry o'rniga @retry() (yoki aksincha)G'alati TypeErrorParametrli dekoratorni qavs bilan
Hamma istisnoda retryBug'lar ham qayta-qayta urinadiFaqat vaqtinchalik xatolar: tarmoq, 429, 5xx
@contextmanagerda try/finally yo'qXatoda tozalash ishlamaydiyieldni try/finally ichiga
__exit__ tasodifan True qaytaradiXatolar jim yo'qoladiFalse (yoki None) qaytaring
Dekoratorda og'ir ish ta'rif paytidaImport sekinOg'ir ishni wrapper ichiga
suppress(Exception)Hamma xato yutiladiFaqat aniq, kutilgan xato
Amaliy topshiriq

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()]).

Amaliy topshiriq

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().

Amaliy topshiriq

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'.

Amaliy topshiriq

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.)

Yodda saqlang
  • 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.

Test: topshirilmaganMasalalar: 0/3

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.