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()
os.chdir(self.dir)
def __exit__(self, exc_type, exc_val, exc_tb):
os.chdir(self.saved)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():
time.sleep(1.5)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_printprint("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_printimport time
import random
with TimePrint():
print("Začetek")
x = 1 + random.random()
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):
old_dir = os.getcwd()
os.chdir(dir)
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 cmimport contextlib
@moj_contextmanager
def moj_chdir(dir):
old_dir = os.getcwd()
os.chdir(dir)
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