Instrucțiuni tip Python 3 și analiză statică

Python 3.5 a introdus noul modul de tipare care oferă suport standard pentru biblioteci pentru adnotări de funcții de tip leveraging pentru sugestii tip opționale. Asta deschide ușa unor noi și interesante instrumente pentru verificarea tipului de tip static, cum ar fi mypy și în viitor optimizarea automată bazată pe tip. Sugestiile de tip sunt specificate în PEP-483 și PEP-484.

În acest tutorial explorez posibilitățile pe care le prezintă tipul de sugestii și vă arată cum să folosiți mypy pentru a analiza în mod static programele Python și pentru a îmbunătăți în mod semnificativ calitatea codului.

Sugestii tip

Sugestiile de tip sunt construite în partea de sus a adnotărilor funcției. Pe scurt, adnotările de funcții vă permit să adnotați argumentele și valoarea returnată a unei funcții sau metode cu metadate arbitrare. Tipurile de sugestii sunt un caz special de adnotări de funcții care adnotă în mod specific argumentele funcțiilor și valoarea returnată cu informații de tip standard. Adnotările funcționale, în general, și sugestiile de tip sunt, în particular, opționale. Să aruncăm o privire la un exemplu rapid:

"python def reverse_slice (text: str, start: int, end: int) -> str: text retur [start: end] [:: - 1]

revers_slice ('abcdef', 3, 5) 'ed'

Argumentele au fost adnotate atât cu tipul, cât și cu valoarea returnată. Dar este esențial să realizăm că Python ignoră acest lucru complet. Aceasta face ca informațiile de tip disponibile să fie disponibile prin adnotări atributul obiectului de funcție, dar este vorba de el.

end ': int,' retur ': str,' start ': int,' text ': str

Pentru a verifica faptul că Python ignoră cu adevărat sugestiile de tip, hai să ne amestecăm total sugestiile de tip:

"dict: retur text [începutul: sfârșitul] [:: - 1]

revers_slice ('abcdef', 3, 5) 'ed'

După cum puteți vedea, codul se comportă la fel, indiferent de sugestiile de tip.

Motivația pentru sugestii de tip

O.K. Sugestiile de tip sunt opționale. Sugestiile de tip sunt total ignorate de Python. De ce e vorba, atunci? Ei bine, există mai multe motive bune:

  • analiza statică
  • Suport IDE
  • documentație standard

Mă voi arunca mai târziu în analiză statică cu Mypy. Suportul IDE a început deja cu suportul PyCharm 5 pentru sugestii de tip. Documentația standard este excelentă pentru dezvoltatori care dau seama cu ușurință de tipul de argumente și de valoarea returnată doar prin analizarea unei semnături a funcției, precum și a generatoarelor de documentație automată care pot extrage informații de tip din sugestii.

tastare Modul

Modulul de tipare conține tipuri proiectate pentru a susține sugestiile de tip. De ce nu folosiți doar tipurile existente de Python cum ar fi int, str, list și dict? Puteți folosi cu siguranță aceste tipuri, dar din cauza tipăririi dinamice a Python, dincolo de tipurile de bază, nu obțineți prea multe informații. De exemplu, dacă doriți să specificați că un argument poate fi o mapare între un șir și un întreg, nu există nici o modalitate de ao face cu tipurile standard Python. Cu modulul de scriere, este la fel de ușor ca:

maparea python [str, int]

Să analizăm un exemplu mai complet: o funcție care are două argumente. Una dintre ele este o listă de dicționare în care fiecare dicționar conține chei care sunt șiruri și valori care sunt numere întregi. Celălalt argument este fie un șir sau un întreg. Modulul de tiparire permite specificarea exactă a unor astfel de argumente complicate.

"Python de la tastarea listei de import, Dict, Union

Definiți o listă de dicționare și returnați numărul de dicționare "" "dacă este o substanță (b, int, int) str): b = int (b) pentru i în intervalul (b): print (a)

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

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

