Modifica della firma del metodo mantenendo la compatibilità con le versioni precedenti

4

Ho ereditato un'API da mantenere.

Gli utenti possono passare una funzione di callback alla classe che viene chiamata su qualche evento. La funzione di callback è attualmente passata in un singolo argomento.

Devo modificare la firma prevista della funzione di callback per accettare ulteriori argomenti.

Questo andrebbe bene se la funzione di callback di tutti richiedesse * args e ** kwargs, ma sfortunatamente non lo fanno ed è troppo lavoro per convincere tutti a cambiarli.

Qual è il modo più pulito con cui posso apportare le modifiche all'API mantenendo il backback compatibile?

L'ho usato come shoe-in, ma non è una prova per il futuro, né è molto elegante:

...
if self._callback.func_code.co_argcount > 1:
    self._callback( data, additional_arg )
else:
    self._callback( data )
...
    
posta Griffin 03.12.2014 - 20:11
fonte

2 risposte

5

In Python, è "Più facile chiedere perdono che il permesso" - è normale pratica "pitonica" utilizzare le eccezioni e la gestione degli errori, piuttosto che ad es if check up-front ( "Guarda prima di saltare" ) per gestire potenziali problemi. La documentazione fornisce alcuni esempi che dimostrano dove quest'ultimo può davvero causare problemi - se la situazione cambia tra l'aspetto e il salto, hai serie difficoltà!

Su questa base e dato che una funzione aumenterà un TypeError se fornita con il numero sbagliato di argomenti, potresti usare:

try:
    # Have a go with the new interface
    self._callback(data, additional_arg)
except TypeError:
    # Fall back to the old one
    self._callback(data)

Potresti utilizzare una funzione di decorazione per ricomporre qualsiasi callback:

def api_compatible(func):
    @functools.wraps(func)
    def wrapper(data, *args, **kwargs):
        try:
            return func(data, *args, **kwargs)
        except TypeError:
            return func(data)
    return wrapper

Ora diventa:

self._callback = api_compatible(callback)
...
self._callback(data, additional_arg)
    
risposta data 04.12.2014 - 11:33
fonte
4

Un controllo esplicito della capacità del callback di gestire i parametri è il migliore che riuscirai a fare. Python potrebbe essere loosie-goosie nella sua digitazione anatra, ma si lamenterà e genererà un'eccezione TypeError se si alimenta una funzione con il numero sbagliato di parametri. Nessun ifs, ands o buts.

Hai funzioni esistenti nel campo che non ritieni di poter cambiare - e probabilmente per una buona ragione. Quindi, avendo un'ispezione che chiede "cosa può accettare questa funzione di callback?" o "cosa si aspetta questa funzione di callback?" - potrebbe essere inelegante, ma questo è ciò che è a tua disposizione.

Le altre scelte riguardano:

  1. Avere qualche forma di stato relativo all'oggetto (come un valore o metodo di istanza aggiuntivo) o anche uno stato globale (che sia una vera variabile globale, una variabile di classe, un oggetto di segnalazione singleton o whathaveyou) quali callback possono riferimento per ottenere le informazioni estese che stai ora offrendo loro. Ecco come C, Unix e molte altre codebase gestiscono informazioni eccezionali e aggiuntive. Sfortunatamente non è particolarmente elegante e può essere piuttosto problematico / non lavorativo per le applicazioni con multithreading.

  2. Nella remota possibilità che il tuo parametro data sia un tipo estensibile, potresti essere in grado di aggiungere campi ad esso. Se è un dict o una struttura simile, sei d'oro, purché le callback suonino secondo le regole di digitazione anatra e non controlli rigorosamente che abbiano ottenuto solo i campi che si aspettavano. Questo è un trucco che spesso funziona in linguaggi dinamici, anche se cadrebbe piatto sul suo volto in linguaggi tipizzati staticamente / ambienti che passano dati. Ma devi essere fortunato ad avere questo lavoro. Se data è un tipo più statico, torni alla scelta 1 o al tuo approccio basato sull'ispezione ("scelta 0").

Aggiorna

Per inciso, il tuo codice funzionerà correttamente in Python 2. Ma Python 3 cambia la posizione in cui è memorizzato il codice. Per comprendere sia il tuo attuale Python 2 che le future mosse su Python 3, ecco uno shim che funziona in entrambi:

import sys
_PY2 = sys.version_info[0] == 2

def arg_count(f):
    code = f.func_code if _PY2 else f.__code__
    return code.co_argcount

Quindi:

if arg_count(self._callback) > 1:
    ....
    
risposta data 03.12.2014 - 20:35
fonte

Leggi altre domande sui tag