withdeyiminin__enter__ve__exit__metotlarını nasıl çağırdığını açıklamak- İstisnaları doğru ele alan bir bağlam yöneticisi sınıfı yazmak
@contextmanagerile üreteç tabanlı bir yönetici oluşturmaksuppress,redirect_stdoutveExitStackgibi hazır yöneticileri kullanmak
Bir program bir dosya, veritabanı bağlantısı ya da ağ soketi açar ve yolun yarısında bir şeyler ters gider: bir istisna oluşur ya da fonksiyon erkenden return eder. Kaynak serbest bırakılmazsa dosyalar kilitli kalır, bağlantılar birikir ve veriler diske hiç ulaşmayabilir. with open(...) yazımıyla dosyalar dersinde tanıştın; şimdi içinde ne olduğunu görelim ve önce hazırlanıp sonra mutlaka temizlenmesi gereken her kaynak için kendi bağlam yöneticilerimizi (context manager) yazalım.
with aslında ne yapar
Bağlam yöneticisi, iki metodu olan bir nesnedir. with manager as x: yazımı manager.__enter__() metodunu çağırır ve döndürdüğü değeri x'e bağlar. Sonra blok çalışır. Ardından ne olursa olsun (blok normal biterse, bir return, break ya da istisna olursa) Python manager.__exit__(exc_type, exc, tb) metodunu çağırır. Dosya nesnesi kendi bağlam yöneticisidir ve __exit__ metodu dosyayı kapatı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)▸ Beklenen çıktı
inside: False after: True error: something went wrong closed even after the error: True
Python bir with deyimini kabaca aşağıdaki koda dönüştürür. Dikkat: __exit__ istisnayı alır ve yayılmaya devam edip etmeyeceğine kendisi karar 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, aksi hâlde her yerde elle yazman gereken try/finally kalıbının aynısıdır. Bağlam yöneticisi hazırlık ve temizlik mantığını yeniden kullanılabilir tek bir nesnede toplar; kaynağı kullanan kod kısa kalır ve temizliği “unutamaz”.
Kendi bağlam yöneticisi sınıfın
__exit__'in üç argümanı istisnayı tanımlar: türü, istisna nesnesi ve çağrı izi (traceback). Blok normal bittiyse üçü de None olur. Dönüş değeri önemlidir: doğru (true) bir değer istisnayı bastırır, False ya da None ise yayılmasına izin verir. Aşağıdaki yönetici, blok içinde bir şey ters giderse sözlüğü eski hâline getirir; tıpkı küçük bir veritabanı işlemi (transaction) gibi:
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)▸ Beklenen çıktı
{'Aysel': 70, 'Murad': 80}
cancelled: not enough money
{'Aysel': 70, 'Murad': 80}İlk transfer (30) gerçekleşti. İkincisi yarıda kesildi: hata fark edildiğinde 500 zaten Aysel'den düşülüp Murad'a eklenmişti, ama __exit__ yedeği geri yükledi. Çağıran kod istisnayı yine de görür, çünkü __exit__ False döndürdü.
contextlib.contextmanager: üreteçten yönetici
Her yönetici için sınıf yazmak yorucudur. **@contextmanager** dekoratörü bir üreteç fonksiyonunu bağlam yöneticisine dönüştürür: yield'den önceki kod __enter__ rolünü üstlenir, yield'in verdiği değer as'e gider, yield'den sonraki kod ise __exit__ rolünü üstlenir. Blokta bir istisna oluşursa üretecin içinde tam yield satırında yeniden fırlatılır; bu yüzden temizlik kodu finally içinde 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})')▸ Beklenen çıktı
<ul> <li> tea (inside li) </li> <li> plov (inside li) </li> </ul>
Çıktı sırayı gösteriyor: her li, iç içe parantezler gibi ul'un içinde açılıp kapanır. Yöneticilerden giriş sırasının tersiyle çıkılır: son giren ilk çıkar.
with bloğunun içindeki kodun kaç milisaniye sürdüğünü yazdıran bir timer(label) bağlam yöneticisi yaz. Blokta istisna oluşsa bile süre yazdırılmalıdır.
Çözümü gösterÇözümü gizle
yield'den önce başlangıç zamanını kaydet: start = time.perf_counter().perf_counter(), aralık ölçmek için tasarlanmış hassas bir saattir.yield'i try içine koy ve time.perf_counter() - start farkını finally içinde hesapla; böylece süre bir hatadan sonra da yazdırılır.Bloğun bir değere ihtiyacı yok; bu yüzden yalnızca
yield yazmak yeterlidir ve as kullanılmaz.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 kütüphanedeki yararlı yöneticiler
contextlib modülünde birkaç hazır yönetici bulunur. suppress(Error) belirtilen istisnaları yok sayar, redirect_stdout(buffer) ise print çıktısını geçici olarak başka bir akışa yönlendirir:
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())▸ Beklenen çıktı
no crash captured: this goes into the buffer
Kaynakların sayısı ancak program çalışırken belli oluyorsa **ExitStack** kullan: istediğin sayıda yöneticiyi toplar ve bloğun sonunda hepsinden ters sırayla çıkar; üçüncü dosya açılamasa bile:
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))▸ Beklenen çıktı
['file 0', 'file 1', 'file 2'] True
Başka birçok nesne de with ile çalışır. Bir şeyin geçici olarak kurulduğunu gördüğünde bir bağlam yöneticisi ara:
| Nesne | Çıkışta ne olur |
|---|---|
open(...) | dosya kapatılır |
threading.Lock() | kilit serbest bırakılır |
decimal.localcontext() | önceki duyarlık geri yüklenir |
tempfile.TemporaryDirectory() | klasör içeriğiyle birlikte silinir |
unittest.mock.patch(...) | asıl nesne yerine konur |
sqlite3.connect(...) | işlem onaylanır ya da geri alınır (bağlantı kapatılmaz!) |
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))▸ Beklenen çıktı
0.1428571428571428571428571429 0.14286 0.1428571428571428571428571429
section(title) bağlam yöneticisini tamamla: girişte == title ==, çıkışta -- end of title -- yazdırsın. Çıkış satırı blokta bir istisna oluştuğunda da yazdırılmalı, istisna ise çağıran koda ulaşmalı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)▸ Beklenen çıktı
== Report == all good -- end of Report -- == Import == -- end of Import -- error: bad file
Ignore(*exceptions) sınıfının __exit__ metodunu düzelt: bloktaki istisna verilen tiplerden biriyse onu bastırsın; aksi hâlde (ve istisna yoksa) False döndürsü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')▸ Beklenen çıktı
still running done
Önemli noktalar
with m as x,m.__enter__()'ı çağırır (sonuçx'e gider) ve sonundam.__exit__()her zaman çağrılır.__exit__(exc_type, exc, tb)istisnanın bilgilerini alır; doğru bir değer döndürmek istisnayı bastırır.@contextmanager'dayield'den önceki kod giriş, sonraki kod çıkıştır; temizlikfinallyiçinde olmalıdır.- Birden fazla yöneticiden ters sırayla çıkılır; değişken sayıda yönetici için
ExitStackvardır. suppress,redirect_stdout,localcontextveTemporaryDirectoryhazır bağlam yöneticileridir.
Kendini test et
10 soru. Her doğru cevap XP kazandırır.
__exit__ metodu ne zaman çağrılır?