Python 3 Type Hints en Statische Analyse

Python 3.5 introduceerde de nieuwe typemodule die standaard bibliotheekondersteuning biedt voor het gebruik van functieannotaties voor optionele typehints. Dat opent de deur naar nieuwe en interessante tools voor statische typecontrole zoals mypy en in de toekomst mogelijk geautomatiseerde typografische optimalisatie. Type hints worden gespecificeerd in PEP-483 en PEP-484.

In deze tutorial onderzoek ik de mogelijkheden die hints presenteren en laat je zien hoe je mypy gebruikt om statistisch je Python-programma's te analyseren en de kwaliteit van je code aanzienlijk te verbeteren..

Typ Hints

Typehints worden bovenop functieaantekeningen gebouwd. Kort samengevat, annotaties van functies laten u aantekeningen maken van de argumenten en de geretourneerde waarde van een functie of methode met willekeurige metadata. Typehints zijn een speciaal geval van functieannotaties die functieargumenten en de retourwaarde met standaardtype-informatie specifiek annoteren. Functieannotaties in het algemeen en typetips in het bijzonder zijn volledig optioneel. Laten we een snel voorbeeld bekijken:

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

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

De argumenten zijn geannoteerd met zowel hun type als de retourwaarde. Maar het is van cruciaal belang om te beseffen dat Python dit volledig negeert. Het maakt de type-informatie beschikbaar via de annotaties attribuut van het functieobject, maar dat is het zo'n beetje.

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

Om te controleren of Python echt de typeaanwijzingen negeert, laten we de typehints volledig verpesten:

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

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

Zoals u kunt zien, gedraagt ​​de code zich hetzelfde, ongeacht de hints van het type.

Motivatie voor typehints

OK. Type hints zijn optioneel. Type hints worden volledig genegeerd door Python. Waar gaat het dan om? Welnu, er zijn verschillende goede redenen:

  • statische analyse
  • IDE-ondersteuning
  • standaard documentatie

Ik duik later in een statische analyse met Mypy. IDE-ondersteuning is al gestart met ondersteuning door PyCharm 5 voor typehints. Standaarddocumentatie is geweldig voor ontwikkelaars die eenvoudig het type argumenten kunnen achterhalen en de waarde kunnen teruggeven door alleen naar een handtekening van een functie te kijken, evenals geautomatiseerde documentatie-generatoren die de typegegevens uit de hints kunnen halen.

De typen module

De typemodule bevat typen die ontworpen zijn om typeaanwijzingen te ondersteunen. Waarom niet alleen bestaande Python-typen zoals int, str, list en dict gebruiken? Je kunt deze typen zeker gebruiken, maar door het dynamische typen van Python krijg je naast basistypen niet veel informatie. Als u bijvoorbeeld wilt opgeven dat een argument een toewijzing tussen een tekenreeks en een geheel getal is, kunt u dit niet doen met standaard Python-typen. Met de typemodule is het net zo eenvoudig als:

python Mapping [str, int]

Laten we naar een completer voorbeeld kijken: een functie die twee argumenten vereist. Een daarvan is een lijst met woordenboeken waarin elk woordenboek sleutels bevat die tekenreeksen en waarden zijn die gehele getallen zijn. Het andere argument is een tekenreeks of een geheel getal. De typemodule biedt nauwkeurige specificaties van dergelijke gecompliceerde argumenten.

"python van het importeren van de lijst, Dict, Union

def foo (a: Lijst [Dict [str, int]], b: Union [str, int]) -> int: "" "Druk een lijst met woordenboeken af ​​en retourneer het aantal woordenboeken" "" if isinstance (b, str): b = int (b) voor i in bereik (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] "

Handige soorten

Laten we enkele van de meer interessante typen uit de typemodule bekijken.

Met het type Callable kun je de functie opgeven die als argument kan worden doorgegeven of als resultaat kan worden geretourneerd, aangezien Python functies behandelt als eersteklas burgers. De syntaxis voor callables is om een ​​array met argumenttypen te bieden (opnieuw vanuit de typemodule) gevolgd door een geretourneerde waarde. Als dat verwarrend is, is hier een voorbeeld:

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

"

De on_error callback-functie is opgegeven als een functie die een uitzondering en een geheel getal als argumenten neemt en niets retourneert.

Het willekeurige type betekent dat een statisch type controleur elke bewerking moet toestaan, evenals toewijzing aan een ander type. Elk type is een subtype van Any.

Het Union-type dat u eerder zag, is handig wanneer een argument meerdere typen kan hebben, wat heel gebruikelijk is in Python. In het volgende voorbeeld, de verify_config () functie accepteert een configargument, dat een Config-object of een bestandsnaam kan zijn. Als het een bestandsnaam is, roept deze een andere functie op om het bestand te ontleden in een Config-object en het terug te sturen.

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

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

"

