Python
Ce face o adnotare de tip la rularea unui program Python
Pe scurt
Aproape nimic. O adnotare este o expresie pe care Python o evaluează când se execută def-ul sau corpul clasei, o stochează într-un dicționar __annotations__ de pe obiect și apoi nu o mai consultă niciodată — apelarea unei funcții nu verifică nicio adnotare. Genericele parametrizate, precum list[float], construiesc un obiect types.GenericAlias care poartă o origine și niște argumente, dar niciun element nu este vreodată inspectat, iar isinstance refuză din start un generic parametrizat. Un program separat — mypy, pyright, editorul tău — citește aceleași adnotări ca pe o specificație, fără să ruleze codul, iar acea împărțire a muncii este tot designul.
Cum funcționează
O adnotare este Python obișnuit. def quote_freight(weight_kg: float) -> float: evaluează float de două ori la momentul definirii și depune rezultatele în quote_freight.__annotations__. Bytecodeul care apelează funcția este identic cu cel al uneia neadnotate: nicio conversie, nicio validare, nicio ramificare. Adnotările sunt metadate prinse pe un obiect, în același fel în care este un docstring.
Unde aterizează depinde de ce este adnotat. Funcțiile și clasele primesc fiecare propriul dicționar __annotations__. O adnotare de variabilă simplă, precum rates: list[float], la nivel de modul sau de clasă, înregistrează intrarea fără să creeze deloc variabila — adnotează fără să atribui, iar numele tot ridică NameError la citire. Sub from __future__ import annotations nu se evaluează nimic la momentul definirii; fiecare adnotare este stocată ca șir, iar typing.get_type_hints este cel care le transformă înapoi în obiecte, rezolvând referințele înainte chiar în clipa în care întrebi.
Parametrizarea este un obiect de la rulare, nu o verificare de la rulare. list[float] apelează list.__class_getitem__ și returnează un types.GenericAlias care conține __origin__ (list) și __args__ ((float,)), pe care typing.get_origin și get_args le citesc înapoi. Acel obiect este suficient pentru ca un verificator să raționeze cu el și suficient pentru introspecție, dar verificarea lui ar însemna parcurgerea fiecărui element al containerului la fiecare verificare — un cost O(n) pe care limbajul refuză să îl ascundă în isinstance, motiv pentru care transmiterea unuia ridică TypeError, în loc să returneze un răspuns posibil greșit.
Protocol este structural: o clasă îl satisface având membrii potriviți, fără moștenire și fără înregistrare. Un verificator compară semnături complete — nume, tipuri de parametri, tip de retur. La rulare, @runtime_checkable permite isinstance, dar acea verificare este superficială în mod deliberat: întreabă doar dacă atributele există. Nu compară semnături, deci un obiect a cărui metodă primește argumente greșite trece verificarea și apoi pică la apel.
Acea prăpastie nu este un defect, este chiar despărțirea. Interpretorul tratează adnotările ca pe date inerte, ca adnotarea să nu coste nimic la momentul apelului; verificatorul le tratează ca pe un contract pe care îl verifică exhaustiv, fără să execute nimic. Nimic nu ține cele două în acord, motiv pentru care validarea la rulare — pydantic, un __post_init__, un isinstance explicit — este o treabă separată, pe care o bibliotecă trebuie să aleagă să o facă.
Cum arată la rulare
Creează un fișier:
touch freight_types.py
Rulează-l cu:
python3 freight_types.py
Fiecare pas este un program complet; înlocuiește fișierul de fiecare dată.
Adnotările sunt stocate și apoi ignorate
Cea mai mică demonstrație: o funcție care declară float la intrare și float la ieșire, apelată cu un șir.
def quote_freight(weight_kg: float, region: str) -> float:
return weight_kg * 2
print("annotations :", quote_freight.__annotations__)
print("correct call :", quote_freight(120.0, "eu-west"))
print("str argument :", quote_freight("120", "eu-west"))
print("returned type:", type(quote_freight("120", "eu-west")).__name__)
annotations : {'weight_kg': <class 'float'>, 'region': <class 'str'>, 'return': <class 'float'>}
correct call : 240.0
str argument : 120120
returned type: str
Adnotările sunt prezente și precise, stând într-un dicționar simplu, pe care îl poate citi orice program. Ele nu schimbă totodată nimic: "120" * 2 este Python valid, deci apelul reușește, iar o funcție adnotată -> float returnează șirul "120120". Nu se ridică nicio eroare nici la argument, nici la operație, nici la retur. Observă că valorile adnotărilor sunt chiar obiectele float și str, nu niște nume — ele au fost evaluate când a rulat def-ul.
Un generic parametrizat este un obiect, nu o verificare
O dimensiune în plus: un tip container cu un tip de element. Mediul de rulare are parametrizarea la dispoziție și totuși nu o folosește niciodată.
from typing import get_origin, get_args
Rates = list[float]
print("type(list[float]):", type(Rates).__name__)
print("get_origin :", get_origin(Rates).__name__)
print("get_args :", get_args(Rates))
rates: Rates = ["4.25", "5.10"]
print("isinstance(rates, list):", isinstance(rates, list))
try:
isinstance(rates, list[float])
except TypeError as exc:
print("isinstance(rates, list[float]) ->", exc)
type(list[float]): GenericAlias
get_origin : list
get_args : (<class 'float'>,)
isinstance(rates, list): True
isinstance(rates, list[float]) -> isinstance() argument 2 cannot be a parameterized generic
list[float] este un obiect real, ale cărui părți sunt lizibile la rulare, adică exact ce îi trebuie unui verificator și ce folosesc bibliotecile ca să construiască validatoare. Lista de șiruri este adnotată Rates și este în continuare o listă de șiruri; adnotarea nu a creat nicio obligație. TypeError este linia importantă: Python refuză să răspundă la întrebarea despre tipul elementelor, în loc să răspundă ieftin și greșit, pentru că singura implementare cinstită ar inspecta fiecare element.
runtime_checkable verifică nume, nu semnături
Dimensiunea următoare este tiparea structurală. isinstance față de un Protocol pare o verificare reală și este mult mai slab decât pare.
from typing import Protocol, runtime_checkable
@runtime_checkable
class Scannable(Protocol):
def scan(self, sku: str, quantity: int) -> str: ...
class DockScanner:
def scan(self, sku: str, quantity: int) -> str:
return f"{sku} x{quantity}"
class BrokenScanner:
def scan(self):
return "nothing"
print("DockScanner isinstance:", isinstance(DockScanner(), Scannable))
print("BrokenScanner isinstance:", isinstance(BrokenScanner(), Scannable))
try:
BrokenScanner().scan("SKU-9921", 3)
except TypeError as exc:
print("calling it ->", exc)
DockScanner isinstance: True
BrokenScanner isinstance: True
calling it -> BrokenScanner.scan() takes 1 positional argument but 3 were given
BrokenScanner trece verificarea isinstance și apoi pică în clipa în care este folosit. Verificarea a pus o singură întrebare — există un atribut numit scan? — iar ambele clase răspund da. Parametrii, valorile implicite și tipurile de retur îi sunt invizibile. Asta face din runtime_checkable o comoditate de tipare rață, nu o garanție: este solid pentru respingerea obiectelor cărora le lipsește complet un membru și inutil pentru confirmarea faptului că un membru este utilizabil.
Verificatorul citește aceleași adnotări și nu rulează nimic
Ultima dimensiune este al doilea program. Pune toate cele trei greșeli într-un singur fișier și dă-l lui mypy, nu lui Python.
# freight_types.py
from typing import Protocol
class Scannable(Protocol):
def scan(self, sku: str, quantity: int) -> str: ...
class BrokenScanner:
def scan(self) -> str:
return "nothing"
def quote_freight(weight_kg: float, region: str) -> float:
return weight_kg * 2
def register(scanner: Scannable) -> None: ...
rates: list[float] = ["4.25", "5.10"]
total: float = quote_freight("120", "eu-west")
register(BrokenScanner())
Verifică-l cu:
mypy freight_types.py
freight_types.py:20: error: List item 0 has incompatible type "str"; expected "float" [list-item]
freight_types.py:20: error: List item 1 has incompatible type "str"; expected "float" [list-item]
freight_types.py:21: error: Argument 1 to "quote_freight" has incompatible type "str"; expected "float" [arg-type]
freight_types.py:22: error: Argument 1 to "register" has incompatible type "BrokenScanner"; expected "Scannable" [arg-type]
freight_types.py:22: note: Following member(s) of "BrokenScanner" have conflicts:
freight_types.py:22: note: Expected:
freight_types.py:22: note: def scan(self, sku: str, quantity: int) -> str
freight_types.py:22: note: Got:
freight_types.py:22: note: def scan(self) -> str
Found 4 errors in 1 file (checked 1 source file)
Fiecare eșec demonstrat de pașii anteriori este prins aici, înainte să se execute ceva. Elementele listei sunt verificate individual — parcurgerea pe care a refuzat-o isinstance este gratuită pentru un verificator, pentru că el raționează despre literalul din sursă, nu despre un container din memorie. Ultimul diagnostic este câștigul: mypy compară semnătura completă a lui scan și raportează exact conflictul pe care runtime_checkable l-a lăsat să treacă. Conformitatea cu Protocol este decisă static; verificarea de la rulare este o umbră a ei.
Notă: formularea exactă a lui mypy se schimbă între versiuni. Diagnosticele și codurile lor de eroare sunt stabile; tratează textul ca fiind ilustrativ.
Cazuri limită și capcane
from __future__ import annotationstransformă totul în șiruri. Adnotările sunt stocate neevaluate, deci un nume care există doar subif TYPE_CHECKING:este legal într-o adnotare și absent la rulare — până când ceva apeleazăget_type_hints, care îl rezolvă și ridicăNameError.- Implicit, adnotările sunt evaluate la momentul definirii. O greșeală de scriere într-o adnotare este un
NameErrorla importul modulului, nu la apelarea funcției — unul dintre puținele feluri în care o adnotare poate strica un program care rulează. - Unele biblioteci chiar citesc adnotările.
@dataclassîși construiește câmpurile din__annotations__, iar pydantic validează față de ele. Aceea este o bibliotecă alegând să folosească datele, niciodată interpretorul impunându-le. Anyreduce complet verificatorul la tăcere. O expresie tipizatăAnyse propagă, deci un singurAnype o cale intens folosită poate ascunde erori departe de locul unde a fost introdus.objecteste alternativa sigură când vrei să spui „orice".- O valoare implicită
Nonenu implicăOptional.def f(region: str = None)este o eroare sub verificatoarele moderne; scrie explicitstr | None.
Rezumat
Adnotările sunt evaluate o dată, stocate în __annotations__ și ignorate de fiecare operație ulterioară. Genericele parametrizate există ca obiecte inspectabile, care poartă o origine și niște argumente, dar nu declanșează nicio verificare, iar isinstance le respinge, în loc să plătească parcurgerea unui container. Conformitatea cu Protocol este o judecată statică despre semnături; versiunea runtime_checkable întreabă doar dacă există numele atributelor.
- Apelarea unei funcții nu face absolut niciun fel de verificare de adnotare — o funcție adnotată
-> floatva returna unstrfără să se plângă. - Adnotările sunt evaluate la momentul definirii și stocate ca obiecte, dacă nu cumva
from __future__ import annotationsle stochează ca șiruri, pe careget_type_hintsle rezolvă ulterior. list[float]este untypes.GenericAliascu__origin__și__args__;isinstanceridicăTypeErrorpe el, pentru că verificarea elementelor ar fi O(n).@runtime_checkableverifică doar prezența membrilor, deci un obiect cu o metodă având semnătură greșită trece deisinstanceși pică la apel.- Verificatorul este un program separat, care nu execută niciodată codul, deci nimic nu garantează că mediul de rulare este de acord cu el — validarea la rulare este treaba unei biblioteci.