Decoratori e metaprogrammazione per gestire API deprecate in Python

by theArchitect
SHARE
Decoratori e metaprogrammazione per gestire API deprecate in Python
© Guida-HTML5.it

Introduzione

Quando un progetto Python cresce, alcune funzioni o alcuni metodi possono diventare obsoleti. Tuttavia, eliminarli immediatamente rischia di rompere il codice degli utenti che li stanno ancora utilizzando. Una soluzione professionale consiste nel deprecare gradualmente una parte dell’API: il vecchio elemento continua a funzionare, ma Python mostra un avviso che invita a utilizzare una nuova alternativa.

In questo tutorial costruiremo un sistema riutilizzabile basato su:

  • un decoratore @deprecated;
  • la libreria standard warnings;
  • functools.wraps, per conservare nome e documentazione della funzione;
  • una metaclasse capace di applicare automaticamente una regola a tutti i metodi contrassegnati come obsoleti.

Questo approccio è utile nelle librerie pubbliche, nei framework interni e nei progetti che devono mantenere la compatibilità tra più versioni.

Codice completo

import warnings
from functools import wraps


def deprecated(message, *, removal_version=None):
    """
    Contrassegna una funzione o un metodo come deprecato.
    """

    if removal_version is not None:
        full_message = (
            f"{message} "
            f"La rimozione è prevista nella versione {removal_version}."
        )
    else:
        full_message = message

    def decorator(function):
        @wraps(function)
        def wrapper(*args, **kwargs):
            warnings.warn(
                full_message,
                category=DeprecationWarning,
                stacklevel=2
            )
            return function(*args, **kwargs)

        # Metadati utili per strumenti automatici e metaclassi
        wrapper.__deprecated__ = True
        wrapper.__deprecation_message__ = full_message

        return wrapper

    return decorator


class DeprecationMeta(type):
    """
    Metaclasse che raccoglie i metodi deprecati della classe.
    """

    def __new__(mcls, name, bases, namespace):
        deprecated_methods = []

        for attribute_name, attribute_value in namespace.items():
            if getattr(attribute_value, "__deprecated__", False):
                deprecated_methods.append(attribute_name)

        namespace["__deprecated_methods__"] = tuple(deprecated_methods)

        return super().__new__(mcls, name, bases, namespace)


class UserService(metaclass=DeprecationMeta):
    def find_user(self, user_id):
        return {
            "id": user_id,
            "name": "Ada Lovelace"
        }

    @deprecated(
        "Il metodo get_user() è obsoleto. "
        "Usa find_user() al suo posto.",
        removal_version="3.0"
    )
    def get_user(self, user_id):
        return self.find_user(user_id)


if __name__ == "__main__":
    service = UserService()

    print("Metodi deprecati:", UserService.__deprecated_methods__)

    user = service.get_user(42)
    print(user)

    current_user = service.find_user(42)
    print(current_user)

Spiegazione

Il decoratore deprecated

Il decoratore riceve un messaggio descrittivo e, opzionalmente, la versione in cui il metodo dovrebbe essere rimosso. Restituisce una funzione interna chiamata decorator, che a sua volta riceve la funzione originale.

Il livello più interno, wrapper, intercetta la chiamata al metodo. Prima di eseguire la funzione originale invoca:

warnings.warn(
    full_message,
    category=DeprecationWarning,
    stacklevel=2
)

DeprecationWarning è la categoria standard per segnalare API obsolete. Il parametro stacklevel=2 fa apparire l’avviso nella riga in cui l’utente ha chiamato il metodo, invece che all’interno del decoratore. Questo rende il messaggio molto più utile durante il debugging.

Dopo l’avviso, il wrapper restituisce il risultato della funzione originale:

return function(*args, **kwargs)

In questo modo la compatibilità viene mantenuta: il vecchio metodo continua a funzionare, ma comunica chiaramente che dovrebbe essere sostituito.

Perché usare functools.wraps

Senza @wraps, il metodo decorato apparirebbe con il nome wrapper e perderebbe parte delle informazioni originali, come la docstring e il nome della funzione. wraps copia automaticamente i principali metadati:

  • __name__;
  • __doc__;
  • __module__;
  • altri attributi descrittivi.

Questa è una best practice importante quando si scrivono decoratori destinati a librerie riutilizzabili.

Metadati personalizzati

Nel codice aggiungiamo due attributi al wrapper:

wrapper.__deprecated__ = True
wrapper.__deprecation_message__ = full_message

Gli attributi personalizzati permettono di riconoscere automaticamente i metodi deprecati. Non ci limitiamo quindi a modificare il comportamento della funzione: aggiungiamo anche informazioni strutturate che possono essere utilizzate da altri strumenti.

La metaclasse DeprecationMeta

Una metaclasse controlla la costruzione delle classi. Il metodo speciale __new__ riceve il nome della classe, le classi base e il dizionario degli attributi dichiarati nel corpo della classe.

Il ciclo:

for attribute_name, attribute_value in namespace.items():
    if getattr(attribute_value, "__deprecated__", False):
        deprecated_methods.append(attribute_name)

cerca tutti gli attributi contrassegnati dal decoratore. Successivamente la metaclasse aggiunge alla classe l’attributo:

__deprecated_methods__

Nel nostro esempio, il risultato sarà:

("get_user",)

Questa informazione può essere utilizzata per generare documentazione, controlli automatici in fase di test o report di migrazione.

La differenza tra decoratore e metaclasse

Il decoratore modifica una singola funzione o un singolo metodo. La metaclasse, invece, lavora sull’intera classe durante la sua creazione. Usati insieme, i due strumenti separano bene le responsabilità:

  • il decoratore definisce il comportamento dell’avviso;
  • la metaclasse raccoglie e organizza le informazioni a livello di classe.

Best practice

  • Indicare sempre l’alternativa: un messaggio come “metodo obsoleto” è poco utile. Specificare quale metodo usare al suo posto.
  • Indicare una versione di rimozione: aiuta gli utenti a pianificare la migrazione.
  • Non nascondere il comportamento: la funzione deprecata dovrebbe continuare a comportarsi come prima durante il periodo di transizione.
  • Usare stacklevel=2: l’avviso deve puntare al codice dell’utilizzatore.
  • Conservare i metadati: utilizzare sempre functools.wraps nei decoratori di funzioni.
  • Testare gli avvisi: il modulo unittest offre assertWarns, utile per verificare che il messaggio venga prodotto.
  • Documentare il piano di rimozione: una deprecazione senza una strategia può rimanere nel progetto per anni.

Durante lo sviluppo è possibile rendere visibili gli avvisi con:

python -Wd applicazione.py

In alternativa, nel codice di test si può usare:

import warnings

with warnings.catch_warnings(record=True) as captured:
    warnings.simplefilter("always")
    service.get_user(42)

    assert len(captured) == 1
    assert "find_user" in str(captured[0].message)

Riepilogo

Un decoratore di deprecazione permette di avvisare gli utenti senza interrompere immediatamente la compatibilità. L’uso di warnings.warn fornisce un meccanismo standard, mentre functools.wraps preserva le informazioni della funzione originale.

La metaclasse aggiunge un livello di metaprogrammazione: analizza la classe mentre viene costruita e raccoglie automaticamente i metodi obsoleti. Il risultato è un sistema leggero, estendibile e adatto alla gestione ordinata del ciclo di vita di un’API.

Approfondisci con risorse ufficiali

SHARE