OzbekPython / Bob 18

Dekoratorlar

Funksiyalar obyekt sifatida, içki funksiyalar va closure, dekorator yoziş, functools.wraps, argumentli dekoratorlar va amaliy misollar.

Yuqori daraja19 daqiqa18 kod misoli18 / 24

Nima uçun dekoratorlar kerak?

Tasavvur qiling, loyihangizda önlab funksiya bor va har birining qança vaqt işlaşini ölçamoqçisiz. Har bir funksiya içiga vaqt ölçaydigan kodni köçirib yoziş — zerikarli va xavfli iş: bir joyda unutasiz, boşqa joyda xato qilasiz. Dekorator aynan şu muammoni hal qiladi: u funksiyani özgartirmasdan, uning atrofiga qöşimça xatti-harakat “örab” beradi.

Dekoratorni tuşuniş uçun avval uçta ğoyani özlaştiriş kerak: funksiyalar oddiy obyekt, funksiya içida funksiya yoziş mumkin va içki funksiya taşqi funksiyaning özgaruvçilarini “eslab qoladi”. Keling, bosqiçma-bosqiç boramiz.

Funksiyalar ham obyekt

Python'da funksiya ham son yoki satr kabi obyekt. Uni özgaruvçiga tenglaştiriş, röyxatga soliş yoki boşqa funksiyaga argument sifatida beriş mumkin. Eʼtibor bering: funksiya nomidan keyin qavs qöymasak, u çaqirilmaydi, balki obyektning özi olinadi.

Python
def salom_ber(ism: str) -> str:
    return f"Assalomu alaykum, {ism}!"

xabar = salom_ber          # qavs yöq — funksiya çaqirilmadi
print(xabar("Dilnoza"))
print(xabar is salom_ber)
print(type(salom_ber))
print(salom_ber.__name__)

Natija:

Natija
Assalomu alaykum, Dilnoza!
True
<class 'function'>
salom_ber

xabar va salom_ber — bitta funksiya obyektiga işora qiluvçi ikki nom. Funksiyaning __name__ atributi esa uning asl nomini saqlaydi; bu keyinroq muhim böladi.

Funksiyani argument sifatida uzatiş

Funksiyani boşqa funksiyaga beriş juda kuçli usul. Masalan, sorted() funksiyasining key parametri aynan şunday işlaydi. Özimiz ham şunday funksiya yozamiz: u narxlar röyxatiga istalgan amalni qöllaydi.

Python
def chegirma(narx: int) -> int:
    return narx * 90 // 100

def qqs_qoshish(narx: int) -> int:
    return narx * 112 // 100

def hammasiga_qollash(amal, narxlar: list[int]) -> list[int]:
    return [amal(n) for n in narxlar]

narxlar = [10_000, 25_000, 48_000]
print(hammasiga_qollash(chegirma, narxlar))
print(hammasiga_qollash(qqs_qoshish, narxlar))

Natija:

Natija
[9000, 22500, 43200]
[11200, 28000, 53760]

hammasiga_qollash qaysi amal bajarilişini bilmaydi va bilişi ham şart emas. Bu kodni moslaşuvçan qiladi: yangi amal kerak bölsa, faqat yangi kiçik funksiya yozasiz.

Funksiya qaytaruvçi funksiya va closure

Funksiya içida boşqa funksiya yaratib, uni natija sifatida qaytariş mumkin. Eng qiziq tomoni şundaki, içki funksiya taşqi funksiyaning özgaruvçilarini taşqi funksiya işini tugatgandan keyin ham eslab qoladi. Bunday funksiya closure (yopiq funksiya) deb ataladi.

Python
def kopaytiruvchi(koeffitsiyent: float):
    def ichki(son: float) -> float:
        return son * koeffitsiyent   # taşqi özgaruvçini eslab qoldi
    return ichki

dollar_som = kopaytiruvchi(12_650)
evro_som = kopaytiruvchi(13_700)

print(dollar_som(20))
print(evro_som(20))
print(dollar_som.__closure__[0].cell_contents)

Natija:

Natija
253000
274000
12650

