Python 3 Typhints och Statisk Analys

Python 3.5 introducerade den nya typmodulen som tillhandahåller standardbiblioteksstöd för användaranmärkningar för valfria typhints. Det öppnar dörren till nya och intressanta verktyg för statisk typkontroll som mypy och i framtiden möjligen automatiserad typbaserad optimering. Typtips anges i PEP-483 och PEP-484.

I denna handledning utforskar jag de möjligheter som typtips presenterar och visar dig hur du använder mypy för att statiskt analysera dina Python-program och förbättra kvaliteten på din kod.

Skriv tips

Typtips byggs ovanpå funktionsannonser. I korthet kan funktionskommentarer kommentera argumenten och returvärdet för en funktion eller metod med godtycklig metadata. Typtips är ett speciellt fall med funktionsanmärkningar som specifikt annoterar funktionsargument och returvärdet med standard typinformation. Funktionsannonser i allmänhet och typtips är särskilt frivilliga. Låt oss ta en titt på ett snabbt exempel:

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

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

Argumenten antecknades med deras typ samt returvärdet. Men det är viktigt att inse att Python ignorerar detta helt. Det gör typinformationen tillgänglig via annoteringar attributet till funktionsobjektet, men det handlar om det.

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

För att verifiera att Python verkligen ignorerar typtipsen, låt oss helt röra upp typtipsen:

"python def reverse_slice (text: float, start: str, slutet: bool) -> dict: return text [start: slutet] [:: - 1]

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

Som du kan se fungerar uppgiften densamma, oavsett typtips.

Motivation för typtips

OK. Typtips är valfria. Typhypotesen ignoreras helt av Python. Vad är meningen med dem då? Tja, det finns flera bra skäl:

  • statisk analys
  • IDE-stöd
  • standard dokumentation

Jag dyker in i statisk analys med Mypy senare. IDE-stöd har redan börjat med PyCharm 5: s stöd för typhints. Standarddokumentation är utmärkt för utvecklare som enkelt kan räkna ut typ av argument och returnera värde bara genom att titta på en funktionssignatur samt automatiserade dokumentationsgeneratorer som kan extrahera typinformationen från hinten.

De skriver Modul

Typmodulen innehåller typer som är utformade för att stödja typtips. Varför inte bara använda befintliga Python typer som int, str, lista och dict? Du kan definitivt använda dessa typer, men på grund av Pythons dynamiska typning, utöver grundläggande typer får du inte mycket information. Om du till exempel vill ange att ett argument kan vara en kartläggning mellan en sträng och ett heltal, kan du inte göra det med standard Python-typer. Med typmodulen är det lika enkelt som:

python kartläggning [str, int]

Låt oss titta på ett mer komplett exempel: en funktion som tar två argument. En av dem är en lista över ordböcker där varje ordbok innehåller nycklar som är strängar och värden som är heltal. Det andra argumentet är antingen en sträng eller ett heltal. Typmodulen tillåter exakta specifikationer för sådana komplicerade argument.

"python från att skriva importlista, Dict, Union

def foo (a: Lista [Dict [str, int]], b: Union [str, int]) -> int: "" "Skriv ut en lista med ordböcker och returnera antalet ordböcker" "" om instans (b, str): b = int (b) för jag i intervall (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] "

Användbara typer

Låt oss se några av de mer intressanta typerna från skrivmodulen.

Den röstbara typen kan du ange vilken funktion som kan överföras som argument eller returneras som ett resultat, eftersom Python behandlar funktioner som förstklassiga medborgare. Synkronen för callables är att tillhandahålla en rad argumenttyper (igen från skrivmodulen) följt av ett returvärde. Om det är förvirrande, är det här ett exempel:

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

"

On_error callback-funktionen anges som en funktion som tar en Undantag och ett heltal som argument och returnerar ingenting.

Vilken typ som helst betyder att en statisk typkontroll ska tillåta vilken operation som helst som tilldelas någon annan typ. Varje typ är en undertyp av Any.

Unionen du såg tidigare är användbar när ett argument kan ha flera typer, vilket är mycket vanligt i Python. I följande exempel är verify_config () funktionen accepterar ett config-argument, vilket kan vara antingen ett Config-objekt eller ett filnamn. Om det är ett filnamn, kallas det en annan funktion för att analysera filen i ett Config-objekt och returnera det.

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

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

"

Den valfria typen betyder att argumentet kan vara None too. Tillval [T] är ekvivalent med Union [T, None]

