Python 3 Type Hint og Statisk Analyse

Python 3.5 introduserte den nye skrivemodulen som gir standardbiblioteksstøtte for bruk av funksjonsannonser for valgfrie typen hint. Det åpner døren for nye og interessante verktøy for statisk type som sjekker som mypy og i fremtiden muligens automatisert typebasert optimalisering. Type hint er spesifisert i PEP-483 og PEP-484.

I denne opplæringen undersøker jeg mulighetene som skriver hint til stede og viser deg hvordan du bruker mypy til å statisk analysere Python-programmene og forbedre kodenes kvalitet betydelig..

Skriv tips

Type hint er bygget på toppen av funksjonsannonser. Kort sagt, med funksjonskommentarer kan du annotere argumentene og returnere verdien av en funksjon eller metode med vilkårlig metadata. Type hint er et spesielt tilfelle av funksjonsannonser som spesifikt annoterer funksjonsargumenter og returverdi med standard typen informasjon. Funksjonsannonser generelt og spesielle tips er spesielt valgfrie. La oss ta en titt på et raskt eksempel:

"python def reverse_slice (tekst: str, start: int, slutt: int) -> str: return text [start: slutt] [:: - 1]

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

Argumentene ble merket med deres type samt returverdi. Men det er kritisk å innse at Python ignorerer dette helt. Det gjør typen informasjon tilgjengelig gjennom merknader Attributt til funksjonsobjektet, men det handler om det.

python reverse_slice .__ annotasjoner 'ende': int, 'return': str, 'start': int, 'text': str

For å bekrefte at Python virkelig ignorerer type hintene, la oss helt rote opp typen hint:

"python def reverse_slice (tekst: float, start: str, end: bool) -> dikt: returtekst [start: slutt] [:: - 1]

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

Som du kan se, oppfører koden det samme, uansett typen hint.

Motivasjon for Type Hint

OK. Type hint er valgfritt. Type tips er helt ignorert av Python. Hva er meningen med dem da? Vel, det er flere gode grunner:

  • statisk analyse
  • IDE-støtte
  • standard dokumentasjon

Jeg vil dykke inn i statisk analyse med Mypy senere. IDE-støtte allerede startet med PyCharm 5s støtte for typehenvisninger. Standard dokumentasjon er bra for utviklere som lett kan finne ut hvilken type argumenter og returverdier bare ved å se på en funksjons signatur samt automatiserte dokumentasjonsgeneratorer som kan trekke ut typeinformasjonen fra hintene.

De typing modul

Typemodulen inneholder typer som er utformet for å støtte typetips. Hvorfor ikke bare bruke eksisterende Python-typer som int, str, list og dict? Du kan definitivt bruke disse typene, men på grunn av Pythons dynamiske skriving, utover grunnleggende typer, får du ikke mye informasjon. Hvis du for eksempel vil angi at et argument kan være en kartlegging mellom en streng og et heltall, kan du ikke gjøre det med standard Python-typer. Med skrivemodulen er det like enkelt som:

python kartlegging [str, int]

La oss se på et mer komplett eksempel: en funksjon som tar to argumenter. En av dem er en liste over ordbøker hvor hver ordbok inneholder nøkler som er strenger og verdier som er heltall. Det andre argumentet er enten en streng eller et heltall. Typemodulen gir nøyaktige spesifikasjoner for slike kompliserte argumenter.

"python fra å skrive importliste, dikt, union

def [f] (a: Liste [Dikt [str, int]], b: Union [str, int]) -> int: "" "Skriv ut en liste over ordbøker og returner antall ordbøker" str): b = int (b) for jeg i rekkevidde (b): print (a)

