Suggerimenti di tipo Python 3 e analisi statica

Python 3.5 ha introdotto il nuovo modulo di tipizzazione che fornisce supporto di libreria standard per l'utilizzo di annotazioni di funzioni per suggerimenti tipo facoltativi. Ciò apre la porta a nuovi e interessanti strumenti per il controllo di tipo statico come il mypy e, in futuro, l'ottimizzazione basata sul tipo di automazione. I suggerimenti tipo sono specificati in PEP-483 e PEP-484.

In questo tutorial esploro le possibilità che sono presenti i suggerimenti e ti mostro come usare mypy per analizzare staticamente i tuoi programmi Python e migliorare significativamente la qualità del tuo codice.

Tipo Suggerimenti

I suggerimenti dei tipi sono costruiti sopra le annotazioni delle funzioni. In breve, le annotazioni delle funzioni consentono di annotare gli argomenti e restituire il valore di una funzione o metodo con metadati arbitrari. Gli hint di tipo sono un caso speciale di annotazioni di funzioni che annotano specificamente gli argomenti della funzione e il valore di ritorno con informazioni di tipo standard. Le annotazioni delle funzioni in generale e i suggerimenti sui tipi in particolare sono totalmente opzionali. Diamo un'occhiata ad un rapido esempio:

"python def reverse_slice (testo: str, inizio: int, fine: int) -> str: restituisci testo [inizio: fine] [:: - 1]

reverse_slice ('abcdef', 3, 5) 'ed "

Gli argomenti sono stati annotati con il loro tipo e il valore restituito. Ma è fondamentale rendersi conto che Python lo ignora completamente. Rende le informazioni sul tipo disponibili attraverso il annotazioni attributo dell'oggetto funzione, ma questo è tutto.

python reverse_slice .__ annotations 'end': int, 'return': str, 'start': int, 'text': str

Per verificare che Python ignori davvero i suggerimenti del tipo, diamo una scusa ai suggerimenti tipo:

"python def reverse_slice (testo: float, inizio: str, fine: bool) -> dict: restituisce il testo [inizio: fine] [:: - 1]

reverse_slice ('abcdef', 3, 5) 'ed "

Come puoi vedere, il codice si comporta allo stesso modo, indipendentemente dai suggerimenti del tipo.

Motivazione per i suggerimenti di tipo

OK. I suggerimenti di tipo sono facoltativi. Gli hint di tipo sono totalmente ignorati da Python. Qual è il punto di loro, allora? Bene, ci sono diversi buoni motivi:

  • analisi statica
  • Supporto IDE
  • documentazione standard

Mi tufferò nell'analisi statica con Mypy più tardi. Il supporto IDE è già stato avviato con il supporto di PyCharm 5 per i suggerimenti tipo. La documentazione standard è ottima per gli sviluppatori che possono facilmente capire il tipo di argomenti e restituire il valore semplicemente guardando la firma di una funzione così come i generatori di documentazione automatica che possono estrarre le informazioni sul tipo dai suggerimenti.

Il digitando Modulo

Il modulo di battitura contiene tipi progettati per supportare suggerimenti tipo. Perché non usare solo i tipi Python esistenti come int, str, list e dict? Puoi sicuramente usare questi tipi, ma a causa della digitazione dinamica di Python, oltre ai tipi di base non si ottengono molte informazioni. Ad esempio, se si desidera specificare che un argomento può essere un mapping tra una stringa e un intero, non c'è modo di farlo con i tipi standard di Python. Con il modulo di battitura, è facile come:

python Mapping [str, int]

Diamo un'occhiata ad un esempio più completo: una funzione che accetta due argomenti. Uno di questi è un elenco di dizionari in cui ogni dizionario contiene chiavi che sono stringhe e valori che sono numeri interi. L'altro argomento è una stringa o un intero. Il modulo di battitura consente specifiche precise di argomenti così complicati.

"python da digitare Elenco di importazione, Dict, Union

