Python Python

Ce face o adnotare de tip la rularea unui program Python

Dima Авг 30, 2026

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 annotations transformă totul în șiruri. Adnotările sunt stocate neevaluate, deci un nume care există doar sub if 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 NameError la 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.
  • Any reduce complet verificatorul la tăcere. O expresie tipizată Any se propagă, deci un singur Any pe o cale intens folosită poate ascunde erori departe de locul unde a fost introdus. object este alternativa sigură când vrei să spui „orice".
  • O valoare implicită None nu implică Optional. def f(region: str = None) este o eroare sub verificatoarele moderne; scrie explicit str | 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ă -> float va returna un str fără să se plângă.
  • Adnotările sunt evaluate la momentul definirii și stocate ca obiecte, dacă nu cumva from __future__ import annotations le stochează ca șiruri, pe care get_type_hints le rezolvă ulterior.
  • list[float] este un types.GenericAlias cu __origin__ și __args__; isinstance ridică TypeError pe el, pentru că verificarea elementelor ar fi O(n).
  • @runtime_checkable verifică doar prezența membrilor, deci un obiect cu o metodă având semnătură greșită trece de isinstance ș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.

Чтобы получить доступ к облачной лаборатории, необходимо войти в систему.

Войти