Het optionele type betekent dat het argument ook Geen kan zijn. Optioneel [T] is gelijk aan Union [T, None]

Er zijn veel meer typen die verschillende capaciteiten aanduiden, zoals Iterable, Iterator, Reversible, SupportsInt, SupportsFloat, Sequence, MutableSequence en IO. Bekijk de documentatie van de typemodule voor de volledige lijst.

Het belangrijkste is dat je het type argumenten op een zeer fijnmazige manier kunt specificeren dat het Python-type systeem op een high fidelity ondersteunt en generieke en abstracte basisklassen ook mogelijk maakt.

Voorwaartse verwijzingen

Soms wilt u verwijzen naar een klasse in een hint van een type binnen een van de methoden. Laten we bijvoorbeeld aannemen dat klasse A een bepaalde samenvoegbewerking kan uitvoeren die nog een exemplaar van A neemt, samenvoegt met zichzelf en het resultaat retourneert. Hier is een naïeve poging om typehints te gebruiken om het te specificeren:

"python klasse A: def samenvoegen (andere: A) -> A: ...

 1 klasse A: ----> 2 def samenvoegen (andere: A = Geen) -> A: 3 ... 4 

NameError: naam 'A' is niet gedefinieerd "

Wat is er gebeurd? De klasse A is nog niet gedefinieerd wanneer de typetips voor de samenvoegings () -methode worden gecontroleerd door Python, dus de klasse A kan op dit moment (rechtstreeks) niet worden gebruikt. De oplossing is vrij eenvoudig en ik heb hem eerder door SQLAlchemy gebruikt. U geeft gewoon de type-hint op als een tekenreeks. Python zal begrijpen dat het een referentie is en zal het juiste doen:

python klasse A: def samenvoegen (andere: 'A' = Geen) -> 'A': ...

Typ Aliassen

Een nadeel van het gebruik van typeaanwijzingen voor lange typespecificaties is dat het de code kan vervuilen en het minder leesbaar maken, zelfs als het veel type-informatie biedt. U kunt alias typen net als elk ander object. Het is zo simpel als:

"python Data = Dict [int, Sequence [Dict [str, Optioneel [Lijst [float]]]]

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

De get_type_hints () Helper-functie

De typemodule biedt de functie get_type_hints (), die informatie biedt over de argumenttypen en de retourwaarde. Terwijl de annotaties attribuut geeft type hints terug omdat het gewoon annotaties zijn, ik raad nog steeds aan om de functie get_type_hints () te gebruiken omdat het doorverwijzingen oplost. Als u een standaard van None opgeeft voor een van de argumenten, wordt de functie get_type_hints () automatisch teruggestuurd als Union [T, NoneType] als u zojuist T hebt opgegeven. Laten we het verschil zien met de methode A.merge () eerder gedefinieerd:

"python print (A.merge.annotaties)

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

De annotaties attribuut retourneert eenvoudig de annotatiewaarde zoals deze is. In dit geval is het alleen het teken 'A' en niet het object A-klasse, waarbij 'A' slechts een verwijzing naar voren is.

"python print (get_type_hints (A.merge))

'Terug': hoofd.A '>,' other ': typing.Union [hoofd.A, NoneType] "

De functie get_type_hints () heeft het type geconverteerd anders argument voor een Union of A (de klasse) en NoneType vanwege het standaard-argument None. Het retourtype is ook geconverteerd naar de klasse A.

De decorateurs

Typetips zijn een specialisatie van functieannotaties en ze kunnen ook naast andere annotaties van functies werken.

Om dit te doen, biedt de typemodule twee decorateurs: @no_type_check en @no_type_check_decorator. De @no_type_check decorateur kan op een klasse of een functie worden toegepast. Het voegt de toe no_type_check attribuut aan de functie (of aan elke methode van de klasse). Op deze manier weten checkers de annotaties te negeren, dit zijn geen typehints.

Het is een beetje omslachtig, want als u een bibliotheek schrijft die breed wordt gebruikt, moet u ervan uitgaan dat een typecontrole zal worden gebruikt en als u uw functies annoteert met niet-typerende hints, moet u ze ook versieren met @no_type_check.

Een veelvoorkomend scenario bij het gebruik van reguliere functieannotaties is ook om een ​​decorateur te hebben die erover werkt. U wilt in dit geval ook de typecontrole uitschakelen. Een optie is om de @no_type_check decorateur naast je binnenhuisarchitect, maar dat wordt oud. In plaats daarvan, de @no_Type_check_decorator kan worden gebruikt om uw binnenhuisarchitect in te richten, zodat deze zich ook gedraagt @no_type_check (voegt de no_type_check attribuut).

Laat me al deze concepten illustreren. Als u probeert get_type_hint () te krijgen (zoals elk type controleur zal doen) op een functie die is geannoteerd met een reguliere stringannotatie, zal get_type_hints () dit interpreteren als een forward-verwijzing:

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

drukken (get_type_hints (f))