x = [dikt (a = 1, b = 2), dikt (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] "

Nyttige typer

La oss se noen av de mer interessante typene fra skrivemodulen.

Den Callable-typen lar deg spesifisere funksjonen som kan sendes som argumenter eller returneres som et resultat, siden Python behandler funksjoner som førsteklasses borgere. Syntaksen for callables er å gi en rekke argumenttyper (igjen fra skrivemodulen) etterfulgt av en returverdi. Hvis det er forvirrende, er dette et eksempel:

"python def do_something_fancy (data: Set [float], on_error: Callable [[Unntak, int], Ingen]): ...

"

On_error callback-funksjonen er spesifisert som en funksjon som tar en unntak og et heltall som argumenter og returnerer ingenting.

Enhver type betyr at en statisk type kontroller skal tillate enhver operasjon så vel som tildeling til en hvilken som helst annen type. Hver type er en undertype av Any.

Unionens type du så tidligere er nyttig når et argument kan ha flere typer, noe som er veldig vanlig i Python. I følgende eksempel er verify_config () funksjonen godtar et config-argument, som kan enten være en Config-objekt eller et filnavn. Hvis det er et filnavn, kalles det en annen funksjon for å analysere filen i et Config-objekt og returnere det.

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

def parse_config_file (filnavn: str) -> Config: ...

"

Den valgfrie typen betyr at argumentet kan være None too. Valgfritt [T] tilsvarer Union [T, None]

Det er mange flere typer som angir ulike evner som Iterable, Iterator, Reversible, SupportsInt, SupportsFloat, Sequence, MutableSequence og IO. Sjekk ut dokumentasjonen til typemodulen for hele listen.

Det viktigste er at du kan spesifisere typen argumenter på en veldig finkornet måte som støtter Python-typen på høy troskap og gjør det mulig for generiske og abstrakte grunnklasser også.

Forward Referanser

Noen ganger vil du referere til en klasse i en type hint innenfor en av metodene sine. For eksempel, la oss anta at klasse A kan utføre litt fletteoperasjon som tar en annen forekomst av A, fusjonerer med seg selv og returnerer resultatet. Her er et naivt forsøk på å bruke typehint for å spesifisere det:

"python klasse A: def fusjon (andre: A) -> A: ...

 1 klasse A: ----> 2 def fusjon (andre: A = Ingen) -> A: 3 ... 4 

NameError: navn 'A' er ikke definert "

Hva skjedde? Klassen A er ikke definert ennå, når typespissen for sin sammenføyning () -metode er sjekket av Python, slik at klasse A ikke kan brukes på dette punktet (direkte). Løsningen er ganske enkel, og jeg har sett den brukt tidligere av SQLAlchemy. Du angir bare typen hint som en streng. Python vil forstå at det er en forward referanse og vil gjøre det riktige:

python klasse A: def merge (andre: 'A' = Ingen) -> 'A': ...

Skriv aliaser

Én ulempe ved bruk av typehint for lange typespesifikasjoner er at det kan koble koden og gjøre den mindre lesbar, selv om den gir mye informasjon om typen. Du kan alias typer akkurat som alle andre objekter. Det er så enkelt som:

"python Data = Dikt [int, Sequence [Dict [str, Valgfritt [Liste [flyt]]]]

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

De get_type_hints () Hjelperfunksjon

Typemodulen gir get_type_hints () -funksjonen, som gir informasjon om argumenttyper og returverdi. Mens merknader Attributt returnerer type tips fordi de bare er merknader, jeg anbefaler fortsatt at du bruker funksjonen get_type_hints () fordi den løser frem referanser. Også, hvis du angir en standard av None til en av argumentene, returnerer funksjonen get_type_hints () automatisk sin type som Union [T, NoneType] hvis du bare angav T. La oss se forskjellen ved å bruke A.merge () -metoden tidligere definert:

"python print (A.merge.merknader)

'andre': 'A', 'return': 'A' "

De merknader Attributt returnerer bare annoteringsverdien som det er. I dette tilfellet er det bare strenget 'A' og ikke A-klasseobjektet, som 'A' bare er en referanse for.

"python print (get_type_hints (A.merge))

'komme tilbake': hoved-.A '>,' other ': typing.Union [hoved-.A, NoneType] "

Funksjonen get_type_hints () konverterte typen av annen argument til en Union of A (klassen) og NoneType på grunn av Ingen standard argument. Returtypen ble også konvertert til klasse A.

Dekoratørene

Type hint er en spesialisering av funksjonsannonser, og de kan også fungere side om side med annen funksjonsannotasjon.

For å gjøre det, gir skrivemodulen to dekoratører: @no_type_check og @no_type_check_decorator. De @no_type_check dekoratør kan brukes til enten en klasse eller en funksjon. Det legger til no_type_check Tilordne funksjonen (eller hver metode i klassen). På denne måten vil typen av brikker vite at de ignorerer merknader, som ikke er typen hint.

Det er litt tungvint fordi fordi du skriver et bibliotek som vil bli brukt i stor grad, må du anta at en type kontroller vil bli brukt, og hvis du vil annotere dine funksjoner med ikke-type hint, må du også dekorere dem med @no_type_check.

Et vanlig scenario ved bruk av regelmessige funksjonskommentarer er også å ha en dekoratør som opererer over dem. Du vil også slå av typen kontroll i dette tilfellet. Ett alternativ er å bruke @no_type_check dekoratør i tillegg til dekoratøren din, men det blir gammelt. I stedet er det @no_Type_check_decorator kan brukes til å dekorere dekoratøren din slik at den også oppfører seg som @no_type_check (legger til no_type_check Egenskap).

La meg illustrere alle disse konseptene. Hvis du prøver å få_type_hint () (som en hvilken som helst type kontroller vil gjøre) på en funksjon som er annotert med en vanlig strengannotasjon, vil get_type_hints () tolke den som en forward-referanse:

"python def f (a:" noen merknad "): pass

print (get_type_hints (f))

SyntaxError: ForwardRef må være et uttrykk - har noen kommentar

For å unngå det, legg til @no_type_check dekoratoren, og get_type_hints returnerer bare en tom dikt, mens __annotations__ attributt returnerer merknadene:

"python @no_type_check def f (a:" noen annotasjon "): pass

skriv ut (get_type_hints (f))

print (f.merknader) 'a': 'noen annotasjon' "

Nå, anta at vi har en dekoratør som skriver ut kommentarene dikt. Du kan dekorere den med @no_Type_check_decorator og deretter dekorere funksjonen og ikke bekymre deg for noen type sjekker ringer get_type_hints () og bli forvirret. Dette er trolig en god praksis for alle dekoratører som opererer på kommentarer. Ikke glem det @ functools.wraps, ellers vil merknadene ikke kopieres til den dekorerte funksjonen, og alt vil falle fra hverandre. Dette er dekket i detalj i Python 3 Function Annotations.

python @no_type_check_decorator def print_annotations (f): @ functools.wraps (f) def dekorert (* args, ** kwargs): print (f .__ annotations__) returnere f (* args, ** kwargs)

Nå kan du dekorere funksjonen bare med @print_annotations, og når det blir kalt, vil det skrive ut sine kommentarer.

"python @print_annotations def f (a:" noen annotasjon "): pass

f (4) 'a': 'noen merknad' "

ringe get_type_hints () er også trygt og returnerer en tom dikt.

python print (get_type_hints (f))

Statisk analyse med Mypy

Mypy er en statisk type kontroller som var inspirasjonen til type hint og typemodulen. Guido van Rossum selv er forfatteren av PEP-483 og en medforfatter av PEP-484.

Installerer Mypy

Mypy er i veldig aktiv utvikling, og med denne skrivingen er pakken på PyPI utdatert og fungerer ikke med Python 3.5. For å bruke Mypy med Python 3.5, få det siste fra Mypy's repository på GitHub. Det er så enkelt som:

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

Spiller med Mypy

Når du har installert Mypy, kan du bare kjøre Mypy på programmene dine. Følgende program definerer en funksjon som forventer en liste over strenger. Det påkaller så funksjonen med en liste over heltall.

"python fra å skrive importliste

def case_insensitive_dedupe (data: List [str]): "" "Konverterer alle verdier til små bokstaver og fjerner duplikater" "" returliste (sett (x.lower () for x i data))

skriv ut (case_insensitive_dedupe ([1, 2])) "

Når du kjører programmet, mislykkes det tydeligvis ved kjøring med følgende feil:

ren python3 dedupe.py Traceback (siste samtalen sist): Fil "dedupe.py", linje 8, i Skriv ut (case_insensitive_dedupe) ("1, 2, 3")) Fil "dedupe.py", linje 5, i tilfelle_insensitive_dedupe returliste (sett (x.lower () for x i data)) Fil "dedupe.py", linje 5 , i returliste (sett (x.lower () for x i data)) AttributeError: 'int' -objektet har ingen attributt 'lavere'