Tipuri utile

Să vedem câteva dintre cele mai interesante tipuri din modulul de tastare.

Tipul Callable vă permite să specificați funcția care poate fi trecută ca argumente sau returnată ca rezultat, deoarece Python tratează funcțiile ca cetățeni de primă clasă. Sintaxa pentru cei chemați este să furnizeze o serie de tipuri de argumente (din nou de la modulul de tastare) urmată de o valoare de returnare. Dacă este confuz, iată un exemplu:

"python def do_something_fancy (date: Setați [float], on_error: Callable [[Exception, int], None]): ...

"

Funcția on_error callback este specificată ca o funcție care ia ca excepție o excepție și un întreg și nu returnează nimic.

Orice tip înseamnă că un controler de tip static ar trebui să permită orice operație, precum și alocarea unui alt tip. Fiecare tip este un subtip de Orice.

Tipul de Uniune pe care l-ați văzut mai devreme este util atunci când un argument poate avea mai multe tipuri, lucru foarte frecvent în Python. În exemplul următor, verify_config () funcția acceptă un argument config, care poate fi fie un obiect Config, fie un nume de fișier. Dacă este un nume de fișier, acesta apelează o altă funcție de a analiza fișierul într-un obiect Config și de ao returna.

"python def verific_config (config: Union [str, Config]): dacă este instanța (config, str): config = parse_config_file (config) ...

def parse_config_file (nume fișier: str) -> Config: ...

"

Tipul opțional înseamnă că argumentul poate fi Nici unul. Opțional [T] este echivalent cu Uniunea [T, Nici unul]

Există multe alte tipuri care denotă diferite capabilități, cum ar fi Iterable, Iterator, Reversible, SupportsInt, SupportsFloat, Sequence, MutableSequence și IO. Consultați documentația modulului de tastare pentru lista completă.

Principalul lucru este că puteți specifica tipul de argumente într-un mod foarte granular, care susține sistemul de tip Python la o înaltă fidelitate și permite generice și clase de bază abstracte.

Redirecționați referințele

Uneori doriți să vă referiți la o clasă într-un indiciu de tip în cadrul uneia dintre metodele sale. De exemplu, să presupunem că clasa A poate efectua o operație de îmbinare care ia o altă instanță a lui A, se îmbină cu ea însăși și returnează rezultatul. Iată o încercare naivă de a folosi sugestii de tip pentru ao specifica:

"Python clasa A: def merge (altele: A) -> A: ...

 1 clasa A: ----> 2 def merge (altele: A = Nici unul) -> A: 3 ... 4 

NumeError: numele "A" nu este definit "

Ce s-a întâmplat? Clasa A nu este definită încă atunci când indicația de tip pentru metoda de îmbinare () este verificată de Python, astfel încât clasa A nu poate fi utilizată în acest moment (direct). Soluția este destul de simplă și am văzut-o folosită înainte de SQLAlchemy. Trebuie doar să specificați sugestia de tip ca șir. Python va înțelege că este o referință înainte și va face ceea ce trebuie:

Python clasa A: def merge (altele: 'A' = Nici unul) -> 'A': ...

Alias ​​de tip

Un dezavantaj al utilizării sugestiilor de tip pentru specificațiile de lungă durată constă în faptul că poate dezordine codul și poate fi mai ușor de citit, chiar dacă oferă o mulțime de informații de tip. Aveți posibilitatea să tipuri de alias ca orice alt obiect. Este la fel de simplu ca:

"python Data = Dict [int, secvență [Dict [str, opțional [Lista [float]]]]

def foo (date: date) -> bool: ... "

get_type_hints () Funcția Helper