Içki funksiya taşqi özgaruvçini özgartirmoqçi bölsa, nonlocal kalit sözi kerak. Quyidagi hisoblagiç har çaqirilganda bittaga oşadi:

Python
def hisoblagich_yarat():
    soni = 0
    def keyingi() -> int:
        nonlocal soni
        soni += 1
        return soni
    return keyingi

navbat = hisoblagich_yarat()
print(navbat(), navbat(), navbat())

Natija:

Natija
1 2 3

*args va **kwargs: qisqa takror

Dekorator istalgan funksiyani öraşi kerak, şuning uçun u funksiya qanday argumentlar olişini oldindan bilmaydi. Bu yerda *args (pozitsion argumentlar kortej körinişida) va **kwargs (nomlangan argumentlar luğat körinişida) yordamga keladi.

Python
def korsat(*args, **kwargs):
    print("args:", args)
    print("kwargs:", kwargs)

def uzat(*args, **kwargs):
    # olingan hamma narsani boşqa funksiyaga özgarişsiz beramiz
    korsat(*args, **kwargs)

uzat(1, 2, viloyat="Samarqand", yil=2026)

Natija:

Natija
args: (1, 2)
kwargs: {'viloyat': 'Samarqand', 'yil': 2026}

Birinçi dekoratorimiz

Endi hamma bölaklar tayyor. Dekorator — bu funksiya oladigan va yangi funksiya qaytaradigan funksiya. Yangi funksiya (odatda wrapper yoki orovchi deb nomlanadi) asl funksiyani çaqiradi, lekin oldin va keyin öz işini bajaradi.

Python
def baland_ovoz(funksiya):
    def orovchi(*args, **kwargs):
        natija = funksiya(*args, **kwargs)
        return natija.upper() + "!"
    return orovchi

def tabrik(ism: str) -> str:
    return f"tuğilgan kuningiz bilan, {ism}"

tabrik = baland_ovoz(tabrik)   # qölda “bezaş”
print(tabrik("Sardor"))

Natija:

Natija
TUĞILGAN KUNINGIZ BILAN, SARDOR!

tabrik = baland_ovoz(tabrik) qatori eski funksiyani öralgan yangi funksiya bilan almaştirdi. Endi tabrik deb çaqirsak, aslida orovchi işlaydi, u esa içida asl funksiyani çaqiradi.

@ sintaksisi

Yuqoridagi tabrik = baland_ovoz(tabrik) yozuvi biroz noqulay: funksiya nomi uç marta takrorlanadi. Python buning uçun qisqa yozuv beradi — funksiya taʼrifining ustiga @dekorator_nomi yoziladi. Maʼnosi aynan bir xil.

Python
def chiziq_bilan(funksiya):
    def orovchi(*args, **kwargs):
        print("-" * 20)
        natija = funksiya(*args, **kwargs)
        print("-" * 20)
        return natija
    return orovchi

@chiziq_bilan
def chek_chiqar(mahsulot: str, narx: int) -> None:
    print(f"{mahsulot}: {narx:,} söm".replace(",", " "))

chek_chiqar("Non", 4_000)
chek_chiqar(mahsulot="Çoy", narx=18_500)

Natija:

Natija
--------------------
Non: 4 000 söm
--------------------
--------------------
Çoy: 18 500 söm
--------------------

Bir funksiyaga bir neçta dekorator qöyiş ham mumkin. Ular pastdan yuqoriga qarab qöllanadi: funksiyaga eng yaqin dekorator birinçi bölib öraydi.

functools.wraps: funksiya “pasporti”ni saqlaş

Dekoratorning bitta yaşirin muammosi bor: öralgan funksiya öz nomi va hujjat satrini (docstring) yöqotadi, çunki endi uning örnida içki funksiya turibdi. Bu xatolarni qidirişda va hujjatlar yaratişda çalkaşlik keltirib çiqaradi. Quyidagi misolda içki funksiyani inglizça wrapper deb nomladik — bu ham keng tarqalgan nom.

Python
from functools import wraps

def oddiy(funksiya):
    def wrapper(*args, **kwargs):
        return funksiya(*args, **kwargs)
    return wrapper

