Məzmuna keç
Educora
İrəli18 dəq17 / 42

Kontekst menecerləri və with operatoru

`with` əslində nə edir: `__enter__` və `__exit__`, `__exit__`-də istisnaların emalı, `contextlib.contextmanager` ilə menecer yazmaq və hazır alətlər — `suppress`, `redirect_stdout`, `ExitStack`.

Özünü yoxla
Bu dərsdə öyrənəcəksən
  • with operatorunun __enter__ və __exit__ metodlarını necə çağırdığını izah etmək
  • İstisnaları düzgün emal edən kontekst meneceri sinfi yazmaq
  • @contextmanager ilə generator əsasında menecer yaratmaq
  • suppress, redirect_stdout, ExitStack kimi hazır menecerlərdən istifadə etmək

Proqram fayl, verilənlər bazası ilə əlaqə və ya şəbəkə soketi açır, sonra yarı yolda nəsə səhv gedir — istisna baş verir və ya funksiya vaxtından əvvəl return edir. Resurs azad edilməsə, fayllar kilidli qalır, əlaqələr yığılır, verilənlər isə diskə heç çatmaya bilər. with open(...) yazılışı ilə fayllar dərsində tanış olmusan; indi onun içində nə baş verdiyini görək və əvvəlcə hazırlanıb sonra mütləq təmizlənməli olan istənilən resurs üçün öz kontekst menecerlərimizi yazaq.

with əslində nə edir

Kontekst meneceri iki metodu olan obyektdir. with manager as x: yazılışı manager.__enter__() metodunu çağırır və onun qaytardığı qiyməti x-ə bağlayır. Sonra blok icra olunur. Bundan sonra nə baş versə — blok normal bitsə, return, break və ya istisna olsa — Python manager.__exit__(exc_type, exc, tb) metodunu çağırır. Fayl obyekti öz-özünün kontekst meneceridir və onun __exit__-i faylı bağlayır:

Python
with open('notes.txt', 'w', encoding='utf-8') as f:
    f.write('first line\n')
    print('inside:', f.closed)
print('after:', f.closed)

try:
    with open('notes.txt', encoding='utf-8') as f:
        text = f.read()
        raise ValueError('something went wrong')
except ValueError as e:
    print('error:', e)
print('closed even after the error:', f.closed)
▸ Gözlənilən nəticə
inside: False
after: True
error: something went wrong
closed even after the error: True

Python with operatorunu təxminən aşağıdakı koda çevirir. Diqqət et: __exit__ istisnanı alır və onun yayılmağa davam edib-etməyəcəyinə özü qərar verir:

Python
manager = open('notes.txt', encoding='utf-8')
f = manager.__enter__()
try:
    text = f.read()
except BaseException as e:
    if not manager.__exit__(type(e), e, e.__traceback__):
        raise
else:
    manager.__exit__(None, None, None)
Sadələşdirilmiş variant: həqiqi mexanizm bir neçə nadir halı da nəzərə alır.

Bu, əks halda hər yerdə əl ilə yazmalı olduğun try/finally şablonunun eynisidir. Kontekst meneceri hazırlıq və təmizləmə məntiqini təkrar istifadə olunan bir obyektə yığır: resursdan istifadə edən kod qısa qalır və təmizləməni «unuda» bilmir.

Öz kontekst meneceri sinfin

__exit__-in üç arqumenti istisnanı təsvir edir: onun tipi, istisna obyekti və çağırışlar izi (traceback). Blok normal bitibsə, üçü də None-dur. Qaytarılan qiymət vacibdir: doğru qiymət istisnanı susdurur, False və ya None isə onun yayılmasına imkan verir. Aşağıdakı menecer blokun içində nəsə alınmasa, lüğəti əvvəlki vəziyyətinə qaytarır — kiçik verilənlər bazası tranzaksiyası kimi:

Python
class Transaction:
    def __init__(self, account):
        self.account = account

    def __enter__(self):
        self.backup = dict(self.account)
        return self.account

    def __exit__(self, exc_type, exc, tb):
        if exc_type is not None:
            self.account.clear()
            self.account.update(self.backup)
        return False

