withoperatorunun__enter__və__exit__metodlarını necə çağırdığını izah etmək- İstisnaları düzgün emal edən kontekst meneceri sinfi yazmaq
@contextmanagerilə generator əsasında menecer yaratmaqsuppress,redirect_stdout,ExitStackkimi 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:
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:
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)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:
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:
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.
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ərHəllini gizlət
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.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)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:
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ə:
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:
| Obyekt | Blokdan çı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!) |
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
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.
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
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.
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
ExitStackvar. 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.
__exit__ metodu nə vaxt çağırılır?