def foo (a: List [Dict [str, int]], b: Union [str, int]) -> int: "" "Stampa un elenco di dizionari e restituisce il numero di dizionari" "" se isinstance (b, str): b = int (b) per i in range (b): print (a)

x = [dict (a = 1, b = 2), dict (c = 3, d = 4)] foo (x, '3')

['b': 2, 'a': 1, 'd': 4, 'c': 3] ['b': 2, 'a': 1, 'd': 4 , 'c': 3] ['b': 2, 'a': 1, 'd': 4, 'c': 3] "

Tipi utili

Vediamo alcuni dei tipi più interessanti dal modulo di battitura.

Il tipo Callable consente di specificare la funzione che può essere passata come argomento o restituita come risultato, poiché Python considera le funzioni come cittadini di prima classe. La sintassi per callables consiste nel fornire una matrice di tipi di argomenti (sempre dal modulo di battitura) seguita da un valore di ritorno. Se questo è confuso, ecco un esempio:

"python def do_something_fancy (data: set [float], on_error: callable [[Exception, int], None]): ...

"

La funzione di callback on_error viene specificata come una funzione che accetta un'eccezione e un intero come argomenti e non restituisce nulla.

Il tipo Qualsiasi indica che un controllo di tipo statico dovrebbe consentire qualsiasi operazione e l'assegnazione a qualsiasi altro tipo. Ogni tipo è un sottotipo di Any.

Il tipo di Unione che hai visto prima è utile quando un argomento può avere più tipi, che è molto comune in Python. Nell'esempio seguente, il verify_config () la funzione accetta un argomento di configurazione, che può essere un oggetto Config o un nome file. Se si tratta di un nome file, chiama un'altra funzione per analizzare il file in un oggetto Config e restituirlo.

"python def verify_config (config: Union [str, Config]): if isinstance (config, str): config = parse_config_file (config) ...

def parse_config_file (nome file: str) -> Config: ...

"

Il tipo facoltativo indica che l'argomento potrebbe essere anche Nessuno. Opzionale [T] è equivalente a Union [T, None]

Ci sono molti altri tipi che denotano varie funzionalità come Iterable, Iterator, Reversible, SupportsInt, SupportsFloat, Sequence, MutableSequence e IO. Controlla la documentazione del modulo di battitura per l'elenco completo.

La cosa principale è che puoi specificare il tipo di argomenti in un modo molto fine che supporta il sistema di tipo Python ad alta fedeltà e consente anche a generici e classi astratte di base.

Forward References

A volte vuoi fare riferimento a una classe in un suggerimento sul tipo all'interno di uno dei suoi metodi. Ad esempio, supponiamo che la classe A possa eseguire un'operazione di unione che prende un'altra istanza di A, si unisce a se stessa e restituisce il risultato. Ecco un ingenuo tentativo di utilizzare i suggerimenti tipo per specificarlo:

"python classe A: def fusione (altro: A) -> A: ...

 1 classe A: ----> 2 def unione (altro: A = Nessuno) -> A: 3 ... 4 

NameError: nome 'A' non è definito "

Quello che è successo? La classe A non è ancora definita quando il tipo hint per il suo metodo merge () è controllato da Python, quindi la classe A non può essere utilizzata a questo punto (direttamente). La soluzione è abbastanza semplice e l'ho già vista in precedenza da SQLAlchemy. Basta specificare il suggerimento sul tipo di stringa. Python capirà che è un riferimento futuro e farà la cosa giusta:

python classe A: def fusione (altro: 'A' = Nessuno) -> 'A': ...

Digita alias

Uno svantaggio dell'uso di suggerimenti tipo per le specifiche di tipo lungo è che può ingombrare il codice e renderlo meno leggibile, anche se fornisce molte informazioni sul tipo. Puoi creare tipi di alias come qualsiasi altro oggetto. È semplice come:

"python Data = Dict [int, Sequence [Dict [str, Facoltativo [Elenco [float]]]]

def foo (data: Dati) -> bool: ... "

Il get_type_hints () Funzione di supporto

