IlmHamroh
Python kursi/Ilgor til imkoniyatlari7/17-dars28 daqiqa
Mundarija (22)

10.7-dars: contextlib

10-QISM — ILG'OR TIL IMKONIYATLARI · 7-dars


1. Kirish va motivatsiya

9.3-darsda @contextmanager ni context manager yozishning qisqa yo'li sifatida ko'rgan edik. Mana shunday yozilgan taymer:

python
from contextlib import contextmanager
import time

@contextmanager
def taymer(nom):
    bosh = time.perf_counter()
    yield
    print(f"{nom}: {time.perf_counter() - bosh:.3f} s")

with taymer("import"):
    malumotlarni_yukla()

Ishlaydi. Endi malumotlarni_yukla() xato bersa nima bo'ladi? Vaqt chop etilmaydi. Chunki yield dan keyingi qator finally ichida emas: blokdagi istisno generator ichiga aynan yield qatorida "otiladi", va funksiyaning qolgan qismi bajarilmay qoladi.

Yana bir holat:

python
t = taymer("hisobot")
with t: ...
with t: ...     # AttributeError: '_GeneratorContextManager' object has no attribute 'args'

Bu xabardan hech narsa tushunib bo'lmaydi. Aslida sabab oddiy: generator faqat bir marta ishlaydi.

contextlib vositalari qulay, lekin qanday ishlashini bilmasangiz, ular xatolarni yashiradi. Bu darsda 9.3-darsdagi "qanday ishlatiladi" dan "ichida nima bo'ladi" ga o'tamiz.

Bu darsda:

  • @contextmanager ichidan: generator qanday context manager'ga aylanadi
  • yield atrofidagi uch qoida
  • Context manager'ni dekorator sifatida ishlatish — ContextDecorator
  • ExitStack chuqur: enter_context, callback, push, pop_all
  • Resursga egalikni boshqa obyektga o'tkazish
  • suppress va ExceptionGroup, nullcontext, chdir, redirect_stdout, AbstractContextManager

2. Nazariya — chuqur tushuntirish

2.1. @contextmanager ichidan

@contextmanager generator funksiyani oladi va har chaqiruvda yangi _GeneratorContextManager obyektini qaytaradi. Uning mantig'i soddalashtirilgan holda:

python
class GenMenejer:
    def __init__(self, funksiya, args, kwargs):
        self.gen = funksiya(*args, **kwargs)

    def __enter__(self):
        try:
            return next(self.gen)                    # ⭐ yield gacha bajaradi
        except StopIteration:
            raise RuntimeError("generator didn't yield") from None

    def __exit__(self, tur, qiymat, tb):
        if tur is None:                              # blok xatosiz tugadi
            try:
                next(self.gen)                       # yield dan keyingi qismni bajaradi
            except StopIteration:
                return False
            raise RuntimeError("generator didn't stop")

        try:
            self.gen.throw(qiymat)                   # ⭐ istisnoni yield qatoriga otadi
        except StopIteration as e:
            return e is not qiymat                   # generator jim tugadi → yutildi
        except BaseException as e:
            if e is qiymat:
                return False                         # xuddi o'sha istisno → o'tkazib yuboradi
            raise                                    # boshqa istisno → uni ko'taradi
        raise RuntimeError("generator didn't stop after throw()")

Demak:

Generator ichida Natija
yield gacha bo'lgan kod __enter__
yield qiymati as NOM
yield dan keyingi kod __exit__ — faqat blok xatosiz tugasa
Blokdagi istisno yield qatorida ko'tariladi
Generator istisnoni ushlab, jim tugasa Istisno yutiladi
Generator istisnoni qayta ko'tarsa Istisno o'tadi
Generator boshqa istisno ko'tarsa Asl istisno o'rniga shu chiqadi

Haqiqiy kodda PEP 479 (10.3-dars) tufayli blokdagi StopIteration generator ichida RuntimeError ga aylanishini hisobga oladigan qo'shimcha tekshiruv ham bor.

2.2. yield atrofidagi uch qoida

1. Tozalash finally da bo'lsin.

python
@contextmanager
def ulanish(manzil):
    u = ulan(manzil)
    try:
        yield u
    finally:
        u.yop()             # ✅ blokda xato bo'lsa ham

2. Aniq bitta yield. Hech yield qilmasa — RuntimeError: generator didn't yield, ikkinchi yield ga yetib borsa — generator didn't stop. Shartli yield yozmang:

python
@contextmanager
def yomon(kerak):
    if kerak:
        yield               # ❌ kerak=False bo'lsa — RuntimeError

3. except — bu yutish. Generator istisnoni ushlab, qayta ko'tarmasa, u yo'qoladi:

python
@contextmanager
def jurnal_bilan():
    try:
        yield
    except Exception as e:
        jurnal.error(e)     # ⚠️ raise yo'q — istisno yutildi

Istisnoni ko'rish, lekin o'tkazib yuborish kerak bo'lsa — raise yozing.

2.3. Istisnoni o'zgartirish

Generator ichida boshqa istisno ko'tarish mumkin — asl istisno __cause__ yoki __context__ ga tushadi (9.5-dars):

python
@contextmanager
def domen_xatolari():
    try:
        yield
    except sqlite3.IntegrityError as e:
        raise DublikatXato("bunday foydalanuvchi bor") from e

Bu qatlamlar orasida xato turlarini tarjima qilishning toza usuli.

2.4. Bir martalik va dekorator sifatida

@contextmanager qaytargan obyekt bir martalik: generator bir marta ishlaydi. __enter__ o'z argumentlarini o'chiradi (ular endi kerak emas), shuning uchun ikkinchi with tushunarsiz AttributeError beradi.

Lekin u dekorator sifatida bemalol qayta ishlatiladi:

python
@taymer("hisobot")
def hisobot_tuz():
    ...

hisobot_tuz()               # har chaqiruvda yangi generator
hisobot_tuz()               # ✅

Sababi — ContextDecorator bazaviy sinfi. Dekorator funksiyani quyidagicha o'raydi:

python
def __call__(self, funksiya):
    def orovchi(*args, **kwargs):
        with self._recreate_cm():            # ⭐ har chaqiruvda yangi menejer
            return funksiya(*args, **kwargs)
    return orovchi

_GeneratorContextManager._recreate_cm() o'sha argumentlar bilan yangi obyekt yaratadi. Oddiy ContextDecorator esa self ni qaytaradi — ya'ni sinf asosidagi menejer bitta nusxa bilan barcha chaqiruvlarda ishlaydi va qayta kiriladigan bo'lishi kerak (10.6-dars, 2.8).

Dekorator shaklida as yo'q — yield qiymati funksiyaga yetib bormaydi.

2.5. ExitStack — dinamik with

9.3-darsda ExitStack ni soni oldindan noma'lum fayllar uchun ishlatgan edik. Uning to'liq interfeysi:

Metod Nima qiladi Chiqishda chaqiriladi
enter_context(cm) cm.__enter__() ni chaqiradi, natijani qaytaradi cm.__exit__(...)
callback(f, *args, **kw) Hech narsa chaqirmaydi f(*args, **kw) — istisno ma'lumotisiz
push(exit_f) Hech narsa chaqirmaydi exit_f(tur, qiymat, tb) — yuta oladi
pop_all() Barcha yozuvlarni yangi ExitStack ga ko'chiradi Joriysi endi hech narsa qilmaydi
close() Hozirning o'zida hammasini chaqiradi —

Qoidalar:

  • Chiqish — teskari tartibda (LIFO)
  • Istisno ma'lumoti keyingi (tashqariroq) yozuvga uzatiladi; biror __exit__/push funksiyasi uni yutsa, qolganlar (None, None, None) oladi
  • Yozuvlardan biri xato bersa, keyingilari baribir chaqiriladi, xatolar __context__ zanjiriga bog'lanadi — xuddi ichma-ich with dagidek
  • push va callback dekorator sifatida ham ishlaydi: @stack.callback

2.6. pop_all — egalikni o'tkazish

Bir nechta resursni olish kerak, ulardan biri ochilmasa — ochilganlarini yopish kerak. Hammasi ochilsa — ularni boshqa obyektga topshirish kerak, yopmaslik:

python
def ulan_hammasi(manzillar):
    with ExitStack() as stack:
        ulanishlar = [stack.enter_context(Ulanish(m)) for m in manzillar]
        # shu qatorgacha xato bo'lsa — with ochilganlarini yopadi
        yopuvchi = stack.pop_all()
        # ⭐ endi stack bo'sh, with chiqishda hech narsa yopmaydi
    return ulanishlar, yopuvchi

Chaqiruvchi keyinroq with yopuvchi: yoki yopuvchi.close() bilan hammasini yopadi. Bu __enter__ ichida bir nechta resurs olishning eng ishonchli usuli (10.6-dars, 6-xato).

2.7. Kichik vositalar

nullcontext(qiymat=None) — hech narsa qilmaydigan menejer, __enter__ qiymat ni qaytaradi:

python
kontekst = qulf if parallel else nullcontext()
with kontekst:
    ...

suppress(*turlar) — tanlangan istisnolarni yutadi. 3.12 dan ExceptionGroup bilan ishlaydi: mos keladiganlarini guruhdan olib tashlaydi, qolganlarini ko'taradi.

closing(obj) — chiqishda obj.close(); __enter__ obj ni qaytaradi.

chdir(yol) (3.11+) — joriy papkani vaqtincha almashtiradi. Joriy papka butun jarayonga tegishli — iplar bilan xavfsiz emas.

redirect_stdout(f) / redirect_stderr(f) — sys.stdout ni vaqtincha almashtiradi. Qayta kiriladigan, lekin global holatni o'zgartiradi: kutubxona kodida va iplarda ishlatmang.

AbstractContextManager — __enter__ ni self qaytaradigan qilib beradi, __exit__ ni abstrakt qiladi. isinstance(obj, AbstractContextManager) ikkala metodi bor har qanday obyekt uchun True (merosxo'r bo'lmasa ham).

2.8. Asinxron juftlari

asynccontextmanager, AsyncExitStack, aclosing — xuddi shu g'oyalar async with uchun. Ular 14-qismda.


3. Tez ma'lumotnoma

@contextmanager shabloni

python
@contextmanager
def nom(...):
    resurs = ol()                 # __enter__
    try:
        yield resurs              # as NOM
    except Tur:                   # ixtiyoriy — yutish yoki tarjima
        raise BoshqaXato(...) from None
    finally:
        resurs.qoy()              # __exit__ — har doim

Xatolar

yield yo'q               → RuntimeError: generator didn't yield
ikkinchi yield           → RuntimeError: generator didn't stop
qayta with               → AttributeError: ... no attribute 'args'
finally yo'q + istisno   → tozalash bajarilmaydi (xabarsiz!)
except + raise yo'q      → istisno yutiladi

ExitStack

python
with ExitStack() as s:
    f = s.enter_context(open(yol))     # menejer
    s.callback(os.remove, vaqtincha)   # oddiy funksiya
    s.push(exit_funksiya)              # (tur, qiymat, tb) → bool
    egasi = s.pop_all()                # egalikni o'tkazish

Vositalar

nullcontext(x)        → as x, hech narsa qilmaydi
suppress(T, ...)      → T larni yutadi; ExceptionGroup ni bo'ladi
closing(obj)          → obj.close()
chdir(yol)            → os.chdir vaqtincha (3.11+)
redirect_stdout(f)    → sys.stdout vaqtincha
ContextDecorator      → menejer = dekorator
AbstractContextManager → __enter__ = self

4. Batafsil misollar

Misol 1 — O'z @contextmanager imizni yozib, asli bilan solishtirish

python
"""Soddalashtirilgan GenMenejer va contextlib.contextmanager bir xil holatlarda qanday ishlaydi."""

import contextlib


class GenMenejer:
    def __init__(self, funksiya, args, kwargs):
        self.gen = funksiya(*args, **kwargs)

    def __enter__(self):
        try:
            return next(self.gen)
        except StopIteration:
            raise RuntimeError("generator didn't yield") from None

    def __exit__(self, tur, qiymat, tb):
        if tur is None:
            try:
                next(self.gen)
            except StopIteration:
                return False
            raise RuntimeError("generator didn't stop")
        try:
            self.gen.throw(qiymat)
        except StopIteration as e:
            return e is not qiymat
        except RuntimeError as e:
            # PEP 479: blokdagi StopIteration generator ichida RuntimeError ga aylanadi
            if e is qiymat or (isinstance(qiymat, StopIteration) and e.__cause__ is qiymat):
                return False
            raise
        except BaseException as e:
            if e is qiymat:
                return False
            raise
        raise RuntimeError("generator didn't stop after throw()")


def menejer_qil(funksiya):
    def yaratuvchi(*args, **kwargs):
        return GenMenejer(funksiya, args, kwargs)
    return yaratuvchi


iz = []


def oddiy():
    iz.append("kirdi")
    try:
        yield "resurs"
    finally:
        iz.append("tozaladi")


def finallysiz():
    iz.append("kirdi")
    yield "resurs"
    iz.append("tozaladi")


def yutuvchi():
    try:
        yield
    except KeyError:
        iz.append("KeyError yutildi")


def tarjimon():
    try:
        yield
    except KeyError as e:
        raise LookupError(f"topilmadi: {e}") from e


def yieldsiz():
    return
    yield


def ikki_yield():
    yield 1
    yield 2


HOLATLAR = [
    ("oddiy, xatosiz", oddiy, None),
    ("oddiy, ValueError", oddiy, ValueError("v")),
    ("⚠️ finallysiz, ValueError", finallysiz, ValueError("v")),
    ("yutuvchi, KeyError", yutuvchi, KeyError("k")),
    ("yutuvchi, TypeError", yutuvchi, TypeError("t")),
    ("tarjimon, KeyError", tarjimon, KeyError("id")),
    ("yieldsiz", yieldsiz, None),
    ("ikki_yield, xatosiz", ikki_yield, None),
    ("oddiy, StopIteration", oddiy, StopIteration("s")),
]