def toza(funksiya):
    @wraps(funksiya)
    def wrapper(*args, **kwargs):
        return funksiya(*args, **kwargs)
    return wrapper

@oddiy
def ayir():
    """Ayiriş funksiyasi hujjati."""

@toza
def kopaytir():
    """Köpaytiriş funksiyasi hujjati."""

print(ayir.__name__, "|", ayir.__doc__)
print(kopaytir.__name__, "|", kopaytir.__doc__)

Natija:

Natija
wrapper | None
kopaytir | Köpaytiriş funksiyasi hujjati.

@wraps(funksiya) asl funksiyaning nomi, hujjati va boşqa maʼlumotlarini wrapperga köçiradi. Şuning uçun har bir dekoratorda wraps işlatişni odat qiling.

Argumentli dekoratorlar

Baʼzan dekoratorni sozlaş kerak: masalan, funksiyani neça marta qayta urinib körişni körsatiş. Buning uçun yana bir qavat qöşiladi: taşqi funksiya sozlamani oladi va dekorator qaytaradi.

Python
from functools import wraps

def qayta_urin(marta: int):
    def dekorator(funksiya):
        @wraps(funksiya)
        def orovchi(*args, **kwargs):
            for urinish in range(1, marta + 1):
                try:
                    return funksiya(*args, **kwargs)
                except ConnectionError as xato:
                    print(f"{urinish}-uriniş muvaffaqiyatsiz: {xato}")
            print("Barça urinişlar tugadi.")
        return orovchi
    return dekorator

holat = {"çaqiruv": 0}

@qayta_urin(marta=3)
def server_bilan_boglan() -> str:
    holat["çaqiruv"] += 1
    if holat["çaqiruv"] < 3:
        raise ConnectionError("server javob bermadi")
    return "Ulaniş muvaffaqiyatli"

print(server_bilan_boglan())

Natija:

Natija
1-uriniş muvaffaqiyatsiz: server javob bermadi
2-uriniş muvaffaqiyatsiz: server javob bermadi
Ulaniş muvaffaqiyatli

@qayta_urin(marta=3) yozuvini ikki bosqiçda öqing: avval qayta_urin(marta=3) çaqiriladi va u dekorator funksiyasini qaytaradi, keyin öşa dekorator funksiyamizga qöllanadi.

Amaliy dekorator: vaqtni ölçaş

Endi kirişda aytilgan vazifaga qaytamiz. Vaqtni aniq ölçaş uçun time.perf_counter() işlatiladi: u eng aniq soatdan foydalanadi va aynan oraliqlarni ölçaş uçun möljallangan.

Python
import time
from functools import wraps

def vaqtni_olcha(funksiya):
    @wraps(funksiya)
    def orovchi(*args, **kwargs):
        boshlanish = time.perf_counter()
        natija = funksiya(*args, **kwargs)
        sarflandi = time.perf_counter() - boshlanish
        print(f"{funksiya.__name__} {sarflandi:.4f} soniya işladi")
        return natija
    return orovchi

@vaqtni_olcha
def kvadratlar_yigindisi(n: int) -> int:
    return sum(i * i for i in range(n))

print(kvadratlar_yigindisi(1_000_000))

Natija:

Natija
kvadratlar_yigindisi 0.0479 soniya işladi
333332833333500000

Amaliy dekorator: natijalarni eslab qoliş (keş)

Agar funksiya bir xil argument bilan qayta-qayta çaqirilsa va har safar bir xil natija bersa, natijani eslab qoliş (keşlaş) vaqtni keskin tejaydi. Klassik misol — Fibonaççi sonlari: oddiy rekursiv yeçim bir xil qiymatlarni millionlab marta qayta hisoblaydi.

Python
from functools import wraps

def kesh(funksiya):
    xotira: dict = {}
    @wraps(funksiya)
    def orovchi(*args):
        if args not in xotira:
            xotira[args] = funksiya(*args)
        return xotira[args]
    orovchi.xotira = xotira
    return orovchi

@kesh
def fibonachchi(n: int) -> int:
    if n < 2:
        return n
    return fibonachchi(n - 1) + fibonachchi(n - 2)

