Vai al contenuto principale

Scrivere context manager personalizzati in Python

Impara gli aspetti avanzati della gestione delle risorse in Python padroneggiando la scrittura di context manager personalizzati.

Scopri con l'IA

Motivazione

Potresti già conoscere l'istruzione with, un modo conciso per gestire le risorse in Python. Ma ti sei mai chiesto come funzioni dietro le quinte? Ebbene, la potenza della keyword with deriva dai context manager.

I context manager sono un pattern fondamentale in Python che offre un approccio strutturato alla gestione delle risorse. Garantisce che le risorse vengano acquisite, usate correttamente e infine rilasciate o ripulite, anche in presenza di errori o eccezioni.

Questo diventa particolarmente importante quando si lavora con risorse come file, connessioni di rete o handle di database. Usando i context manager, possiamo scrivere codice più pulito e affidabile, liberandoci dalla preoccupazione di dimenticare di chiudere un file o rilasciare un lock.

In questo tutorial, andremo oltre i context manager predefiniti di Python e impareremo a scrivere i nostri.

Capire i context manager in Python

Dietro le quinte, i context manager sono oggetti che definiscono due metodi speciali: __enter__ e __exit__. Il metodo __enter__ viene chiamato quando entri nel blocco with, e il suo valore di ritorno viene assegnato a una variabile all'interno di quel blocco. Il metodo __exit__, invece, viene chiamato quando si esce dal blocco with, indipendentemente dal fatto che termini normalmente o con un'eccezione.

Questa struttura garantisce una corretta gestione delle risorse. Vediamo alcuni context manager integrati in Python per capirlo meglio:

1. Gestione dei file

Prendiamo il classico esempio dell'apertura di un file:

with open('file.txt', 'r') as file:
   data = file.read()

Qui, open('file.txt', 'r') funge da context manager. Quando entri nel blocco with, il metodo enter dell'oggetto file viene chiamato, aprendo il file e assegnandolo alla variabile file. Puoi quindi usare file.read() per accedere al contenuto del file.

Successivamente, il metodo __exit__ dell'oggetto file è garantito alla chiusura del blocco with, anche se si verifica un'eccezione. Questo metodo si occupa di chiudere il file, assicurandoti di non lasciare handle di file aperti in giro.

2. Lock dei thread

Oltre ai file, i context manager possono essere usati anche per la sincronizzazione dei thread con threading.Lock():

import threading

lock = threading.Lock()
with lock:
   # Critical section
   print("This code is executed under lock protection.")

Qui, lock è un oggetto threading.Lock, un altro context manager. Quando entri nel blocco with, il metodo __enter__ acquisisce il lock, assicurando che solo un thread alla volta possa eseguire la sezione critica. Infine, il metodo __exit__ rilascia il lock all'uscita dal blocco with, permettendo agli altri thread di continuare.

3. Connessioni al database

Allo stesso modo, i context manager possono gestire le connessioni ai database:

import sqlite3

with sqlite3.connect('database.db') as connection:
    cursor = connection.cursor()
    cursor.execute("SELECT * FROM table")
    rows = cursor.fetchall()

La chiamata sqlite3.connect('database.db') è un context manager. Entrando nel blocco with viene stabilita una connessione al database, assegnata a connection. Puoi quindi usare un cursor per interagire con il database. Il metodo __exit__ garantisce la chiusura della connessione all'uscita dal blocco with, prevenendo leak di risorse.

4. Socket di rete

I context manager possono gestire anche la comunicazione di rete:

import socket

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect(('localhost', 8080))
    s.sendall(b'Hello, world')
    data = s.recv(1024)

Qui, socket.socket(socket.AF_INET, socket.SOCK_STREAM) crea un oggetto socket che funge da context manager. Il metodo __enter__ crea il socket e, all'interno del blocco with, puoi connetterti, inviare e ricevere dati. Il metodo __exit__ assicura che il socket venga chiuso correttamente al termine.

5. Scansione di directory

Questa volta, os.scandir('.') fornisce un modo per iterare sulle voci di una directory:

with os.scandir('.') as entries:
   for entry in entries:
       print(entry.name)

os.scandir('.') funge da context manager qui. Il metodo __enter__ apre una scansione della directory, e puoi iterare sulle voci all'interno del blocco with. Il metodo __exit__ ripulisce la scansione della directory all'uscita.