Il modulo di battitura fornisce la funzione get_type_hints (), che fornisce informazioni sui tipi di argomenti e sul valore restituito. Mentre il annotazioni attributo restituisce suggerimenti tipo perché sono solo annotazioni, ti consiglio comunque di utilizzare la funzione get_type_hints () perché risolve i riferimenti avanzati. Inoltre, se si specifica un valore predefinito di Nessuno su uno degli argomenti, la funzione get_type_hints () restituirà automaticamente il suo tipo come Unione [T, NoneType] se si è appena specificato T. Vediamo la differenza utilizzando il metodo A.merge () definito in precedenza:

"stampa pitone (A.merge.annotazioni)

'altro': 'A', 'ritorno': 'A' "

Il annotazioni attributo restituisce semplicemente il valore di annotazione così com'è. In questo caso è solo la stringa 'A' e non l'oggetto di classe A, a cui 'A' è solo un riferimento diretto.

"python print (get_type_hints (A.merge))

'ritorno': principale.A '>,' altro ': typing.Union [principale.A, NoneType] "

La funzione get_type_hints () ha convertito il tipo di altro argomento a Union of A (the class) e NoneType a causa dell'argomento default None. Anche il tipo di reso è stato convertito in classe A.

I decoratori

Gli hint di tipo sono una specializzazione delle annotazioni di funzione e possono anche funzionare fianco a fianco con altre annotazioni di funzione.

Per fare ciò, il modulo di battitura fornisce due decoratori: @no_type_check e @no_type_check_decorator. Il @no_type_check decoratore può essere applicato a una classe oa una funzione. Aggiunge il no_type_check attributo alla funzione (o ad ogni metodo della classe). In questo modo, i controllori di tipi sapranno di ignorare le annotazioni, che non sono suggerimenti di tipo.

È un po 'macchinoso perché se si scrive una libreria che verrà utilizzata in senso ampio, è necessario presumere che verrà utilizzato un controllo di tipo e se si desidera annotare le proprie funzioni con suggerimenti non di tipo, è necessario anche decorarle con @no_type_check.

Uno scenario comune quando si usano annotazioni di funzioni regolari è anche avere un decoratore che operi su di essi. In questo caso, si desidera disattivare anche la verifica del tipo. Un'opzione è usare il @no_type_check decoratore oltre al tuo decoratore, ma che invecchia. Invece, il @no_Type_check_decorator può essere usato per decorare il tuo decoratore in modo che si comporti anche come @no_type_check (aggiunge il no_type_check attributo).

Lasciatemi illustrare tutti questi concetti. Se si prova a get_type_hint () (come qualsiasi altro tipo di controllo di tipo) su una funzione annotata con un'annotazione di stringa regolare, get_type_hints () la interpreterà come riferimento futuro:

"python def f (a: 'some annotation'): pass

stampa (get_type_hints (f))

SyntaxError: ForwardRef deve essere un'espressione - ottenuto "qualche annotazione"

Per evitarlo, aggiungi il decoratore @no_type_check e get_type_hints restituisce semplicemente un dict vuoto, mentre il __annotations__ l'attributo restituisce le annotazioni:

"python @no_type_check def f (a: 'some annotation'): pass

print (get_type_hints (f))

stampa (f.annotazioni) 'a': 'some annotation' "

Ora, supponiamo di avere un decoratore che stampa le annotazioni dettate. Puoi decorarlo con il @no_Type_check_decorator e quindi decorare la funzione e non preoccuparti di un controllo di tipo che chiama get_type_hints () e si confonde. Questa è probabilmente una best practice per ogni decoratore che opera su annotazioni. Non dimenticare il @ functools.wraps, altrimenti le annotazioni non verranno copiate sulla funzione decorata e tutto andrà in pezzi. Questo è trattato in dettaglio in Annotazioni di funzioni di Python 3.

python @no_type_check_decorator def print_annotations (f): @ functools.wraps (f) def decorato (* args, ** kwargs): print (f .__ annotations__) return f (* args, ** kwargs) reso decorato