print(fibonachchi(80))
print("Xotiradagi qiymatlar soni:", len(fibonachchi.xotira))

Natija:

Natija
23416728348467685
Xotiradagi qiymatlar soni: 81

Bu ğoya şunçalik keng tarqalganki, standart kutubxonada tayyor yeçim bor: functools.lru_cache. U hajm çegarasini qöllab-quvvatlaydi va statistikani ham körsatadi. maxsize=None — çeksiz xotira degani; Python 3.9 dan boşlab buning qisqa şakli functools.cache ham bor.

Python
from functools import lru_cache

@lru_cache(maxsize=None)
def yollar_soni(qator: int, ustun: int) -> int:
    """Tör böylab faqat öngga va pastga yurib neça xil yöl bor."""
    if qator == 0 or ustun == 0:
        return 1
    return yollar_soni(qator - 1, ustun) + yollar_soni(qator, ustun - 1)

print(yollar_soni(16, 16))
info = yollar_soni.cache_info()
print(f"Keşdan olindi: {info.hits}, hisoblandi: {info.misses}")

Natija:

Natija
601080390
Keşdan olindi: 225, hisoblandi: 288

Amaliy dekorator: jurnal (log) yoziş

Xatolarni qidirişda qaysi funksiya qanday argumentlar bilan çaqirilgani va nima qaytargani haqida maʼlumot juda foydali. Quyidagi dekorator buni avtomatik yozib boradi. Haqiqiy loyihalarda print örniga logging modulidan foydalaniladi, lekin ğoya bir xil.

Python
from functools import wraps

def jurnal(funksiya):
    @wraps(funksiya)
    def orovchi(*args, **kwargs):
        qismlar = [repr(a) for a in args]
        qismlar += [f"{k}={v!r}" for k, v in kwargs.items()]
        print(f"[LOG] {funksiya.__name__}({', '.join(qismlar)}) çaqirildi")
        natija = funksiya(*args, **kwargs)
        print(f"[LOG] {funksiya.__name__} -> {natija!r}")
        return natija
    return orovchi

@jurnal
def orta_baho(baholar: list[int], xona: int = 1) -> float:
    return round(sum(baholar) / len(baholar), xona)

orta_baho([5, 4, 5, 3], xona=2)

Natija:

Natija
[LOG] orta_baho([5, 4, 5, 3], xona=2) çaqirildi
[LOG] orta_baho -> 4.25

Amaliy dekorator: ruxsatni tekşiriş

Veb-ilovalarda köp funksiyalar faqat maʼlum roldagi foydalanuvçilar uçun oçiq böladi. Tekşiruvni har bir funksiya içiga yoziş örniga, uni argumentli dekoratorga çiqaramiz. Funksiyaning birinçi argumenti foydalanuvçi deb kelişib olamiz.

Python
from dataclasses import dataclass
from functools import wraps

@dataclass
class Foydalanuvchi:
    ism: str
    rol: str

def ruxsat(*rollar: str):
    def dekorator(funksiya):
        @wraps(funksiya)
        def orovchi(foydalanuvchi: Foydalanuvchi, *args, **kwargs):
            if foydalanuvchi.rol not in rollar:
                raise PermissionError(f"{foydalanuvchi.ism} uçun ruxsat yöq")
            return funksiya(foydalanuvchi, *args, **kwargs)
        return orovchi
    return dekorator

@ruxsat("admin", "öqituvçi")
def baho_qoy(kim: Foydalanuvchi, talaba: str, baho: int) -> str:
    return f"{kim.ism}: {talaba}ga {baho} qöyildi"

ustoz = Foydalanuvchi("Malika opa", "öqituvçi")
talaba = Foydalanuvchi("Jasur", "talaba")

print(baho_qoy(ustoz, "Jasur", 5))
try:
    baho_qoy(talaba, "Jasur", 5)
except PermissionError as xato:
    print("Xato:", xato)

Natija:

Natija
Malika opa: Jasurga 5 qöyildi
Xato: Jasur uçun ruxsat yöq

Endi yangi “himoyalangan” funksiya kerak bölsa, tepasiga bitta @ruxsat(...) qatorini yoziş kifoya. Ruxsat qoidalari bitta joyda turadi va ularni özgartiriş oson.