def sina(dekorator, funksiya, istisno):
    iz.clear()
    menejer = dekorator(funksiya)
    try:
        with menejer() as q:
            iz.append(f"blok({q})")
            if istisno is not None:
                raise istisno
        natija = "jim tugadi"
    except BaseException as e:
        sabab = f" ← {type(e.__cause__).__name__}" if e.__cause__ else ""
        natija = f"{type(e).__name__}: {e}{sabab}"
    return f"{natija} | {' → '.join(iz) or '-'}"


print("=== Har bir holat: asl contextmanager va bizning GenMenejer ===")
mos = 0
for nom, funksiya, istisno in HOLATLAR:
    asli = sina(contextlib.contextmanager, funksiya, istisno)
    bizniki = sina(menejer_qil, funksiya, istisno)
    mos += asli == bizniki
    belgi = "=" if asli == bizniki else "≠"
    print(f"\n  {nom}")
    print(f"    asli:    {asli}")
    print(f"    bizniki: {bizniki}   {belgi}")
print(f"\n  mos kelgan holatlar: {mos}/{len(HOLATLAR)}")

print("\n=== ⚠️ Bir martalik: bir obyekt bilan ikki marta with ===")
cm = contextlib.contextmanager(oddiy)()
with cm:
    pass
try:
    with cm:
        pass
except AttributeError as e:
    print(f"  AttributeError: {e}")
print(f"  turi: {type(cm).__name__}, bazalari: {[b.__name__ for b in type(cm).__mro__[1:-1]]}")

Natijaning muhim qismi:

text
=== Har bir holat: asl contextmanager va bizning GenMenejer ===

  oddiy, xatosiz
    asli:    jim tugadi | kirdi → blok(resurs) → tozaladi
    bizniki: jim tugadi | kirdi → blok(resurs) → tozaladi   =

  oddiy, ValueError
    asli:    ValueError: v | kirdi → blok(resurs) → tozaladi
    bizniki: ValueError: v | kirdi → blok(resurs) → tozaladi   =

  ⚠️ finallysiz, ValueError
    asli:    ValueError: v | kirdi → blok(resurs)
    bizniki: ValueError: v | kirdi → blok(resurs)   =

  yutuvchi, KeyError
    asli:    jim tugadi | blok(None) → KeyError yutildi
    bizniki: jim tugadi | blok(None) → KeyError yutildi   =

  yutuvchi, TypeError
    asli:    TypeError: t | blok(None)
    bizniki: TypeError: t | blok(None)   =

  tarjimon, KeyError
    asli:    LookupError: topilmadi: 'id' ← KeyError | blok(None)
    bizniki: LookupError: topilmadi: 'id' ← KeyError | blok(None)   =

  yieldsiz
    asli:    RuntimeError: generator didn't yield | -
    bizniki: RuntimeError: generator didn't yield | -   =

  ikki_yield, xatosiz
    asli:    RuntimeError: generator didn't stop | blok(1)
    bizniki: RuntimeError: generator didn't stop | blok(1)   =

  oddiy, StopIteration
    asli:    StopIteration: s | kirdi → blok(resurs) → tozaladi
    bizniki: StopIteration: s | kirdi → blok(resurs) → tozaladi   =

  mos kelgan holatlar: 9/9

=== ⚠️ Bir martalik: bir obyekt bilan ikki marta with ===
  AttributeError: '_GeneratorContextManager' object has no attribute 'args'
  turi: _GeneratorContextManager, bazalari: ['_GeneratorContextManagerBase', 'AbstractContextManager', 'ABC', 'ContextDecorator']

Nima ko'rsatdi: 2.1, 2.2, 2.3, 2.4-bo'limlar.

Misol 2 — Menejer dekorator sifatida va kichik vositalar

python
"""ContextDecorator, @contextmanager dekorator sifatida, nullcontext, suppress va ExceptionGroup."""

import contextlib
import io
import threading
from collections import Counter

statistika = Counter()


@contextlib.contextmanager
def kuzat(nom):
    statistika[f"{nom}:boshlandi"] += 1
    try:
        yield
    except Exception:
        statistika[f"{nom}:xato"] += 1
        raise
    else:
        statistika[f"{nom}:muvaffaqiyat"] += 1


print("=== 1. Bitta menejer — with va dekorator shaklida ===")
with kuzat("import"):
    pass


@kuzat("hisobot")
def hisobot_tuz(oy):
    if oy > 12:
        raise ValueError(f"oy noto'g'ri: {oy}")
    return f"{oy}-oy hisoboti"


for oy in (3, 7, 13):
    try:
        print(f"  {hisobot_tuz(oy)}")
    except ValueError as e:
        print(f"  ❌ {e}")
print(f"  {dict(sorted(statistika.items()))}")
print("  ⭐ dekorator har chaqiruvda yangi generator yaratdi — 3 marta ishladi")

print("\n=== 2. Sinf asosida ContextDecorator — bitta nusxa, holat stekda ===")


class Chuqurlik(contextlib.ContextDecorator):
    def __init__(self):
        self.stek = []

    def __enter__(self):
        self.stek.append(len(self.stek) + 1)
        return self

    def __exit__(self, *istisno):
        self.stek.pop()
        return False


chuqurlik = Chuqurlik()


@chuqurlik
def faktorial(n):
    if n <= 1:
        print(f"  eng chuqur nuqtada stek: {chuqurlik.stek}")
        return 1
    return n * faktorial(n - 1)


print(f"  5! = {faktorial(5)}, keyin stek: {chuqurlik.stek}")
print(f"  _recreate_cm() is self: {chuqurlik._recreate_cm() is chuqurlik}")

print("\n=== 3. nullcontext — shartli menejer ===")


def yoz(qatorlar, parallel):
    qulf = threading.Lock()
    kontekst = qulf if parallel else contextlib.nullcontext()
    bufer = io.StringIO()
    for q in qatorlar:
        with kontekst:
            bufer.write(q)
    return f"{type(kontekst).__name__}: {bufer.getvalue()!r}"


print(f"  {yoz(['a', 'b'], parallel=True)}")
print(f"  {yoz(['a', 'b'], parallel=False)}")
with contextlib.nullcontext({"sozlama": 1}) as s:
    print(f"  nullcontext(qiymat) as s → {s}")

print("\n=== 4. suppress: oddiy va ExceptionGroup bilan ===")
kesh = {}
with contextlib.suppress(KeyError):
    del kesh["yo'q kalit"]
    print("  bu qator bajarilmaydi")
print("  KeyError yutildi, blokning qolgani o'tkazib yuborildi")