Come vedi, l'istruzione with, grazie ai context manager, semplifica la gestione delle risorse occupandosi automaticamente di allocazione e deallocazione.

Scrivere context manager personalizzati in Python

Ora vediamo finalmente come scrivere i tuoi manager personalizzati per avere un controllo fine sulla gestione delle risorse.

Ci sono due approcci principali per scrivere context manager personalizzati: basato su classi e basato su funzioni.

Approccio basato su classi

L'approccio basato su classi è il metodo più strutturato e flessibile per scrivere context manager. Qui, definisci una classe che implementa i metodi speciali __enter__ e __exit__.

Vediamo un esempio di classe Timer che misura il tempo di esecuzione:

import time


class Timer:
   def __enter__(self):
       self.start_time = time.time()
       return self

   def __exit__(self, exc_type, exc_value, traceback):
       self.end_time = time.time()
       elapsed_time = self.end_time - self.start_time
       print(f"Elapsed time: {elapsed_time} seconds")


# Example usage
if __name__ == "__main__":
   with Timer() as timer:
       # Code block to measure the execution time
       time.sleep(2)  # Simulate some time-consuming operation
Elapsed time: 2.002082347869873 seconds

Qui, la classe Timer definisce il metodo __enter__ per catturare l'orario di inizio quando entri nel blocco with. Restituisce self per consentire l'accesso all'oggetto all'interno del blocco. Il metodo __exit__ calcola il tempo trascorso all'uscita dal blocco with e lo stampa.

Approccio basato su funzioni

Se preferisci un approccio più conciso, magari a scapito della flessibilità, l'approccio basato su funzioni può essere una buona opzione. Qui usi il decorator contextmanager del modulo contextlib per convertire qualsiasi funzione in un context manager.

Vediamo come funziona:

import time
from contextlib import contextmanager


@contextmanager
def timer():
   start_time = time.time()
   yield
   end_time = time.time()
   elapsed_time = end_time - start_time
   print(f"Elapsed time: {elapsed_time} seconds")


# Example usage
if __name__ == "__main__":
   with timer():
       time.sleep(2)
Elapsed time: 2.0020740032196045 seconds

Il decorator @contextmanager trasforma la funzione timer in un context manager. All'interno della funzione, viene catturato start_time, e l'istruzione yield mette in pausa l'esecuzione, permettendo al codice nel blocco with di essere eseguito.

Infine, la funzionalità di __exit__ è ottenuta catturando il tempo di fine e stampando il tempo trascorso.

In sostanza, scrivi la logica di __enter__ prima della keyword yield, mentre la logica di __exit__ viene dopo. Entrambi gli approcci raggiungono lo stesso risultato, ma la scelta dipende dalla tua preferenza per struttura e leggibilità.

Esempi pratici di context manager

Ora che sai come scrivere context manager personalizzati in Python, vediamo alcuni esempi pratici in cui possono essere estremamente utili.

Gestione delle operazioni su file

Le operazioni su file sono un compito comune in molte applicazioni. Che si tratti di leggere o scrivere file, una gestione corretta garantisce integrità dei dati ed efficienza nell'uso delle risorse. Consideriamo uno scenario in cui vogliamo scrivere del testo su un file usando un context manager personalizzato.

class FileManager:
   def __init__(self, filename, mode):
       self.filename = filename
       self.mode = mode
       self.file = None

   def __enter__(self):
       self.file = open(self.filename, self.mode)
       return self.file

   def __exit__(self, exc_type, exc_value, traceback):
       if self.file:
           self.file.close()


# Example usage
if __name__ == "__main__":
   with FileManager("example.txt", "w") as file:
       file.write("Hello, world!\n")

In questo esempio, definiamo una classe FileManager per imitare la funzione open di Python che gestisce le operazioni sui file. Vediamo cosa fa ciascun metodo:

  • __init__(self, filename, mode): Questo metodo è il costruttore della classe. Inizializza l'oggetto FileManager con il nome file e la modalità forniti, che specificano il file da aprire e la modalità in cui aprirlo (ad esempio, lettura, scrittura, append).
  • __enter__(self): Questo metodo viene chiamato all'ingresso del blocco with, come sempre. Apre il file specificato da filename nella modalità specificata da mode. L'oggetto file aperto viene assegnato a self.file e restituito per essere usato all'interno del blocco with.
  • __exit__(self, exc_type, exc_value, traceback): Questo metodo viene chiamato all'uscita dal blocco with, indipendentemente dal fatto che si sia verificata un'eccezione all'interno del blocco. Garantisce che il file venga chiuso correttamente chiamando self.file.close() se self.file non è None. Questo previene leak di risorse e assicura una corretta pulizia.