wallet = {'Aysel': 100, 'Murad': 50}
for amount in (30, 500):
    try:
        with Transaction(wallet) as w:
            w['Aysel'] -= amount
            w['Murad'] += amount
            if w['Aysel'] < 0:
                raise ValueError('not enough money')
    except ValueError as e:
        print('cancelled:', e)
    print(wallet)
▸ Gözlənilən nəticə
{'Aysel': 70, 'Murad': 80}
cancelled: not enough money
{'Aysel': 70, 'Murad': 80}

Birinci köçürmə (30) keçdi. İkincisi yarı yolda qırıldı: xəta aşkarlananda 500 artıq Ayseldən çıxılıb Murada əlavə edilmişdi, amma __exit__ ehtiyat nüsxəni bərpa etdi. Çağıran kod istisnanı yenə də görür, çünki __exit__ False qaytardı.

contextlib.contextmanager: generatordan menecer

Hər menecer üçün sinif yazmaq yorucudur. **@contextmanager** dekoratoru generator funksiyasını kontekst menecerinə çevirir: yield-dən əvvəlki kod __enter__ rolunu oynayır, yield-in verdiyi qiymət as-a gedir, yield-dən sonrakı kod isə __exit__ rolunu oynayır. Blokda istisna baş verərsə, o, generatorun içində məhz yield sətrində yenidən qaldırılır — ona görə təmizləmə kodu finally blokunda olmalıdır:

Python
from contextlib import contextmanager

@contextmanager
def tag(name):
    print(f'<{name}>')
    try:
        yield name
    finally:
        print(f'</{name}>')

with tag('ul'):
    for item in ['tea', 'plov']:
        with tag('li') as t:
            print(f'  {item} (inside {t})')
▸ Gözlənilən nəticə
<ul>
<li>
  tea (inside li)
</li>
<li>
  plov (inside li)
</li>
</ul>

Çıxış ardıcıllığı göstərir: hər li iç-içə mötərizələr kimi ul-un içində açılıb bağlanır. Menecerlərdən daxil olma sırasının əksinə çıxılır — sonuncu daxil olan birinci çıxır.

Nümunə: kodun icra müddətini ölçən menecer

with blokunun içindəki kodun neçə millisaniyə çəkdiyini çap edən timer(label) kontekst menecerini yaz. Blokda istisna baş versə belə, vaxt çap olunmalıdır.

Həllini göstər
yield-dən əvvəl başlanğıc vaxtı yadda saxla: start = time.perf_counter().
perf_counter() intervalları ölçmək üçün nəzərdə tutulmuş dəqiq saatdır.
yield-i try-ın içinə qoy, time.perf_counter() - start fərqini isə finally-də hesabla — belə olanda vaxt xətadan sonra da çap olunacaq.
Bloka qiymət lazım deyil, ona görə sadəcə yield yazmaq kifayətdir, as isə işlədilmir.
Python
import time
from contextlib import contextmanager

@contextmanager
def timer(label):
    start = time.perf_counter()
    try:
        yield
    finally:
        elapsed = time.perf_counter() - start
        print(f'{label}: {elapsed * 1000:.1f} ms')

with timer('sum of squares'):
    total = sum(n * n for n in range(1_000_000))
print(total)
Millisaniyələrin sayı kompüterdən asılıdır, ona görə hər dəfə bir az fərqli olacaq.

Standart kitabxanadakı faydalı menecerlər

contextlib modulunda bir neçə hazır menecer var. suppress(Error) göstərilən istisnaları nəzərə almır, redirect_stdout(buffer) isə print-in çıxışını müvəqqəti olaraq başqa axına yönəldir:

Python
from contextlib import suppress, redirect_stdout
import io
import os

with suppress(FileNotFoundError):
    os.remove('no-such-file.txt')
print('no crash')

buffer = io.StringIO()
with redirect_stdout(buffer):
    print('this goes into the buffer')
print('captured:', buffer.getvalue().strip())
▸ Gözlənilən nəticə
no crash
captured: this goes into the buffer

