Pythonov modul contextlib
- ki ima, kot bi bilo bistremu
človeku jasno že iz imena, nekaj opraviti s konteksti - ima funkcijo
chdir
, s katero lahko začasno zamenjamo trenutni delovni
direktorij.
import os
import contextlib
print(os.getcwd())
with contextlib.chdir("/Users/janez/Downloads"):
print(os.getcwd())
print(os.getcwd())
/Users/janez/Desktop/predavanja/p1/drobnarije
/Users/janez/Downloads
/Users/janez/Desktop/predavanja/p1/drobnarije
To je uporabno. Če tega ne bi bilo, bi se morali narediti sami. (Eni
smo si že: contextlib.chdir
obstaja šele od Pythona
3.11.)
Da naredimo upravljalec konteksta (context manager) moramo poznati bodisi razrede bodisi generatorje in dekoratorje. Razrede nekateri že poznate, o generatorjih govorimo v ločenem zapisku. V teh zapiskih pokažimo oba načina. Prvi je poučnejši, drugi prikladnejši.
Upravljalec konteksta je razred, ki ima metodi __enter__
in __exit__
. with
pokliče pred vstopom v blok
in drugo po izhodu. Prva ima le en argument, self
(ekvivalent this
-a v nekaterih drugih jezikih; v Pythonu ga
je potrebno eksplicitno navesti med argumenti), druga pa ima tri
argumente, ki povedo, ali je bila znotraj bloka sprožena izjema
(exception) ter kakšna in kje.
class NotInVen:
def __enter__(self):
print("vstopamo")
def __exit__(self, exc_type, exc_val, exc_tb):
print("izstopamo")
print("začetek")
with NotInVen():
print("v bloku")
print("konec")
začetek
vstopamo
v bloku
izstopamo
konec
To je to. To je že (skoraj) vsa znanost. Ostane le še, da lahko
__enter__
vrne kako vrednost; v with
jo z
as
priredimo imenu. Takole.
class NotInVen:
def __enter__(self):
print("vstopamo")
return 42
def __exit__(self, exc_type, exc_val, exc_tb):
print("izstopamo")
print("začetek")
with NotInVen() as x:
print("v bloku")
print("x =", x)
print("konec")
začetek
vstopamo
v bloku
x = 42
izstopamo
konec
Upravljalec konteksta, ki zamenja trenutni delovni direktorij znotraj bloka, je trivialna zadeva. Če znamo napisati razred v Pythonu, seve.
import os
class MojChDir:
def __init__(self, dir):
self.dir = dir
self.saved = None
def __enter__(self):
self.saved = os.getcwd()
self.dir)
os.chdir(
def __exit__(self, exc_type, exc_val, exc_tb):
self.saved) os.chdir(
print(os.getcwd())
with MojChDir("/Users/janez/Downloads"):
print(os.getcwd())
print(os.getcwd())
/Users/janez/Desktop/predavanja/p1/drobnarije
/Users/janez/Downloads
/Users/janez/Desktop/predavanja/p1/drobnarije
Za še en primer napišimo kontekst, ki pove, koliko časa se izvaja blok.
import time
class Timer:
def __enter__(self):
self.start = time.time()
def __exit__(self, exc_type, exc_val, exc_tb):
print("Elapsed time: ", time.time() - self.start)
Pa pomerimo, če Python prav spi.
with Timer():
1.5) time.sleep(
Elapsed time: 1.505854845046997
Kar v redu.
class SupressPrint:
def __enter__(self):
global print
self.old_print = print
def print(*args, **kwargs):
pass
def __exit__(self, *_):
global print
print = self.old_print
print("Pišem")
with SupressPrint():
print("Ne pišem.")
print("Spet pišem.")
Pišem
Spet pišem.
To ne bo delovalo vedno; če ta razred uvozite iz modula, ne bo imel
učinka, ker global
ne deluje čisto tako, kot si (najbrž)
predstavljate, da deluje.
Kaj pa tole?
class TimePrint:
def __enter__(self):
global print
self.old_print = print
def print(*args, **kwargs):
self.old_print(f"{time.time()}:", *args, **kwargs)
def __exit__(self, *_):
global print
print = self.old_print
import time
import random
with TimePrint():
print("Začetek")
= 1 + random.random()
x
time.sleep(x)print("in čez", round(x, 2), "sekund")
print("Zdaj sem pa spet normalen.")
1734281983.302903: Začetek
1734281984.607671: in čez 1.3 sekund
Zdaj sem pa spet normalen.
Čisto preprosto. Datoteke imajo poleg vseh read
-ov in
readline
-ov in write
-ov še metodi
__enter__
in __exit__
, ki delata približno
(ali celo točno) tole:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
Elegantno, ni?
Da nam ne bi bilo potrebno pisati razredov, lahko kontekste pišemo
tudi s funkcijami. Točneje, generatorskimi funkcijami, ki morajo
vsebovati natančno en yield
. Kar je pred
yield
-om se zgodi pred vstopom v blok, kar po njem, ob
izhodu iz bloka. Vrednost, ki jo generiramo, pa je rezultat metode
__enter__
. Tako zapisan kontekst je potrebno
dekorirati s contextlib.contextmanager
, tako da
pred definicijo funkcije dodamo vrstico
@contextlib.contextmanager
.
Napišimo, recimo, začasno spremembo direktorija.
import contextlib
@contextlib.contextmanager
def moj_chdir(dir):
= os.getcwd()
old_dir dir)
os.chdir(yield
os.chdir(old_dir)
print(os.getcwd())
with moj_chdir("/Users/janez/Downloads"):
print(os.getcwd())
print(os.getcwd())
/Users/janez/Desktop/predavanja/p1/drobnarije
/Users/janez/Downloads
/Users/janez/Desktop/predavanja/p1/drobnarije
Tako je še veliko preprosteje, ni?
Če delamo tako reč čisto zares, je prav, da poskrbimo še za izjeme (exception). Detajle preberite v dokumentaciji.
Pythonovi konteksti vedno uporabljajo razrede.
contextlib.contextmanager
je samo funkcija, ki sama sestavi
razred namesto nas. Ljubitelji črne magije naj raziščejo, zakaj tale
funkcija dela isto kot contextlib.contextmanager
.
def moj_contextmanager(f):
class cm:
def __init__(self, *args, **kwargs):
self.g = f(*args, **kwargs)
def __enter__(self):
return next(self.g)
def __exit__(self, *_):
try:
next(self.g)
except StopIteration:
pass
return cm
import contextlib
@moj_contextmanager
def moj_chdir(dir):
= os.getcwd()
old_dir dir)
os.chdir(yield
os.chdir(old_dir)
print(os.getcwd())
with moj_chdir("/Users/janez/Downloads"):
print(os.getcwd())
print(os.getcwd())
/Users/janez/Desktop/predavanja/p1/drobnarije
/Users/janez/Downloads
/Users/janez/Desktop/predavanja/p1/drobnarije