Xulosa

  • Python'da funksiyalar obyekt: ularni özgaruvçiga beriş, argument sifatida uzatiş va qaytariş mumkin.
  • Içki funksiya taşqi özgaruvçilarni eslab qoladi (closure); özgartiriş uçun nonlocal kerak.
  • Dekorator — funksiya olib, yangi funksiya qaytaradigan funksiya; @nom yozuvi f = nom(f) ning qisqa şakli.
  • *args, **kwargs dekoratorni istalgan funksiyaga mos qiladi, functools.wraps esa asl nom va hujjatni saqlaydi.
  • Argumentli dekorator uç qavatdan iborat: sozlama → dekorator → örovçi.
  • Amaliy qöllanişlar: vaqtni ölçaş, keşlaş (lru_cache), jurnal yoziş, ruxsat tekşiriş, qayta uriniş.

Maşqlar

  1. necha_marta dekoratorini yozing: u funksiya neça marta çaqirilganini chaqiruvlar atributida saqlasin. wraps işlatişni unutmang.
Yeçim
Python
from functools import wraps

def necha_marta(funksiya):
    @wraps(funksiya)
    def orovchi(*args, **kwargs):
        orovchi.chaqiruvlar += 1
        return funksiya(*args, **kwargs)
    orovchi.chaqiruvlar = 0
    return orovchi

@necha_marta
def salom(ism):
    return f"Salom, {ism}"

salom("Ali")
salom("Vali")
print(salom.__name__, salom.chaqiruvlar)
  1. takrorla(n) argumentli dekoratorini yozing: u funksiyani n marta çaqirsin va barça natijalarni röyxat qilib qaytarsin.
Yeçim
Python
import random
from functools import wraps

def takrorla(n: int):
    def dekorator(funksiya):
        @wraps(funksiya)
        def orovchi(*args, **kwargs):
            return [funksiya(*args, **kwargs) for _ in range(n)]
        return orovchi
    return dekorator

@takrorla(5)
def zar_tashla() -> int:
    return random.randint(1, 6)

print(zar_tashla())
  1. musbat_son dekoratorini yozing: funksiyaga berilgan pozitsion argumentlardan birortasi manfiy son bölsa, ValueError kötarsin. Uni ikki narxni qöşadigan funksiyada sinab köring.
Yeçim
Python
from functools import wraps

def musbat_son(funksiya):
    @wraps(funksiya)
    def orovchi(*args, **kwargs):
        for qiymat in args:
            if isinstance(qiymat, (int, float)) and qiymat < 0:
                raise ValueError(f"Manfiy qiymat: {qiymat}")
        return funksiya(*args, **kwargs)
    return orovchi

@musbat_son
def jami(a: int, b: int) -> int:
    return a + b

print(jami(12_000, 5_500))
try:
    jami(12_000, -5_500)
except ValueError as xato:
    print("Xato:", xato)
  1. Rekursiv faktorial funksiyasini yozing va unga lru_cache hamda özingiz yozgan vaqtni_olcha dekoratorini qöllang. Qaysi tartibda yoziş kerakligini öylab köring: rekursiv çaqiruvlar har birida vaqt çiqarmasligi uçun vaqtni_olchani alohida örovçi funksiyaga qöying.
Yeçim
Python
import time
from functools import lru_cache, wraps

def vaqtni_olcha(funksiya):
    @wraps(funksiya)
    def orovchi(*args, **kwargs):
        boshlanish = time.perf_counter()
        natija = funksiya(*args, **kwargs)
        print(f"{funksiya.__name__}: {time.perf_counter() - boshlanish:.6f} s")
        return natija
    return orovchi

@lru_cache(maxsize=None)
def faktorial(n: int) -> int:
    return 1 if n <= 1 else n * faktorial(n - 1)

@vaqtni_olcha
def faktorial_hisobla(n: int) -> int:
    return faktorial(n)

print(len(str(faktorial_hisobla(200))), "xonali son")
print(len(str(faktorial_hisobla(200))), "xonali son (keşdan)")