Se ispezioni il contenuto di example.txt dopo aver eseguito il codice sopra, dovresti vedere il testo scritto:

$ cat example.txt
Hello, world!

Ovviamente, il vantaggio della classe FileManager è che puoi implementare qualsiasi logica personalizzata per lavorare con i file. Per esempio, puoi modificarla in modo che la classe tenga traccia, in un file di log, di quali file vengono aperti o chiusi.

import datetime


class FileManager:
   def __init__(self, filename, mode, log_filename="file_log.txt"):
       self.filename = filename
       self.mode = mode
       self.log_filename = log_filename
       self.file = None

   def log_action(self, action):
       timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
       with open(self.log_filename, "a") as log_file:
           log_file.write(f"{timestamp} - {action}: {self.filename}\n")

   def __enter__(self):
       self.file = open(self.filename, self.mode)
       self.log_action("Opened")
       return self.file

   def __exit__(self, exc_type, exc_value, traceback):
       if self.file:
           self.file.close()
           self.log_action("Closed")


# Example Usage
with FileManager("example.txt", "r") as file:
   content = file.read()
   print(content)

Hello, world!

In questa versione modificata:

  • Il modulo datetime è importato per lavorare con i timestamp.
  • È stato aggiunto il metodo log_action per registrare le azioni sui file.
  • È stato aggiunto il parametro log_filename per specificare il nome del file di log. Il valore predefinito è 'file_log.txt'.
  • Il nome del file e l'azione (aperto o chiuso) vengono aggiunti al file di log usando il metodo log_action all'interno dei metodi __enter__ e __exit__.
  • All'interno del metodo log_action, datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") viene usato per ottenere il timestamp corrente nel formato "YYYY-MM-DD HH:MM:SS".
  • Il timestamp viene poi incluso nel log insieme al nome del file e all'azione.

Ora, se stampiamo il contenuto di file_log.txt, dovremmo vedere i log:

$ cat file_log.txt
2024-04-03 12:37:02 - Opened: example.txt
2024-04-03 12:37:02 - Closed: example.txt

Un altro caso d'uso pratico che possiamo implementare come context manager personalizzato è uno strumento di gestione di database SQLite:

import sqlite3


class DatabaseConnection:
   def __init__(self, database_name):
       self.database_name = database_name
       self.connection = None

   def __enter__(self):
       self.connection = sqlite3.connect(self.database_name)
       return self.connection

   def __exit__(self, exc_type, exc_value, traceback):
       if self.connection:
           self.connection.close()

Anche questo imita la funzione sqlite3.connect, ma può essere ulteriormente migliorato aggiungendo logica personalizzata di gestione del database. Verifichiamo se il manager funziona con alcune funzioni di esempio:

# Example usage
def create_table():
   with DatabaseConnection("example.db") as connection:
       cursor = connection.cursor()
       cursor.execute(
           """CREATE TABLE IF NOT EXISTS users (
                           id INTEGER PRIMARY KEY,
                           username TEXT,
                           email TEXT)"""
       )


def insert_data(username, email):
   with DatabaseConnection("example.db") as connection:
       cursor = connection.cursor()
       cursor.execute(
           "INSERT INTO users (username, email) VALUES (?, ?)", (username, email)
       )
       connection.commit()


def fetch_data():
   with DatabaseConnection("example.db") as connection:
       cursor = connection.cursor()
       cursor.execute("SELECT * FROM users")
       return cursor.fetchall()


if __name__ == "__main__":
   create_table()
   insert_data("john_doe", "[email protected]")
   insert_data("jane_doe", "[email protected]")
   users = fetch_data()
   print("Users in the database:")
   for user in users:
       print(user)

Users in the database:
(1, 'john_doe', '[email protected]')
(2, 'jane_doe', '[email protected]')
(3, 'john_doe', '[email protected]')
(4, 'jane_doe', '[email protected]')

Possiamo scrivere codice molto più manutenibile suddividendo le operazioni sul database in funzioni e usando il manager personalizzato che abbiamo appena creato.

Argomenti avanzati relativi ai context manager in Python

In questa sezione tratteremo argomenti avanzati relativi ai context manager in Python. Questi includono la gestione degli errori e l'annidamento dei context manager.