Det finns många fler typer som anger olika funktioner som Iterable, Iterator, Reversible, SupportsInt, SupportsFloat, Sequence, MutableSequence och IO. Kolla in dokumentationen för typmodul för hela listan.

Huvuddelen är att du kan ange typen av argument på ett mycket fint kornat sätt som stöder Python-typsystemet med hög trovärdighet och möjliggör också generiska och abstrakta basklasser.

Framåtreferenser

Ibland vill du hänvisa till en klass i en typtips inom en av dess metoder. Låt oss anta att klass A kan utföra en viss sammanslagningsoperation som tar en annan instans av A, fusionerar med sig själv och returnerar resultatet. Här är ett naivt försök att använda typtips för att ange det:

"python klass A: def merge (andra: A) -> A: ...

 1 klass A: ----> 2 def fusion (annan: A = Ingen) -> A: 3 ... 4 

NameError: namn 'A' är inte definierat "

Vad hände? Klass A är inte definierad ännu när typtipset för dess sammanslagna () -metod kontrolleras av Python, så att klass A inte kan användas vid denna punkt (direkt). Lösningen är ganska enkel, och jag har sett den som tidigare använts av SQLAlchemy. Du anger bara typtipset som en sträng. Python förstår att det är en framåtriktad referens och kommer att göra det rätta:

python klass A: def merge (andra: 'A' = Ingen) -> 'A': ...

Skriv alias

En nackdel med att använda typtips för långa typspecifikationer är att det kan störa koden och göra den mindre läsbar, även om den ger mycket typinformation. Du kan aliasstyper precis som alla andra objekt. Det är så enkelt som:

"python Data = Dikt [int, sekvens [Dict [str, Valfritt [Lista [float]]]]

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

De get_type_hints () Hjälperfunktion

Typmodulen tillhandahåller get_type_hints () -funktionen, som ger information om argumenttyperna och returvärdet. Medan annoteringar attribut returnerar typ tips eftersom de bara är anteckningar, rekommenderar jag fortfarande att du använder funktionen get_type_hints () eftersom den löser fram referenser. Om du anger en standard av None till ett av argumenten, returnerar funktionen get_type_hints () automatiskt sin typ som Union [T, NoneType] om du bara angav T. Låt oss se skillnaden med metoden A.merge () definierad tidigare:

"python print (A.merge.annoteringar)

'other': 'A', 'return': 'A' "

De annoteringar attributet returnerar bara annoteringsvärdet som det är. I det här fallet är det bara strängen 'A' och inte A-klassobjektet, till vilket 'A' bara är en framåtriktad referens.

"python print (get_type_hints (A.merge))

'lämna tillbaka': huvud.A '>,' other ': typing.Union [huvud.A, NoneType] "

Funktionen get_type_hints () konverterade typen av andra argument till en union av A (klassen) och NoneType på grund av Ingen standardargument. Returtypen omvandlades också till klass A.

Decoratorsna

Typtips är en specialisering av funktionsanmärkningar, och de kan också fungera sida vid sida med annan funktionsannotation.

För att göra det ger skrivningsmodulen två dekoratörer: @no_type_check och @no_type_check_decorator. De @no_type_check dekoratorn kan appliceras på antingen en klass eller en funktion. Det lägger till no_type_check attribut till funktionen (eller varje metod i klassen). På så sätt kan typkontrollers känna att ignorera noteringar, vilka inte är typtips.

Det är lite omständligt eftersom om du skriver ett bibliotek som används i stor utsträckning, måste du anta att en typkontroll kommer att användas, och om du vill annotera dina funktioner med tips av andra slag, måste du också dekorera dem med @no_type_check.

Ett vanligt scenario när man använder regelbundna funktionskommentarer är också att ha en dekoratör som arbetar över dem. Du vill också stänga av typkontroll i det här fallet. Ett alternativ är att använda @no_type_check dekoratör förutom din dekoratör, men det blir gammalt. Istället är det @no_Type_check_decorator kan användas för att dekorera din dekoratör så att den också beter sig @no_type_check (lägger till no_type_check attribut).

Låt mig illustrera alla dessa begrepp. Om du försöker få_type_hint () (som vilken typ av kontrollör som helst) på en funktion som är annoterad med en vanlig strängannotation, kommer get_type_hints () att tolka den som en framåtriktad referens:

"python def f (a:" lite anteckning "): passera

trycket (get_type_hints (f))

SyntaxError: ForwardRef måste vara ett uttryck - fick en del anteckning "