def tekshir(yozuv):
    xatolar = []
    if not yozuv.get("ism"):
        xatolar.append(ValueError("ism bo'sh"))
    yosh = yozuv.get("yosh")
    if not isinstance(yosh, int):
        xatolar.append(TypeError("yosh butun son emas"))
    elif yosh < 0:
        xatolar.append(ValueError("yosh manfiy"))
    if xatolar:
        raise ExceptionGroup("yozuv yaroqsiz", xatolar)


for yozuv in ({"ism": "", "yosh": -5}, {"ism": "", "yosh": "o'ttiz"}):
    try:
        with contextlib.suppress(ValueError):
            tekshir(yozuv)
        print(f"  {yozuv}: hamma xato ValueError edi — jim o'tdi")
    except ExceptionGroup as eg:
        print(f"  {yozuv}: qoldi → {eg.exceptions}")

print("\n=== 5. AbstractContextManager ===")


class Oddiy(contextlib.AbstractContextManager):
    def __exit__(self, *istisno):
        return None


o = Oddiy()
with o as x:
    print(f"  __enter__ meros bo'yicha self qaytardi: {x is o}")


class Begona:
    def __enter__(self):
        return self

    def __exit__(self, *a):
        return None


print(f"  merossiz sinf isinstance: {isinstance(Begona(), contextlib.AbstractContextManager)}")
print(f"  fayl isinstance: {isinstance(io.StringIO(), contextlib.AbstractContextManager)}")
try:
    class Chala(contextlib.AbstractContextManager):
        pass
    Chala()
except TypeError as e:
    print(f"  __exit__ siz: TypeError: {e}")

Natijaning muhim qismi:

text
=== 1. Bitta menejer — with va dekorator shaklida ===
  3-oy hisoboti
  7-oy hisoboti
  ❌ oy noto'g'ri: 13
  {'hisobot:boshlandi': 3, 'hisobot:muvaffaqiyat': 2, 'hisobot:xato': 1, 'import:boshlandi': 1, 'import:muvaffaqiyat': 1}
  ⭐ dekorator har chaqiruvda yangi generator yaratdi — 3 marta ishladi

=== 2. Sinf asosida ContextDecorator — bitta nusxa, holat stekda ===
  eng chuqur nuqtada stek: [1, 2, 3, 4, 5]
  5! = 120, keyin stek: []
  _recreate_cm() is self: True

=== 3. nullcontext — shartli menejer ===
  lock: 'ab'
  nullcontext: 'ab'
  nullcontext(qiymat) as s → {'sozlama': 1}

=== 4. suppress: oddiy va ExceptionGroup bilan ===
  KeyError yutildi, blokning qolgani o'tkazib yuborildi
  {'ism': '', 'yosh': -5}: hamma xato ValueError edi — jim o'tdi
  {'ism': '', 'yosh': "o'ttiz"}: qoldi → (TypeError('yosh butun son emas'),)

=== 5. AbstractContextManager ===
  __enter__ meros bo'yicha self qaytardi: True
  merossiz sinf isinstance: True
  fayl isinstance: True
  __exit__ siz: TypeError: Can't instantiate abstract class Chala without an implementation for abstract method '__exit__'

Nima ko'rsatdi: 2.4, 2.7-bo'limlar.

Misol 3 — ExitStack chuqur

python
"""enter_context, callback, push, istisno uzatish, xatolar zanjiri va pop_all."""

from contextlib import ExitStack


class Resurs:
    ochiq = set()

    def __init__(self, nom, buziq=False):
        self.nom, self.buziq = nom, buziq

    def __enter__(self):
        if self.buziq:
            raise ConnectionError(f"{self.nom} ochilmadi")
        Resurs.ochiq.add(self.nom)
        print(f"    ochildi {self.nom}")
        return self

    def __exit__(self, tur, qiymat, tb):
        Resurs.ochiq.discard(self.nom)
        print(f"    yopildi {self.nom} (tur={tur.__name__ if tur else None})")
        return False


print("=== 1. Uch xil yozuv va LIFO tartib ===")
with ExitStack() as s:
    s.enter_context(Resurs("A"))
    s.callback(print, "    callback: vaqtincha fayl o'chirildi")

    @s.push
    def kuzatuvchi(tur, qiymat, tb):
        print(f"    push: istisno ma'lumoti = {tur.__name__ if tur else None}")
        return False

    s.enter_context(Resurs("B"))
    print("    --- blok tugadi ---")

print("\n=== 2. Istisno uzatilishi va yutish ===")
try:
    with ExitStack() as s:
        s.enter_context(Resurs("tashqi"))

        @s.push
        def yutuvchi(tur, qiymat, tb):
            print(f"    yutuvchi oldi: {tur.__name__ if tur else None}")
            return tur is not None and issubclass(tur, KeyError)

        s.enter_context(Resurs("ichki"))
        raise KeyError("kalit")
    print("  ⭐ KeyError yutildi: 'ichki' uni ko'rdi, 'tashqi' esa tur=None oldi")
except KeyError:
    print("  yutilmadi")

print("\n=== 3. Yozuvlar o'zi xato bersa — hammasi chaqiriladi, zanjir saqlanadi ===")


def yopuvchi(nom, xato=None):
    def f(*istisno):
        print(f"    {nom} chaqirildi")
        if xato:
            raise xato
    return f


try:
    with ExitStack() as s:
        s.push(yopuvchi("1-yozuv"))
        s.push(yopuvchi("2-yozuv", OSError("disk")))
        s.push(yopuvchi("3-yozuv", TimeoutError("tarmoq")))
        raise ValueError("asl xato")
except Exception as e:
    zanjir = []
    while e is not None:
        zanjir.append(repr(e))
        e = e.__context__
    print("  zanjir: " + " ← ".join(zanjir))

print("\n=== 4. ⭐ pop_all: hammasi ochilsa — egalikni topshirish ===")


def ulan_hammasi(nomlar, buziq=None):
    with ExitStack() as s:
        resurslar = [s.enter_context(Resurs(n, buziq=(n == buziq))) for n in nomlar]
        egasi = s.pop_all()
    return resurslar, egasi


resurslar, egasi = ulan_hammasi(["db", "kesh", "navbat"])
print(f"  funksiyadan qaytgach ochiq: {sorted(Resurs.ochiq)}")
print("  ... ilova ishlayapti ...")
egasi.close()
print(f"  egasi.close() dan keyin ochiq: {sorted(Resurs.ochiq)}")

print("\n=== 5. pop_all: biri ochilmasa — ochilganlari yopiladi ===")
try:
    ulan_hammasi(["db", "kesh", "navbat"], buziq="navbat")
except ConnectionError as e:
    print(f"  ❌ {e}; ochiq qolgan: {sorted(Resurs.ochiq)}")

print("\n=== 6. Menejer bo'lmagan obyekt ===")
try:
    with ExitStack() as s:
        s.enter_context(open)
except TypeError as e:
    print(f"  TypeError: {e}")

Natijaning muhim qismi:

