Context manageri și preocupările transversale în Python

  • 3 minute de citit
  • 4 exemple de cod

Disponibil și în English

Pe această pagină

Salut,

În acest articol scurt vreau să vorbesc despre context manageri. Personal consider că, în esență, sunt doar o formă de decoratori. Dacă nu știi ce este un decorator, consultă articolul de pe Wikipedia despre Decorator Pattern.

Decoratorii pot fi folosiți pentru a implementa preocupări transversale (cross-cutting concerns). Avem componenta A și avem nevoie de logging și securitate. Am putea scrie logica pentru logging și securitate direct în componenta A, dar unii consideră că o componentă trebuie să fie doar componenta A, nu componentaCareȘtieDespreSecuritateȘiAlteLucruri. Deoarece nu este responsabilitatea componentei să autorizeze cereri sau să trimită apeluri către un serviciu extern de logging, putem înfășura componenta A într-un decorator care face exact asta.

Definiția formală a preocupărilor transversale, preluată de pe Wikipedia, este următoarea:

În dezvoltarea software orientată pe aspecte, preocupările transversale sunt aspecte ale unui program care afectează alte preocupări. Aceste preocupări nu pot, de multe ori, să fie separate curat de restul sistemului, nici în proiectare, nici în implementare, și pot duce fie la împrăștiere (duplicarea codului), fie la încâlcire (dependențe semnificative între sisteme), sau la ambele.

Iată câteva exemple de preocupări transversale:

Deoarece context managerii sunt, într-un fel, asemănători decoratorilor, îi poți folosi pentru a implementa preocupări transversale. Să explorăm.

Exemplu simplu

În Python există două tipuri de context manageri: unul sub formă de funcție și unul sub formă de clasă. Pentru ca o funcție să se comporte ca un context manager, trebuie decorată cu decoratorul @contextmanager, iar pentru ca o clasă să se comporte ca un context manager, trebuie să implementeze __enter__ și __exit__.

Context managerii pot fi apelați folosind instrucțiunea with. Următorul fragment de cod demonstrează doi context manageri:

  • Unul care înregistrează când funcția este apelată și când se termină.
  • Unul care interceptează argumentele funcției.
from contextlib import contextmanager@contextmanagerdef simple_context_manager(function):    try:        print("calling function")        yield function    finally:        print("function call has ended")class SimpleContextManager:    def __init__(self, cb):        self.cb = cb    def _intercept(self, *args, **kwargs):        print(f"calling with {args} {kwargs}")        return print(*args, **kwargs)    def __enter__(self):        print("intercept start")        return self._intercept    def __exit__(self, exc_type, exc_val, exc_tb):        print("intercept end")def main():    with simple_context_manager(print) as print_func:        print_func("hi")    with SimpleContextManager(print) as print_func:        print_func("hi")        print_func("hi", end="\n\n", sep=",")        print_func("hi")if __name__ == '__main__':    main()

Caching

Ce este caching-ul? Pe scurt..

Caching-ul este folosit pentru a stoca rezultatul unui calcul costisitor fie în memorie, fie pe un dispozitiv de stocare persistent, pentru a optimiza programul.

Avem funcția compute_fibonacci, care este destul de lentă. O versiune care folosește cache a fost implementată în clasa CachedComputeFibonacci. Observă că la primul apel din instrucțiunea print(cached_compute_fibonacci(35)) codul durează ceva până afișează rezultatul, dar al doilea print este instantaneu.

def compute_fibonacci(number):    if number <= 1:        return number    return compute_fibonacci(number-1) + compute_fibonacci(number-2)class CachedComputeFibonacci:    def __init__(self):        self._cache = {}    def __call__(self, *args, **kwargs):        number = args[0]        if number in self._cache:            return self._cache[number]        result = compute_fibonacci(number)        self._cache[number] = result        return result    def __enter__(self):        return self    def __exit__(self, exc_type, exc_val, exc_tb):        passdef main():    # Fără cache    print(compute_fibonacci(10))    # Cu cache    with CachedComputeFibonacci() as cached_compute_fibonacci:        print(cached_compute_fibonacci(35))        print(cached_compute_fibonacci(35))if __name__ == '__main__':    main()

Logging

Logging-ul poate fi util în scopuri de depanare și audit.

def compute_fibonacci(number):    if number <= 1:        return number    return compute_fibonacci(number-1) + compute_fibonacci(number-2)class LoggedComputeFibonacci:    def __init__(self):        pass    def __call__(self, *args, **kwargs):        print(f"calling compute_fibonacci with args={args} kwargs={kwargs}")        result = compute_fibonacci(args[0])        print(f"compute_fibonacci={result}")        return result    def __enter__(self):        return self    def __exit__(self, exc_type, exc_val, exc_tb):        passdef main():    # Logging    with LoggedComputeFibonacci() as cached_compute_fibonacci:        print(cached_compute_fibonacci(35))        print(cached_compute_fibonacci(36))if __name__ == '__main__':    main()

Detectarea și corectarea erorilor

Dacă te trezești că duplici aceeași logică try/catch în mai multe locuri din cod, poate o poți extrage într-un context manager pentru gestionarea erorilor:

from contextlib import contextmanager@contextmanagerdef my_error_handler():    try:        yield    except ZeroDivisionError:        print("abort abort")def main():    # gestionarea erorilor    with my_error_handler():        print("0 / 0 =", 0 / 0)if __name__ == '__main__':    main()

Codul este, în opinia mea, mult mai curat așa.

Mulțumesc că ai citit și sper că ai învățat ceva!