Modulul de tipare furnizează funcția get_type_hints (), care oferă informații despre tipurile de argument și valoarea returnată. In timp ce adnotări atributul returnează atribute tip, deoarece acestea sunt doar adnotări, tot recomandăm să utilizați funcția get_type_hints () deoarece rezolvă referințele înainte. De asemenea, dacă specificați o valoare implicită a niciunuia din unul dintre argumente, funcția get_type_hints () va returna automat tipul său ca fiind [T, NoneType] dacă tocmai ați specificat T. Să vedem diferența folosind metoda A.merge () definită mai devreme:

"imprimare python (A.merge.adnotări)

'altul': 'A', 'returnare': 'A' "

adnotări atributul returnează pur și simplu valoarea de adnotare ca atare. În acest caz, este vorba doar de șirul "A" și nu de obiectul de clasă A, la care "A" este doar o referință înainte.

"imprimare python (get_type_hints (A.merge))

'întoarcere': principal.A '>,' altul ': tasting.Union [principal.A, NoneType] "

Funcția get_type_hints () a convertit tipul alte argument pentru o Uniune de A (clasa) și NoneType din cauza argumentului implicit None. Tipul de retur a fost, de asemenea, transformat în clasa A.

Decoratorii

Sugestiile de tip sunt o specializare a adnotărilor de funcții și pot funcționa, de asemenea, una lângă cealaltă cu o altă adnotare a funcțiilor.

Pentru a face acest lucru, modulul de tastare oferă două decoratoare: @no_type_check și @no_type_check_decorator. @no_type_check decoratorul poate fi aplicat fie unei clase, fie unei funcții. Se adaugă no_type_check atribuie funcției (sau fiecărei metode a clasei). În acest fel, verificatorii de tip vor ști să ignore adnotările, care nu sunt sugestii de tip.

Este puțin cam greu, pentru că dacă scrieți o bibliotecă care va fi utilizată pe scară largă, trebuie să presupunem că va fi folosit un checker de tip și dacă doriți să vă adnotați funcțiile cu indicații non-tip, trebuie să le decorați cu @no_type_check.

Un scenariu obișnuit atunci când se utilizează adnotări funcționale regulate este de a avea și un decorator care operează asupra lor. De asemenea, doriți să dezactivați verificarea tipului în acest caz. O opțiune este de a utiliza @no_type_check decorator în plus față de decorator, dar care devine vechi. În schimb, @no_Type_check_decorator poate fi folosit pentru a decora decoratorul dvs., astfel încât să se comporte de asemenea @no_type_check (adaugă no_type_check atribut).

Permiteți-mi să ilustrez toate aceste concepte. Dacă încercați să get_type_hint () (ca orice verificator de tip va face) pe o funcție care este adnotată cu o adnotare de șir regulat, get_type_hints () o va interpreta ca referință în perspectivă:

"python def f (a:" o anumită adnotare "): treci

print (get_type_hints (f))

SyntaxError: ForwardRef trebuie să fie o expresie - a primit "o anumită adnotare"

Pentru a evita acest lucru, adăugați decoratorul @no_type_check, iar get_type_hints returnează pur și simplu un dict gol, în timp ce __annotations__ atributul returnează adnotările:

"python @no_type_check def f (a:" o anumită adnotare "): treceți

print (get_type_hints (f))

imprimare (f.adnotări) 'a': 'o anumită adnotare' "

Acum, să presupunem că avem un decorator care imprimă dictările adnotărilor. Poți să o decorezi cu @no_Type_check_decorator și apoi decorați funcția și nu vă faceți griji cu privire la un tip de checker care apelează get_type_hints () și obține confuzie. Aceasta este probabil o bună practică pentru fiecare decorator care operează pe adnotări. Nu uitați @ functools.wraps, în caz contrar, adnotările nu vor fi copiate în funcția decorată și totul se va desprinde. Acest lucru este acoperit în detaliu în adnotările funcției Python 3.

(* args, ** kwargs): print (f .__ adnotări__) întoarcere f (* args, ** kwargs) întoarcere decorată (fgs)

Acum, puteți decora funcția doar cu @print_annotations, și ori de câte ori se numește, va imprima adnotările sale.