text
=== 1. Uch xil yozuv va LIFO tartib ===
    ochildi A
    ochildi B
    --- blok tugadi ---
    yopildi B (tur=None)
    push: istisno ma'lumoti = None
    callback: vaqtincha fayl o'chirildi
    yopildi A (tur=None)

=== 2. Istisno uzatilishi va yutish ===
    ochildi tashqi
    ochildi ichki
    yopildi ichki (tur=KeyError)
    yutuvchi oldi: KeyError
    yopildi tashqi (tur=None)
  ⭐ KeyError yutildi: 'ichki' uni ko'rdi, 'tashqi' esa tur=None oldi

=== 3. Yozuvlar o'zi xato bersa — hammasi chaqiriladi, zanjir saqlanadi ===
    3-yozuv chaqirildi
    2-yozuv chaqirildi
    1-yozuv chaqirildi
  zanjir: OSError('disk') ← TimeoutError('tarmoq') ← ValueError('asl xato')

=== 4. ⭐ pop_all: hammasi ochilsa — egalikni topshirish ===
    ochildi db
    ochildi kesh
    ochildi navbat
  funksiyadan qaytgach ochiq: ['db', 'kesh', 'navbat']
  ... ilova ishlayapti ...
    yopildi navbat (tur=None)
    yopildi kesh (tur=None)
    yopildi db (tur=None)
  egasi.close() dan keyin ochiq: []

=== 5. pop_all: biri ochilmasa — ochilganlari yopiladi ===
    ochildi db
    ochildi kesh
    yopildi kesh (tur=ConnectionError)
    yopildi db (tur=ConnectionError)
  ❌ navbat ochilmadi; ochiq qolgan: []

=== 6. Menejer bo'lmagan obyekt ===
  TypeError: 'builtins.builtin_function_or_method' object does not support the context manager protocol

Nima ko'rsatdi: 2.5, 2.6-bo'limlar.

Misol 4 — Amaliy: CLI dasturi uchun sinov muhiti

Kichik CLI dasturini testlaymiz. U muhit o'zgaruvchisini o'qiydi, joriy papkaga fayl yozadi, ekranga chop etadi va modul sozlamasiga qaraydi. Har test toza muhitda ishlashi va keyin hamma narsa tiklanishi kerak — istalgan kombinatsiyada.

python
"""ExitStack + @contextmanager + chdir + redirect_stdout: qayta tiklanadigan sinov muhiti."""

import contextlib
import io
import os
import tempfile
from pathlib import Path
from types import SimpleNamespace

SOZLAMA = SimpleNamespace(batafsil=False, format="txt")


def dastur(argv):
    """Sinovdan o'tkaziladigan kichik CLI: hisobot yozadi."""
    til = os.environ.get("HISOBOT_TIL", "uz")
    if not argv:
        raise SystemExit(2)
    sarlavha = {"uz": "Hisobot", "en": "Report"}[til]
    fayl = Path(f"{argv[0]}.{SOZLAMA.format}")
    fayl.write_text(f"{sarlavha}: {', '.join(argv[1:])}\n", encoding="utf-8")
    print(f"{sarlavha} yozildi: {fayl}")
    if SOZLAMA.batafsil:
        print(f"  {len(argv) - 1} ta band")
    return 0


@contextlib.contextmanager
def muhit(**qiymatlar):
    eski = {k: os.environ.get(k) for k in qiymatlar}
    os.environ.update(qiymatlar)
    try:
        yield
    finally:
        for k, v in eski.items():
            if v is None:
                os.environ.pop(k, None)
            else:
                os.environ[k] = v


@contextlib.contextmanager
def sinov_muhiti(*, env=None, sozlama=None, stdout=True):
    """Kerakli qismlarni dinamik yig'adi; har biri o'z tozalashini ExitStack ga qo'shadi."""
    with contextlib.ExitStack() as s:
        papka = Path(s.enter_context(tempfile.TemporaryDirectory()))
        s.enter_context(contextlib.chdir(papka))
        if env:
            s.enter_context(muhit(**env))
        for nom, qiymat in (sozlama or {}).items():
            s.callback(setattr, SOZLAMA, nom, getattr(SOZLAMA, nom))
            setattr(SOZLAMA, nom, qiymat)
        chiqish = io.StringIO() if stdout else None
        s.enter_context(contextlib.redirect_stdout(chiqish) if stdout else contextlib.nullcontext())
        yield SimpleNamespace(papka=papka, chiqish=chiqish)


def holat():
    return (Path.cwd(), os.environ.get("HISOBOT_TIL"), SOZLAMA.batafsil, SOZLAMA.format)


asl = holat()
natijalar = []

print("=== 1. Oddiy test ===")
with sinov_muhiti() as m:
    kod = dastur(["yanvar", "savdo", "xarajat"])
    fayllar = sorted(p.name for p in m.papka.iterdir())
    matn = (m.papka / "yanvar.txt").read_text(encoding="utf-8")
print(f"  kod={kod}, fayllar={fayllar}")
print(f"  fayl: {matn!r}")
print(f"  stdout ushlandi: {m.chiqish.getvalue()!r}")
natijalar.append(holat() == asl)

print("\n=== 2. Muhit va sozlama o'zgartirilgan test ===")
with sinov_muhiti(env={"HISOBOT_TIL": "en"}, sozlama={"batafsil": True, "format": "md"}) as m:
    dastur(["fevral", "a", "b", "c"])
    ichki_holat = (os.environ["HISOBOT_TIL"], SOZLAMA.batafsil, SOZLAMA.format)
    fayllar = sorted(p.name for p in m.papka.iterdir())
print(f"  ichkarida: HISOBOT_TIL, batafsil, format = {ichki_holat}")
print(f"  fayllar={fayllar}")
print(f"  stdout: {m.chiqish.getvalue().splitlines()}")
natijalar.append(holat() == asl)

print("\n=== 3. Dastur xato bilan tugaydi — muhit baribir tiklanadi ===")
try:
    with sinov_muhiti(env={"HISOBOT_TIL": "ru"}, sozlama={"format": "csv"}) as m:
        dastur(["mart"])
except KeyError as e:
    print(f"  ❌ KeyError: {e} — 'ru' tili qo'llab-quvvatlanmaydi")
print(f"  vaqtincha papka o'chirildimi: {not m.papka.exists()}")
natijalar.append(holat() == asl)

print("\n=== 4. SystemExit ham BaseException — tozalash baribir ===")
try:
    with sinov_muhiti(sozlama={"batafsil": True}):
        dastur([])
except SystemExit as e:
    print(f"  SystemExit({e.code})")
natijalar.append(holat() == asl)

print("\n=== 5. stdout ushlanmasin — nullcontext ===")
with sinov_muhiti(stdout=False) as m:
    dastur(["aprel", "x"])
print(f"  m.chiqish = {m.chiqish}")
natijalar.append(holat() == asl)

print(f"\n  har testdan keyin holat tiklandimi: {natijalar}")

Natijaning muhim qismi:

text
=== 1. Oddiy test ===
  kod=0, fayllar=['yanvar.txt']
  fayl: 'Hisobot: savdo, xarajat\n'
  stdout ushlandi: 'Hisobot yozildi: yanvar.txt\n'

=== 2. Muhit va sozlama o'zgartirilgan test ===
  ichkarida: HISOBOT_TIL, batafsil, format = ('en', True, 'md')
  fayllar=['fevral.md']
  stdout: ['Report yozildi: fevral.md', '  3 ta band']

=== 3. Dastur xato bilan tugaydi — muhit baribir tiklanadi ===
  ❌ KeyError: 'ru' — 'ru' tili qo'llab-quvvatlanmaydi
  vaqtincha papka o'chirildimi: True

=== 4. SystemExit ham BaseException — tozalash baribir ===
  SystemExit(2)

=== 5. stdout ushlanmasin — nullcontext ===
Hisobot yozildi: aprel.txt
  m.chiqish = None

  har testdan keyin holat tiklandimi: [True, True, True, True, True]

Nima ko'rsatdi: 2.2, 2.5, 2.7-bo'limlar — bir necha mustaqil tozalash bitta ExitStack da, shartli qismlar bilan.


5. To'g'ri va noto'g'ri tushunishlar

Noto'g'ri fikr To'g'risi
"yield dan keyingi kod har doim bajariladi" Faqat blok xatosiz tugasa; aks holda finally kerak
"Generator ichidagi except istisnoni faqat ko'radi" raise qilinmasa — istisno yutiladi
"@contextmanager obyektini qayta ishlatsa bo'ladi" Bir martalik; dekorator shaklida esa har chaqiruvda yangisi yaratiladi
"ContextDecorator ham har chaqiruvda yangi nusxa yaratadi" Oddiy sinf uchun self ni qaytaradi — bitta nusxa
"ExitStack.callback istisnoni ko'radi" Ko'rmaydi; buning uchun push
"ExitStack dagi bitta yozuv xato bersa, qolganlari qoladi" Hammasi chaqiriladi, xatolar zanjirga bog'lanadi
"suppress ExceptionGroup ni to'liq yutadi" Faqat mos keladigan qismini; qolgani ko'tariladi
"isinstance(x, AbstractContextManager) meros talab qiladi" Ikki metodi bor har qanday obyekt uchun True

6. Keng tarqalgan xatolar va yechimlari

1. yield atrofida finally yo'q

python
@contextmanager
def qulf_ol(q):
    q.acquire()
    yield               # ❌ blokda xato — qulf abadiy band
    q.release()

@contextmanager
def qulf_ol(q):
    q.acquire()
    try:
        yield
    finally:
        q.release()     # ✅

2. Resurs olish try ichida

python
@contextmanager
def fayl(yol):
    try:
        f = open(yol)   # ❌ ochilmasa, finally da f yo'q → UnboundLocalError asl xatoni yashiradi
        yield f
    finally:
        f.close()

@contextmanager
def fayl(yol):
    f = open(yol)       # ✅ try dan oldin
    try:
        yield f
    finally:
        f.close()

3. Kuzatish uchun except yozib, raise ni unutish

python
@contextmanager
def jurnal():
    try:
        yield
    except Exception as e:
        log.error(e)    # ❌ xato yo'qoldi

    except Exception as e:
        log.error(e)
        raise           # ✅

4. Shartli yield

python
@contextmanager
def kesh(yoqilgan):
    if yoqilgan:
        yield Kesh()    # ❌ yoqilgan=False → generator didn't yield

@contextmanager
def kesh(yoqilgan):
    yield Kesh() if yoqilgan else None     # ✅ har doim bitta yield

5. @contextmanager obyektini saqlab qayta ishlatish

python
TRANZAKSIYA = tranzaksiya(db)       # ❌ modul darajasida bitta obyekt
with TRANZAKSIYA: ...
with TRANZAKSIYA: ...               # AttributeError

def ish():
    with tranzaksiya(db): ...       # ✅ har safar yangi

6. ExitStack da callback bilan istisnoga javob berishga urinish

python
s.callback(rollback)                # ❌ har doim chaqiriladi, xatosiz bo'lsa ham

@s.push
def faqat_xatoda(tur, qiymat, tb):  # ✅
    if tur is not None:
        rollback()

7. pop_all siz resurslarni funksiyadan qaytarish

python
def ochish(yollar):
    with ExitStack() as s:
        return [s.enter_context(open(y)) for y in yollar]   # ❌ qaytishda hammasi yopiladi

def ochish(yollar):
    with ExitStack() as s:
        fayllar = [s.enter_context(open(y)) for y in yollar]
        return fayllar, s.pop_all()                         # ✅

8. redirect_stdout ni kutubxona yoki iplarda ishlatish

python
def hisobla():
    with redirect_stdout(io.StringIO()):     # ❌ boshqa iplarning print() lari ham yo'qoladi
        shovqinli_funksiya()

Funksiyaga file= parametri bering yoki logging ishlating; redirect_stdout — skriptlar va testlar uchun.