För att undvika det, lägg till @no_type_check decoratorn, och get_type_hints returnerar helt enkelt en tom dikt, medan __annotations__ attribut returnerar anmärkningarna:

"python @no_type_check def f (a:" lite anteckning "): passera

skriva ut (get_type_hints (f))

trycket (f.annoteringar) 'a': 'lite anteckning' "

Anta nu att vi har en dekoratör som skriver ut anteckningarna dikt. Du kan dekorera den med @no_Type_check_decorator och sedan dekorera funktionen och inte oroa dig för någon typ av kontroller som kallar get_type_hints () och blir förvirrad. Detta är förmodligen en bra praxis för alla dekoratörer som arbetar på anteckningar. Glöm inte @ functools.wraps, Annars kommer inte anteckningarna att kopieras till den inredda funktionen och allt kommer att falla ifrån varandra. Detta beskrivs i detalj i Python 3 Function Annotations.

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

Nu kan du dekorera funktionen bara med @print_annotations, och när det kallas kommer det att skriva ut sina kommentarer.

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

f (4) 'a': 'lite anteckning' "

Kallelse get_type_hints () är också säker och returnerar en tom dikt.

python print (get_type_hints (f))

Statisk analys med Mypy

Mypy är en statisk typkontroll som var inspirationen för typtips och typmodulen. Guido van Rossum själv är författare till PEP-483 och en medförfattare av PEP-484.

Installera Mypy

Mypy är i väldigt aktiv utveckling, och med denna skrivning är paketet på PyPI föråldrat och fungerar inte med Python 3.5. För att använda Mypy med Python 3.5, få det senaste från Mypys arkiv på GitHub. Det är så enkelt som:

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

Spelar med Mypy

När du har installerat Mypy kan du bara köra Mypy på dina program. Följande program definierar en funktion som förväntar sig en lista med strängar. Den anropar sedan funktionen med en lista med heltal.

"python från att skriva importlistan

def case_insensitive_dedupe (data: List [str]): "" "Konverterar alla värden till små bokstäver och tar bort dubbletter" "" returlista (set (x.lower () för x i data))

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

När programmet körs misslyckas det uppenbarligen vid körning med följande fel:

plain python3 dedupe.py Traceback (senaste samtal senast): Filen "dedupe.py", rad 8, i Skriv ut (case_insensitive_dedupe) [Filen "dedupe.py", rad 5, i case_insensitive_dedupe returnera listan (set (x.lower () för x i data)) Filen "dedupe.py", rad 5 , i returlista (set (x.lower () för x i data)) AttributeError: 'int' -objekt har inget attribut 'lägre'

Vad är problemet med det? Problemet är att det inte är klart omedelbart även i det här mycket enkla fallet vad orsaken är. Är det ett inmatningstypproblem? Eller kanske själva koden är fel och borde inte försöka ringa lägre() metod på "int" -objektet. En annan fråga är att om du inte har 100% test täckning (och låt oss vara ärliga, ingen av oss gör), då kan sådana problem lura i någon otestad, sällan använd kodväg och detekteras vid värsta tid i produktion.

Statisk typing, med hjälp av typtips, ger dig ett extra säkerhetsnät genom att se till att du alltid ringer upp dina funktioner (antecknad med typtips) med rätt typ. Här är utmatningen från Mypy:

plain (N)> mypy dedupe.py dedupe.py:8: fel: Listobjekt 0 har inkompatibel typ "int" dedupe.py:8: fel: Listobjekt 1 har inkompatibel typ "int" dedupe.py:8: fel : Listobjekt 2 har inkompatibel typ "int"

Detta är enkelt, pekar direkt på problemet, och kräver inte att du kör många test. En annan fördel med att kontrollera statisk typ är att om du åtar dig, kan du hoppa över dynamisk typkontroll, utom när du analyserar extern ingång (läsfiler, inkommande nätverksförfrågningar eller användarinmatning). Det bygger också mycket förtroende så långt som refactoring går.

Slutsats

Typtips och typmodulen är helt valfria tillägg till Pythons uttrycksförmåga. Medan de kanske inte passar allas smak, för stora projekt och stora lag kan de vara oumbärliga. Beviset är att stora lag redan använder statisk typkontroll. Nu är typinformationen standardiserad, det blir enklare att dela kod, verktyg och verktyg som använder den. IDEs som PyCharm utnyttjar redan det för att ge en bättre utvecklarupplevelse.