Hva er problemet med det? Problemet er at det ikke er klart umiddelbart selv i dette svært enkle tilfellet hva årsaken er. Er det et input type problem? Eller kanskje selve koden er feil, og bør ikke prøve å ringe Nedre() metode på 'int' objektet. Et annet problem er at hvis du ikke har 100% testdekning (og la oss være ærlige, ingen av oss gjør), så kan slike problemer lurke på noen uprøvde, sjelden brukte kodeveier og bli oppdaget i verste fall i produksjonen.

Statisk maskinskriving, støttet av typen hint, gir deg et ekstra sikkerhetsnett ved å sørge for at du alltid ringer til dine funksjoner (merket med type hint) med de riktige typene. Her er resultatet av Mypy:

ren (N)> mypy dedupe.py dedupe.py:8: feil: Listepost 0 har inkompatibel type "int" dedupe.py:8: feil: Listepost 1 har inkompatibel type "int" dedupe.py:8: feil : Liste element 2 har inkompatibel type "int"

Dette er rettferdig, peker direkte på problemet, og krever ikke å kjøre mange tester. En annen fordel ved kontroll av statisk type er at hvis du forplikter seg til det, kan du hoppe over dynamisk typekontroll, bortsett fra når du analyserer ekstern inngang (leser filer, innkommende nettverksforespørsler eller brukerinngang). Det bygger også mye tillit så langt som refactoring går.

Konklusjon

Skriv tips og skrivemodulen er helt valgfrie tillegg til Pythons uttrykksevne. Selv om de kanskje ikke passer til alles smak, for store prosjekter og store lag, kan de være uunnværlige. Beviset er at store lag allerede bruker statisk typekontroll. Denne typen informasjon er standardisert, det vil være lettere å dele kode, verktøy og verktøy som bruker den. IDEer som PyCharm benytter allerede den til å gi en bedre utvikleropplevelse.