Gestione degli errori

La gestione degli errori è un aspetto essenziale della programmazione, e i context manager in Python offrono un modo pratico per gestire gli errori nelle operazioni di gestione delle risorse.

Nell'esempio della classe FileManager, la gestione degli errori può essere implementata all'interno del metodo __exit__ per garantire una corretta pulizia anche se si verifica un'eccezione. Per esempio, se si verifica un errore durante la scrittura del file (mentre siamo dentro il blocco with), vogliamo comunque assicurarci che il file venga chiuso per evitare leak di risorse. Possiamo modificare il metodo __exit__ come segue:

def __exit__(self, exc_type, exc_value, traceback):
   if self.file:
       self.file.close()
       if exc_type is not None:
           print(f"An error occurred: {exc_value}")

Qui, chiudiamo prima il file usando self.file.close() per garantire una corretta pulizia. Poi, verifichiamo se si è verificata un'eccezione (exc_type is not None). In tal caso, stampiamo un messaggio di errore per dare un riscontro su cosa sia andato storto.

Qui:

  • exc_type è la classe dell'eccezione, come AttributeError e IndexError.
  • exc_value è l'istanza dell'eccezione con il messaggio.
  • traceback è il traceback completo dell'errore.

Eseguiamo la classe FileManager con un codice volutamente errato:

with FileManager("example.txt", "r") as file:
   # Simulate a sample error
   content = file.read()
   print(content[150])

An error occurred: string index out of range

Quindi, per implementare logiche personalizzate quando gestisci eccezioni nei tuoi manager, ecco un flusso di lavoro di esempio che puoi seguire:

def __exit__(self, exc_type, exc_value, traceback):
   # Perform cleanup
   ...

   # Handle the exception
   if isinstance(exc_value, AttributeError):
       # Handle AttributeError here
       print(f"Exception message: {exc_value}")
   # Handle another exception
   elif isinstance(exc_value, AnotherError):
       ...

Annidare i context manager

Annidare i context manager ti consente di gestire più risorse contemporaneamente, ciascuna con il proprio context manager, all'interno di una singola istruzione with. Questo è particolarmente utile quando si affrontano operazioni complesse che coinvolgono più risorse. Vediamo come possiamo annidare i context manager nei nostri esempi.

Considera uno scenario in cui vogliamo aprire un file ed eseguire operazioni sul database nello stesso contesto. Magari, vogliamo inserire il contenuto di un file CSV come righe in un database. Possiamo ottenere questo risultato annidando i context manager FileManager e DatabaseConnection nella stessa istruzione with:

if __name__ == "__main__":
   with FileManager("example.txt", "w") as file, DatabaseConnection("example.db") as connection:
       ...

In questo esempio, i context manager FileManager e DatabaseConnection sono annidati nella stessa istruzione with. Questo ci permette di aprire il file e stabilire una connessione al database contemporaneamente. Le successive operazioni di scrittura su file e sul database vengono eseguite all'interno di questo contesto combinato.

Una cosa da tenere a mente è la leggibilità del codice. Ti consiglierei che il numero di contesti simultanei non superi due.

Conclusione

I context manager in Python offrono un approccio strutturato alla gestione delle risorse all'interno di un'istruzione with, garantendo una corretta allocazione e deallocazione delle risorse.

In tutto il tutorial, abbiamo imparato le basi dei context manager, visto esempi pratici e discusso argomenti avanzati come la gestione degli errori e l'annidamento. Padroneggiando la scrittura di context manager personalizzati, potrai scrivere codice più pulito e affidabile, migliorare la gestione degli errori e gestire le risorse in modo efficiente.

L'argomento della scrittura di context manager personalizzati rientra nella categoria "Python avanzato". Ecco alcune risorse correlate per migliorare ulteriormente le tue competenze:

Bexruz (Bex) Tuychiev

Autore

Bexruz (Bex) Tuychiev

Sono un creator di contenuti sulla data science con oltre 2 anni di esperienza e uno dei più seguiti su Medium. Mi piace scrivere articoli dettagliati su AI e ML con un pizzico di sarcasmo, perché bisogna pur fare qualcosa per renderli un po’ meno noiosi. Ho pubblicato oltre 130 articoli e un corso su DataCamp, con un altro in arrivo. I miei contenuti sono stati visti da oltre 5 milioni di persone, 20.000 delle quali sono diventate follower sia su Medium che su LinkedIn. 

Argomenti

Correlato