"python @print_annotations def f (a:" o anumită adnotare "): pass

f (4) 'a': 'o anumită adnotare' "

apel get_type_hints () este, de asemenea, în siguranță și returnează un dict gol.

tipărirea lui python (get_type_hints (f))

Analiză statică cu Mypy

Mypy este un checker de tip static care a fost inspirația pentru sugestiile de tip și modulul de tastare. Guido van Rossum însuși este autorul PEP-483 și co-autor al PEP-484.

Instalarea Mypy

Mypy este în curs de dezvoltare foarte activă și, din această scriere, pachetul de pe PyPI este depășit și nu funcționează cu Python 3.5. Pentru a utiliza Mypy cu Python 3.5, obțineți cele mai recente din repozitoriul Mypy pe GitHub. Este la fel de simplu ca:

bash pip3 instalează git + git: //github.com/JukkaL/mypy.git

Se joacă cu Mypy

Odată ce ați instalat Mypy, puteți rula doar programul Mypy pe programele dvs. Următorul program definește o funcție care așteaptă o listă de șiruri de caractere. Apoi se invocă funcția cu o listă de numere întregi.

"python de la tastarea listei de import

def case_insensitive_dedupe (date: List [str]): "" "Convertește toate valorile în litere mici și elimină duplicatele" "" return (listă (x.lower () pentru x în date)

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

La rularea programului, în mod evident nu reușește la rulare cu următoarea eroare:

plain python3 dedupe.py Traceback (ultimul apel ultimul): Fișierul "dedupe.py", linia 8, în print (case_insensitive_dedupe ([1, 2, 3])) Fișierul "dedupe.py", rândul 5, în lista returnată case_insensitive_dedupe (set (x.lower , în retur (set (x.lower () pentru x în date)) AttributeError: obiectul "int" nu are nici un atribut "inferior"

Care e problema cu asta? Problema este că nu este clar nici chiar în acest caz foarte simplu care este cauza principală. Este o problemă de tip de intrare? Sau poate codul în sine este greșit și nu ar trebui să încerce să apeleze inferior() pe obiectul "int". O altă problemă este că, dacă nu aveți 100% acoperire de testare (și, să fim cinstiți, niciunul dintre noi nu face acest lucru), atunci astfel de probleme pot fi ascunse în unele metode netestate, folosite rar și pot fi detectate în cel mai rău timp de producție.

Încărcarea tipică, ajutată de sugestii de tip, vă oferă o plasă suplimentară de siguranță, asigurându-vă că numiți întotdeauna funcțiile (adnotate cu sugestii de tip) cu tipurile potrivite. Iată outputul lui Mypy:

simplă (N)> mypy dedupe.py dedupe.py:8: eroare: elementul de listă 0 are tipul incompatibil "int" dedupe.py:8: eroare: elementul de listă 1 are tipul incompatibil "int" dedupe.py:8: eroare : Elementul de listă 2 are tipul incompatibil "int"

Acest lucru este simplu, indică direct problema și nu necesită o serie de teste. Un alt avantaj al verificării tipului static este faptul că, dacă vă angajezi, puteți să ignorați tipul de verificare dinamică, cu excepția cazului în parsarea intrărilor externe (citirea fișierelor, solicitările de rețea primite sau intrarea utilizatorilor). De asemenea, construiește multă încredere în ceea ce privește refactorizarea.

Concluzie

Sugestiile de tip și modulul de tastare sunt complet adăugate opțional expresivității Python. Deși este posibil ca acestea să nu se potrivească cu gustul fiecăruia, pentru proiecte mari și echipe mari, ele pot fi indispensabile. Dovezile sunt că echipele mari utilizează deja verificarea tipului static. Acum că informația despre tip este standardizată, va fi mai ușor să partajați codul, utilitățile și instrumentele care o folosesc. IDE-uri cum ar fi PyCharm profită deja de acesta pentru a oferi o experiență mai bună pentru dezvoltatori.

Cod