Ora puoi decorare la funzione solo con @print_annotations, e ogni volta che viene chiamato stamperà le sue annotazioni.

"python @print_annotations def f (a: 'some annotation'): pass

f (4) 'a': 'some annotation' "

chiamata get_type_hints () è anche sicuro e restituisce un dict vuoto.

python print (get_type_hints (f))

Analisi statica con Mypy

Mypy è un controllo di tipo statico che è stato l'ispirazione per i suggerimenti sul tipo e il modulo di battitura. Guido van Rossum stesso è l'autore di PEP-483 e coautore di PEP-484.

Installare Mypy

Mypy è in uno sviluppo molto attivo e al momento della stesura il pacchetto su PyPI non è aggiornato e non funziona con Python 3.5. Per usare Mypy con Python 3.5, ottieni l'ultimo dal repository di Mypy su GitHub. È semplice come:

bash pip3 install git + git: //github.com/JukkaL/mypy.git

Giocando con Mypy

Una volta installato Mypy, puoi semplicemente eseguire Mypy sui tuoi programmi. Il seguente programma definisce una funzione che si aspetta un elenco di stringhe. Quindi richiama la funzione con un elenco di numeri interi.

"python da digitare lista di importazione

def case_insensitive_dedupe (data: List [str]): "" "Converte tutti i valori in minuscolo e rimuove i duplicati" "" elenco di ritorno (set (x.lower () per x nei dati))

print (case_insensitive_dedupe ([1, 2])) "

Quando si esegue il programma, ovviamente non riesce in fase di esecuzione con il seguente errore:

plain python3 dedupe.py Traceback (ultima chiamata ultima): file "dedupe.py", riga 8, in print (case_insensitive_dedupe ([1, 2, 3])) File "dedupe.py", riga 5, in caso_insensitive_dedupe lista di restituzione (set (x.lower () per x nei dati)) File "dedupe.py", riga 5 , nel lista di ritorno (set (x.lower () per x nei dati)) AttributeError: l'oggetto 'int' non ha attributo 'lower'

Qual è il problema con quello? Il problema è che non è chiaro immediatamente anche in questo caso molto semplice quale sia la causa principale. È un problema del tipo di input? O forse il codice stesso è sbagliato e non dovresti provare a chiamare il inferiore() metodo sull'oggetto 'int'. Un altro problema è che se non si dispone di una copertura di prova del 100% (e, siamo onesti, nessuno di noi lo fa), allora tali problemi possono nascondersi in un percorso di codice non testato, raramente utilizzato e essere rilevati nel momento peggiore della produzione.

La tipizzazione statica, aiutata dai suggerimenti del tipo, ti offre una rete di sicurezza extra assicurandoti di chiamare sempre le tue funzioni (annotate con suggerimenti tipo) con i tipi giusti. Ecco l'output di Mypy:

plain (N)> mypy dedupe.py dedupe.py:8: errore: l'elemento di elenco 0 ha tipo incompatibile "int" dedupe.py:8: errore: l'elemento di elenco 1 ha un tipo incompatibile "int" dedupe.py:8: errore : L'elemento di elenco 2 ha il tipo "int" non compatibile

Questo è semplice, punta direttamente al problema e non richiede l'esecuzione di molti test. Un altro vantaggio del controllo di tipo statico è che, se ci si impegna, è possibile saltare il controllo dinamico dei tipi tranne durante l'analisi di input esterni (lettura di file, richieste di rete in entrata o input dell'utente). Inoltre genera molta fiducia per quanto riguarda il refactoring.

Conclusione

Il tipo hint e il modulo di battitura sono aggiunte totalmente opzionali all'espressività di Python. Anche se potrebbero non essere adatti a tutti, per grandi progetti e grandi team possono essere indispensabili. La prova è che le grandi squadre usano già il controllo di tipo statico. Ora che le informazioni sul tipo sono standardizzate, sarà più facile condividere codice, utilità e strumenti che lo utilizzano. IDE come PyCharm già ne approfittano per fornire una migliore esperienza di sviluppo.