7. Integratsiya — bu bilim qayerda kerak bo'ladi

  • 9.3-dars (o'tilgan): contextlib vositalaridan foydalanish
  • 10.3-dars (o'tilgan): throw(), close(), PEP 479 — @contextmanager ning poydevori
  • 10.6-dars (o'tilgan): __enter__/__exit__ protokoli, qayta kiriladigan menejerlar
  • 10.8–10.11-darslar: dekoratorlar — ContextDecorator qanday funksiyani o'raydi
  • 14-qism: asynccontextmanager, AsyncExitStack, aclosing
  • 17-qism: pytest fixture'lari yield bilan — xuddi shu g'oya, ExitStack uslubidagi tozalash
  • 20-qism: FastAPI lifespan va yield li bog'liqliklar

8. Eng yaxshi amaliyotlar

  1. @contextmanager da yield doim try/finally ichida. Istisnosiz tozalash — istisno bo'lganda tozalanmaslik degani.

  2. Resursni try dan oldin oling. Olinmagan resursni yopishga urinmang.

  3. Generator ichida except bo'lsa — raise yoki ongli yutish. Ikkinchisini izoh bilan belgilang.

  4. Holatsiz, oddiy menejer — @contextmanager; holatli, qayta ishlatiladigan yoki tez ishlashi kerak — sinf.

  5. Soni yoki tarkibi ish vaqtida aniqlanadigan resurslar uchun ExitStack. Ichma-ich with va shartli if lar o'rniga.

  6. Resurslarni topshirish kerak bo'lsa — pop_all().

  7. callback — tozalash, push — istisnoga javob.

  8. Global holatni o'zgartiruvchi vositalar (chdir, redirect_stdout) — skript va testlarda. Kutubxona va ko'p ipli kodda emas.


9. Amaliy topshiriq

Vazifa 1: Natijani bashorat qiling

python
from contextlib import contextmanager, ExitStack, suppress, nullcontext

@contextmanager
def g(yut=False):
    print("a", end=" ")
    try:
        yield "q"
    except KeyError:
        print("k", end=" ")
        if not yut:
            raise
    finally:
        print("f", end=" ")
1.  with g() as x:
        print(x, end=" ")
    print()
2.  with g(yut=True):
        raise KeyError
    print("davom")
3.  try:
        with g():
            raise KeyError
    except KeyError:
        print("tashqi")
4.  try:
        with g():
            raise ValueError
    except ValueError:
        print("VE")
5.  @contextmanager
    def h():
        yield
        yield
    try:
        with h():
            pass
    except RuntimeError as e:
        print(e)
6.  @contextmanager
    def h():
        return
        yield
    try:
        with h():
            pass
    except RuntimeError as e:
        print(e)
7.  with ExitStack() as s:
        s.callback(print, 1, end=" ")
        s.callback(print, 2, end=" ")
        print(0, end=" ")
    print()
8.  with nullcontext(5) as n:
        print(n + 1)
9.  with suppress(LookupError):
        print("a", end=" ")
        [][1]
        print("b", end=" ")
    print("c")
10. try:
        with suppress(ValueError):
            raise ExceptionGroup("g", [ValueError(1), KeyError(2)])
    except* KeyError as eg:
        print(eg.exceptions)
11. @g()
    def ish():
        print("ish", end=" ")
    ish()
    ish()
    print()
12. m = g()
    with m:
        pass
    try:
        with m:
            pass
    except AttributeError:
        print("bir martalik")
Javoblar
  1. a q f — yield qiymati as ga tushdi
  2. a k f davom — raise qilinmadi, istisno yutildi
  3. a k f tashqi — except ushladi, lekin qayta ko'tardi
  4. a f VE — except KeyError mos kelmadi, finally bajarildi
  5. generator didn't stop — ikkinchi yield ga yetib bordi
  6. generator didn't yield
  7. 0 2 1 — callback lar teskari tartibda
  8. 6 — nullcontext argumentini qaytaradi
  9. a c — [][1] dagi IndexError (LookupError avlodi) yutildi, b bajarilmadi
  10. (KeyError(2),) — suppress guruhdan ValueError ni olib tashladi
  11. a ish f a ish f — dekorator har chaqiruvda yangi generator yaratadi
  12. a f bir martalik — ikkinchi with AttributeError berdi

Vazifa 2: Xatolarni tuzating

python
1.  @contextmanager
    def vaqtincha_fayl(yol):
        try:
            f = open(yol, "w")
            yield f
        finally:
            f.close()
            os.remove(yol)

2.  @contextmanager
    def tranzaksiya(db):
        db.begin()
        try:
            yield db
        except Exception:
            db.rollback()
        else:
            db.commit()

3.  @contextmanager
    def ixtiyoriy_qulf(qulf):
        if qulf is not None:
            with qulf:
                yield
        
4.  def fayllarni_och(yollar):
        with ExitStack() as s:
            return [s.enter_context(open(y)) for y in yollar]

5.  with ExitStack() as s:
        s.callback(db.rollback)
        ish(db)
        db.commit()
Javoblar
python
1.  @contextmanager
    def vaqtincha_fayl(yol):
        f = open(yol, "w")              # try dan oldin — ochilmasa, f yo'q
        try:
            yield f
        finally:
            f.close()
            os.remove(yol)

2.      except Exception:
            db.rollback()
            raise                       # aks holda xato yutiladi

3.  @contextmanager
    def ixtiyoriy_qulf(qulf):
        with qulf if qulf is not None else nullcontext():
            yield                       # har doim aniq bitta yield

4.  def fayllarni_och(yollar):
        with ExitStack() as s:
            fayllar = [s.enter_context(open(y)) for y in yollar]
            return fayllar, s.pop_all()     # egalik chaqiruvchiga

5.  with ExitStack() as s:
        @s.push
        def xatoda_rollback(tur, qiymat, tb):
            if tur is not None:
                db.rollback()           # callback commit dan keyin ham rollback qilardi
        ish(db)
        db.commit()

Vazifa 3: @contextmanager ni to'liq yozing

Misol 1 dagi GenMenejer ni kengaytiring:

  1. ContextDecorator dan meros oling va _recreate_cm ni yozing — dekorator sifatida ishlasin
  2. Qayta with da tushunarli xabar: RuntimeError("bu menejer bir martalik; har safar funksiyani qayta chaqiring")
  3. functools.update_wrapper bilan yaratuvchi funksiyaga asl nom va hujjatni ko'chiring
  4. Misol 1 dagi 9 holat va dekorator shakli uchun testlar yozing — natijalar asli bilan bir xil bo'lsin

Vazifa 4: ExitStack siz ExitStack

MeningStekim sinfini yozing: enter_context, callback, push, pop_all, close.

  1. LIFO tartib
  2. Yutilgan istisno keyingi yozuvlarga (None, None, None) bo'lib borsin
  3. Yozuv xato bersa, qolganlari baribir chaqirilsin va xatolar __context__ zanjirida bo'lsin
  4. Misol 3 dagi 6 bo'limni MeningStekim bilan ishga tushirib, natijani solishtiring

Vazifa 5: Resurslar hovuzi

ulanishlar_hovuzi(manzillar, kamida) funksiyasini yozing:

  1. Har bir manzilga ulanishga urinadi; ulanmaganlarini o'tkazib yuboradi
  2. Kamida kamida ta ulanish bo'lmasa — ochilganlarini yopib, ExceptionGroup bilan barcha ulanish xatolarini ko'taradi
  3. Yetarli bo'lsa — pop_all bilan Hovuz obyektiga topshiradi, u context manager bo'lsin
  4. Hovuz.ol() — navbatdagi ulanishni qaytaradi (with hovuz.ol() as u:), ishlatib bo'lgach hovuzga qaytaradi

Vazifa 6: Pytest uslubidagi fixture'lar

Kichik test yuritgich yozing:

  1. @fixture — yield li generator funksiyani ro'yxatga oladi
  2. Test funksiyasi parametr nomlari bo'yicha fixture'larni oladi; fixture boshqa fixture'ga bog'liq bo'lishi mumkin
  3. Hammasi bitta ExitStack da ochiladi va test tugagach teskari tartibda yopiladi
  4. Fixture o'rnatilishida xato bo'lsa — test "ERROR", testning o'zida — "FAIL" deb belgilansin; ikkala holatda ham ochilgan fixture'lar yopilsin

Vazifa 7: O'ylash

Context manager'ni sinf bilan ham, @contextmanager bilan ham yozish mumkin. Ular faqat sintaksis bilan farq qiladimi? Qachon qaysi birini tanlaysiz — ishlash tezligi, holat, qayta ishlatish, xatolarni tuzatish va o'qilishi nuqtai nazaridan.

Javob

Qisqa javob: yo'q, farq faqat sintaksisda emas. @contextmanager — generator ustidagi adapter: u bir martalik, sinfdan sezilarli sekinroq, istisnoni throw() orqali uzatadi va holatni generator freymida yashiradi. Evaziga "oldin — yield — keyin" ketma-ketligini bitta funksiyada o'qiladigan qilib yozadi. Oddiy, holatsiz, kam chaqiriladigan menejerlar uchun — @contextmanager; holatli, qayta ishlatiladigan, tez-tez chaqiriladigan yoki ommaviy API qismi bo'lgan menejerlar uchun — sinf.

1. O'qilishi

python
@contextmanager
def vaqtincha_papka():
    yol = mkdtemp()
    try:
        yield Path(yol)
    finally:
        rmtree(yol)
python
class VaqtinchaPapka:
    def __enter__(self):
        self.yol = mkdtemp()
        return Path(self.yol)
    def __exit__(self, tur, qiymat, tb):
        rmtree(self.yol)
        return False

Generator variantida olish va qo'yib yuborish yonma-yon, oradagi lokal o'zgaruvchi (yol) avtomatik saqlanadi. Sinfda esa u self ga ko'chadi va ikki metod orasida bo'linadi. Qisqa menejerlar uchun generator ancha tabiiy o'qiladi.

2. Tezlik

Python 3.14 da bo'sh with blokini o'lchasak (chaqiruv narxi bilan birga, bu kompyuterda):

Usul Taxminiy vaqt
try/finally ~0.04 µs
Sinf ~0.2 µs
nullcontext() ~0.3 µs
@contextmanager ~1.4 µs
ExitStack + callback ~2.5 µs

@contextmanager sinfdan taxminan 6–7 marta sekinroq: har with da generator obyekti, adapter obyekti yaratiladi, next() ikki marta chaqiriladi. Bitta fayl ochish yoki tranzaksiya uchun bu farq ahamiyatsiz — resursning o'zi millisekundlar oladi. Lekin sikl ichida million marta chaqiriladigan qulf yoki o'lchov menejeri uchun farq seziladi. Shuning uchun threading.Lock, decimal.localcontext kabi standart menejerlar sinf (yoki C) da yozilgan.

3. Holat va qayta ishlatish

  • @contextmanager obyekti bir martalik — generator tugagach qayta ishga tushmaydi
  • U qayta kiriladigan bo'la olmaydi: har with uchun alohida generator kerak
  • Holat generator freymida yashirin — tashqaridan ko'rib bo'lmaydi (masalan, Taymer natijasi uchun yield qilingan obyektga yozish kerak bo'ladi)

Sinf esa:

  • qayta ishlatiladigan bo'lishi mumkin (Lock)
  • qayta kiriladigan bo'lishi mumkin (holat stekda, 10.6-dars)
  • holatini atributlar orqali ochiq ko'rsatadi (t.soniya, tx.jurnal)
  • boshqa metodlarga ega bo'lishi mumkin (hovuz.ol(), tx.tasdiqdan_keyin(...))

4. Istisnolar va xatolarni tuzatish

Sinfda __exit__ istisnoni argument sifatida oladi va qaror qabul qiladi — hech narsa ko'tarilmaydi. Generatorda istisno yield qatorida haqiqatan ko'tariladi:

  • traceback'da qo'shimcha contextlib freymlari paydo bo'ladi
  • except yozish tabiiy, lekin raise ni unutish — jim yutish
  • finally ni unutish — jim tozalanmaslik
  • xato xabarlari (generator didn't stop, no attribute 'args') kodga emas, adapterning ichki holatiga ishora qiladi

Sinfda __exit__ qaytish qiymati aniq ko'rinadi, istisnolarni tanlab boshqarish mantig'i issubclass bilan oshkora yoziladi.

5. Qachon qaysi biri

Vaziyat Tanlov
Bir martalik, holatsiz "oldin/keyin" @contextmanager
Xato turlarini tarjima qilish @contextmanager (try/except/raise from)
Test yordamchilari, skriptlar @contextmanager
Tez-tez chaqiriladigan (siklda) Sinf
Qayta ishlatiladigan yoki qayta kiriladigan Sinf
Blokdan keyin natija o'qiladi yoki qo'shimcha metodlar kerak Sinf
Ommaviy kutubxona API si Sinf (aniq xatolar, hujjatlashtirish, tur annotatsiyasi)
Soni ish vaqtida aniqlanadigan resurslar ExitStack — ikkalasini birlashtiradi

6. Aralash yondashuv

Ko'pincha eng yaxshisi — ikkalasi birga: resursning o'zi sinf (Ulanish, Hovuz), undan foydalanish naqshlari esa @contextmanager (tranzaksiya(ulanish), domen_xatolari()). Pytest ham shunday: fixture'lar yield li generatorlar, ular ichida esa sinf asosidagi resurslar ishlatiladi.

7. Xulosa

  1. @contextmanager — generatorni protokolga moslovchi adapter, yangi imkoniyat emas
  2. U o'qilishi oson, lekin bir martalik, sekinroq va xatolarni throw() orqali uzatadi
  3. Sinf holat, qayta ishlatish, qayta kirish va tezlik kerak bo'lganda ustun
  4. yield atrofidagi try/finally va except dagi raise — generator variantining eng ko'p unutiladigan qismlari
  5. Amalda ikkalasi birga ishlatiladi: resurs — sinf, foydalanish naqshi — generator

Nimani mustahkamlaydi: 2.1–2.7-bo'limlar.


Xulosa

Bu darsda contextlib vositalarining ichki ishlashini o'rgandik.

Eng muhim uch fikr:

  1. @contextmanager generatorni protokolga moslaydi. yield gacha — __enter__, yield qiymati — as, blokdagi istisno yield qatorida throw() bilan ko'tariladi. Shuning uchun tozalash albatta finally da, except bo'lsa — raise bilan bo'lishi kerak; aks holda tozalash jim o'tkazib yuboriladi yoki xato jim yutiladi. Obyekt bir martalik, lekin dekorator sifatida har chaqiruvda yangisi yaratiladi.

  2. ExitStack — dasturlash yo'li bilan qurilgan with. enter_context menejerlar uchun, callback oddiy tozalash uchun, push istisnoga javob uchun. Chiqish teskari tartibda, yozuvlardan biri xato bersa ham qolganlari chaqiriladi va xatolar zanjirga bog'lanadi. pop_all() bir nechta resursni xavfsiz olib, egalikni boshqa obyektga topshirish imkonini beradi.

  3. Kichik vositalarning chegarasini biling. nullcontext shartli menejer uchun, suppress ExceptionGroup dan faqat mos qismini olib tashlaydi, AbstractContextManager tekshiruvi meros talab qilmaydi. chdir va redirect_stdout butun jarayonning global holatini o'zgartiradi — ular test va skriptlar uchun, kutubxona va ko'p ipli kod uchun emas.

Keyingi darsda dekoratorlarni boshlaymiz: funksiya — obyekt, uni o'rab, xatti-harakatini o'zgartiradigan funksiya qanday yoziladi va @ sintaksisi aslida nima qiladi.

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
10.7-dars: contextlib — IlmHamroh