Resursların sayı yalnız proqram işləyərkən məlum olursa, **ExitStack** işlət: o, istənilən sayda meneceri toplayır və blokun sonunda hamısından tərs ardıcıllıqla çıxır — hətta üçüncü faylı açmaq alınmasa belə:

Python
from contextlib import ExitStack

names = ['a.txt', 'b.txt', 'c.txt']
for i, name in enumerate(names):
    with open(name, 'w', encoding='utf-8') as f:
        f.write(f'file {i}\n')

with ExitStack() as stack:
    files = [stack.enter_context(open(n, encoding='utf-8')) for n in names]
    print([f.readline().strip() for f in files])
print(all(f.closed for f in files))
▸ Gözlənilən nəticə
['file 0', 'file 1', 'file 2']
True

Bir çox başqa obyekt də with ilə işləyir. Nəyinsə müvəqqəti qurulduğunu görəndə kontekst meneceri axtar:

ObyektBlokdan çıxanda nə baş verir
open(...)fayl bağlanır
threading.Lock()kilid azad edilir
decimal.localcontext()əvvəlki dəqiqlik bərpa olunur
tempfile.TemporaryDirectory()qovluq içindəkilərlə birlikdə silinir
unittest.mock.patch(...)orijinal obyekt yerinə qaytarılır
sqlite3.connect(...)tranzaksiya təsdiqlənir və ya geri qaytarılır (əlaqə bağlanmır!)
Python
from decimal import Decimal, localcontext

print(Decimal(1) / Decimal(7))
with localcontext() as ctx:
    ctx.prec = 5
    print(Decimal(1) / Decimal(7))
print(Decimal(1) / Decimal(7))
▸ Gözlənilən nəticə
0.1428571428571428571428571429
0.14286
0.1428571428571428571428571429
Blokun içində dəqiqlik 5 rəqəmdir, blokdan çıxanda isə susmaya görə olan 28 rəqəm özü bərpa olunur.
Tapşırıq

section(title) kontekst menecerini tamamla: daxil olanda == title ==, çıxanda isə -- end of title -- çap etsin. Çıxış sətri blokda istisna baş verəndə də çap olunmalı, istisna isə çağıran koda çatmalıdır.

Tapşırıq · Python
from contextlib import contextmanager

@contextmanager
def section(title):
    # print '== title ==' on entry and '-- end of title --' on exit, even after an error
    yield

with section('Report'):
    print('all good')

try:
    with section('Import'):
        raise ValueError('bad file')
except ValueError as e:
    print('error:', e)
▸ Gözlənilən nəticə
== Report ==
all good
-- end of Report --
== Import ==
-- end of Import --
error: bad file
Tapşırıq

Ignore(*exceptions) sinfinin __exit__ metodunu düzəlt: blokdakı istisna göstərilən tiplərdən biridirsə, onu susdursun, əks halda (və istisna olmayanda) False qaytarsın.

Tapşırıq · Python
class Ignore:
    def __init__(self, *exceptions):
        self.exceptions = exceptions

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc, tb):
        # return True only if the exception is one of self.exceptions
        return False

with Ignore(ZeroDivisionError):
    print(1 / 0)
print('still running')

with Ignore(KeyError, IndexError):
    [][5]
print('done')
▸ Gözlənilən nəticə
still running
done

Əsas fikirlər

  • with m as x əvvəlcə m.__enter__() çağırır (nəticə x-ə gedir), sonda isə m.__exit__() mütləq çağırılır.
  • __exit__(exc_type, exc, tb) istisna haqqında məlumat alır; doğru qiymət qaytarmaq istisnanı susdurur.
  • @contextmanager-də yield-dən əvvəlki kod daxil olma, sonrakı kod çıxışdır; təmizləmə finally-də olmalıdır.
  • Bir neçə menecerdən tərs ardıcıllıqla çıxılır; dinamik sayda menecer üçün ExitStack var.
  • suppress, redirect_stdout, localcontext, TemporaryDirectory — hazır kontekst menecerləridir.

Özünü yoxla

10 sual. Hər düzgün cavab XP qazandırır.

1 / 10
__exit__ metodu nə vaxt çağırılır?