SyntaxError: ForwardRef moet een expressie zijn - heeft 'een annotatie'

Om dit te voorkomen, voeg je de decoratiemaker @no_type_check toe en krijg get_type_hints gewoon een leeg dictaat, terwijl de __annotations__ attribuut levert de annotaties op:

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

print (get_type_hints (f))

druk (f.annotaties) 'a': 'some annotation' "

Stel dat we een binnenhuisarchitect hebben die de annotaties dicteert. Je kunt het versieren met de @no_Type_check_decorator en versier dan de functie en maak je geen zorgen over een type checker die get_type_hints () aanroept en verward raakt. Dit is waarschijnlijk een beste methode voor elke decorateur die werkt op annotaties. Vergeet het @ functools.wraps, anders worden de annotaties niet gekopieerd naar de ingerichte functie en valt alles uit elkaar. Dit wordt uitgebreid behandeld in Python 3 Function Annotations.

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

Nu kunt u de functie alleen maar versieren met @print_annotations, en wanneer het wordt genoemd, worden de annotaties afgedrukt.

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

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

Roeping get_type_hints () is ook veilig en geeft een leeg dictaat terug.

python print (get_type_hints (f))

Statische analyse met Mypy

Mypy is een statische type checker die de inspiratie was voor type-hints en de typemodule. Guido van Rossum is zelf de auteur van PEP-483 en co-auteur van PEP-484.

Mypy installeren

Mypy is in een zeer actieve ontwikkeling en vanaf dit moment is het pakket op PyPI verouderd en werkt het niet met Python 3.5. Om Mypy te gebruiken met Python 3.5, haal het laatste nieuws uit de repository van Mypy op GitHub. Het is zo simpel als:

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

Spelen met Mypy

Zodra Mypy is geïnstalleerd, kun je Mypy gewoon uitvoeren in je programma's. Het volgende programma definieert een functie die een lijst met strings verwacht. Vervolgens wordt de functie aangeroepen met een lijst met gehele getallen.

"python van het importeren van de lijst

def case_insensitive_dedupe (data: Lijst [str]): "" "Converteert alle waarden naar kleine letters en verwijdert dubbele" "" teruggave lijst (set (x.lower () voor x in data))

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

Bij het uitvoeren van het programma mislukt het uiteraard tijdens runtime met de volgende fout:

plain python3 dedupe.py Traceback (meest recente oproep laatste): Bestand "dedupe.py", regel 8, in print (case_insensitive_dedupe ([1, 2, 3])) Bestand "dedupe.py", regel 5, in case_insensitive_dedupe retourlijst (set (x.lower () voor x in data)) Bestand "dedupe.py", regel 5 , in teruggave lijst (set (x.lower () voor x in data)) AttributeError: 'int' object heeft geen attribuut 'lager'

Wat is het probleem daarmee? Het probleem is dat het niet meteen duidelijk is, zelfs in dit zeer eenvoudige geval, wat de oorzaak is. Is het een probleem met het invoertype? Of misschien is de code zelf verkeerd en zou niet moeten proberen de code te bellen lager() methode op het 'int'-object. Een ander probleem is dat als je geen 100% testbereik hebt (en laten we eerlijk zijn, niemand van ons dat doet), dan kunnen dergelijke problemen op een ongeteste, zelden gebruikt codepad liggen en op het slechtste moment in de productie worden gedetecteerd.

Statisch typen, geholpen door type-hints, geeft u een extra vangnet door ervoor te zorgen dat u altijd uw functies (geannoteerd met type-hints) met de juiste typen oproept. Hier is de uitvoer van Mypy:

plain (N)> mypy dedupe.py dedupe.py:8: fout: Lijstitem 0 heeft incompatibel type "int" dedupe.py:8: fout: Lijstitem 1 heeft incompatibel type "int" dedupe.py:8: fout : Lijstitem 2 heeft incompatibel type "int"

Dit is eenvoudig, wijst rechtstreeks op het probleem en vereist niet veel tests. Een ander voordeel van statische typecontrole is dat als u zich eraan verbindt, u dynamische typecontrole overslaat, behalve bij het parseren van externe invoer (lezen van bestanden, binnenkomende netwerkverzoeken of gebruikersinvoer). Het bouwt ook veel vertrouwen op voor wat refactoring betreft.

Conclusie

Type hints en de typemodule zijn volledig optionele toevoegingen aan de expressiviteit van Python. Hoewel ze misschien niet voor iedereen geschikt zijn, kunnen ze voor grote projecten en grote teams onmisbaar zijn. Het bewijs is dat grote teams al gebruik maken van statische typecontrole. Nu dat type informatie gestandaardiseerd is, zal het eenvoudiger zijn om code, hulpprogramma's en hulpmiddelen die het gebruiken te delen. IDE's zoals PyCharm maken hier al gebruik van om een ​​betere ontwikkelaarervaring te bieden.