Vejen til typekontrol af 4 millioner linjer Python-kode. Del 1

I dag præsenterer vi den første del af oversættelsen af materialet om, hvordan Dropbox arbejder med typekontrol i Python-kode.

Vejen til typekontrol af 4 millioner linjer Python-kode. Del 1

I Dropbox skriver vi meget i Python. Det er et sprog, vi bruger ekstremt meget – både til backend-tjenester og desktop-klientapplikationer. Vi bruger også Go, TypeScript og Rust i stor udstrækning, men Python er vores primære sprog. Givet vores skala, som omhandler millioner af linjer Python-kode, viste det sig, at den dynamiske typificering af denne kode unødvendigt komplicerede forståelsen af den og begyndte seriøst at påvirke produktiviteten. For at afhjælpe dette problem begyndte vi gradvist at konvertere vores kode til statisk typekontrol ved hjælp af mypy. Dette er sandsynligvis det mest populære uafhængige typekontrolsystem til Python. Mypy er et open source-projekt, hvor de vigtigste udviklere arbejder hos Dropbox.

Dropbox har vist sig at være et af de første virksomheder, der implementerede statisk typekontrol i Python-kode i en så stor skala. I dag bruges mypy i tusindvis af projekter. Dette værktøj er blevet 'afprøvet i kamp' utallige gange. For at nå derhen, hvor vi er nu, har vi taget en lang vej. På denne vej har der været mange mislykkede begyndelser og fiasko-eksperimenter. Dette materiale fortæller historien om statisk typekontrol i Python – fra den svære begyndelse, som var en del af mit forskningsprojekt, til i dag, hvor typekontrol og typetips er blevet almindelige blandt utallige udviklere, der skriver i Python. Disse mekanismer understøttes nu af mange værktøjer – såsom IDE'er og kodeanalysatorer.

→ Læs den anden del

Hvorfor er typekontrol nødvendig?

Hvis du nogensinde har brugt det dynamisk typede Python, kan du have en vis forvirring over, hvorfor der i den seneste tid har været så meget fokus på statisk typning og mypy. Måske kan det også være, at du netop værdsætter Python for dets dynamiske typning, og at det, der sker i øjeblikket, frustrerer dig. Nøglen til værdien af statisk typning ligger i projektets omfang: Jo større dit projekt er, jo mere tilbøjelig er du til at vælge statisk typning, og i sidste ende, jo mere har du reelt brug for det.

Lad os antage, at et projekt har vokset sig til titusinder af linjer kode, og at flere programmører arbejder på det. Når vi ser på et sådant projekt, kan vi basere på vores erfaring sige, at forståelsen af koden vil være nøglen til at opretholde udviklernes produktivitet. Uden typeannotering er det ofte svært at finde ud af, for eksempel, hvilke argumenter der skal videregives til en funktion, eller hvilke typer værdier en funktion kan returnere. Her er nogle typiske spørgsmål, som det ofte kan være vanskeligt at besvare uden at bruge typeannotering:

  • Kan denne funktion returnere None?
  • Hvad skal dette argument være items?
  • Hvad er typen af attributten id: int er det, str, eller måske en brugerdefineret type?
  • Skal dette argument være en liste? Kan der videregives et tuple til det?

Hvis vi ser på følgende kodeeksempel, der er udstyret med typeannoteringer, og forsøger at besvare sådanne spørgsmål, vil det vise sig, at det er en ganske simpel opgave:

class Resource:
    id: bytes
    ...
    def read_metadata(self, 
                      items: Sequence[str]) -> Dict[str, MetadataItem]:
        ...

  • read_metadata returnerer ikke None, da den returnerede type ikke er Optional[…].
  • Argumentet items er en sekvens af strenge. Den kan ikke itere i vilkårlig rækkefølge.
  • Attributten id er en byte-streng.

I en perfekt verden kunne man forvente, at alle sådanne nuancer ville være beskrevet i den indbyggede dokumentation (docstring). Men erfaringen giver mange eksempler på, at sådan dokumentation i den kode, man arbejder med, ofte ikke er til stede. Selv hvis sådan dokumentation er tilgængelig i koden, kan man ikke regne med dens absolutte korrekthed. Denne dokumentation kan være uklar, unøjagtig og efterlade mange muligheder for misforståelser. I store teams eller i store projekter kan dette problem blive ekstremt akut.

Selvom Python klarer sig fremragende i de tidlige eller mellemliggende faser af projekter, kan succesfulde projekter og virksomheder, der bruger Python, på et bestemt tidspunkt stå over for det livsvigtige spørgsmål: "Skal vi omskrive alt i et statisk typet sprog?".

Type-checking-systemer som mypy løser ovenstående problem ved at give udviklere et formelt sprog til beskrivelse af typer, og ved at kontrollere, at typebeskrivelserne svarer til programmets implementering (og, hvilket ikke er nødvendigt, kontrollerer deres eksistens). Generelt kan man sige, at disse systemer giver os noget, der ligner omhyggeligt kontrolleret dokumentation.

Der er også andre fordele ved at anvende sådanne systemer, og de er allerede ganske usædvanlige:

  • Type-checking-systemet kan opdage nogle små (og også ikke så små) fejl. Et typisk eksempel er, når man glemmer at håndtere en værdi None eller et andet særligt betingelse.
  • Refaktorering af koden bliver betydeligt lettere, da type-checking-systemet ofte meget præcist angiver, hvilken kode der skal ændres. Desuden behøver vi ikke at håbe på 100% testdækning, hvilket normalt er uopnåeligt. Vi skal ikke dykke ned i dybderne af stack-trace-rapporter for at finde årsagen til fejlen.
  • Selv i store projekter kan mypy ofte udføre en fuld typetjek på brøkdele af et sekund. Udførelsen af test tager normalt titusinder af sekunder eller endda flere minutter. Typetjeksystemet giver programmøren øjeblikkelig feedback og gør det muligt for dem at arbejde hurtigere. Det er ikke længere nødvendigt at skrive skrøbelige og vanskelige modulære tests, som erstatter reelle enheder med mocks og patches kun for at få hurtigere testresultater for koden.

IDE'er og editorer som PyCharm eller Visual Studio Code udnytter funktionerne ved typeannoteringer til at give udviklerne muligheder for automatisk kodefuldførelse, fejlmarkering og understøttelse af ofte brugte sprogkonstruktioner. Og dette er blot nogle af fordelene ved typeannotering. For nogle programmører er det en hovedargument for typeannotering. Det gavner straks efter implementeringen. Denne anvendelse af typer kræver ikke en separat typetjeksystem som mypy, selvom det skal nævnes, at mypy hjælper med at opretholde overensstemmelse mellem typeannoteringer og kode.

Baggrunden for mypy

Historien om mypy begyndte i Storbritannien, i Cambridge, nogle år før jeg kom til Dropbox. Jeg arbejdede som en del af min ph.d.-forskning på problemet med at forene statisk typede og dynamiske sprog. Jeg blev inspireret af Jeremy Siecks og Valida Tachs artikel om gradvis typificering samt projektet Typed Racket. Jeg forsøgte at finde måder at bruge det samme programmeringssprog til forskellige projekter - fra små scripts til kodebaser bestående af mange millioner linjer. Jeg ønskede, at et projekt af enhver størrelse ikke skulle indebære for store kompromiser. En vigtig del af dette var idéen om en gradvis overgang fra et utypificeret prototypeprojekt til en grundigt testet, statisk typet færdigvare. I dag betragtes disse idéer i høj grad som taget for givet, men i 2010 var det et problem, der stadig blev aktivt undersøgt.

Mit oprindelige arbejde med typetjek var ikke rettet mod Python. I stedet brugte jeg et lille "hjemmelavet" sprog. AloreHer er et eksempel, der vil hjælpe dig med at forstå, hvad det handler om (typannoteringer er her ikke nødvendige):

def Fib(n as Int) as Int
  if n <= 1
    return n
  else
    return Fib(n - 1) + Fib(n - 2)
  end
end

Brugen af et forenklet sprog, der er udviklet internt, er en almindelig tilgang i forskning. Dette er især tilfældet, fordi det muliggør hurtige eksperimenter, og det, der ikke er relevant for forskningen, kan nemt ignoreres. De faktiske programmeringssprog, der anvendes, repræsenterer almindeligvis komplekse fænomener med avancerede implementeringer, hvilket nedsætter eksperimenternes hastighed. Men resultater, der er baseret på et forenklet sprog, kan synes lidt mistænkelige, da forskeren kan have ofret overvejelser, der er vigtige for praktisk anvendelse af sprog.

Mit typekontrolværktøj til Alore så meget lovende ud, men jeg ville teste det ved at udføre eksperimenter med rigtig kode, som der stort set ikke var skrevet i Alore. Heldigvis var Alore-sproget i vid udstrækning baseret på de samme ideer som Python. Det var ret enkelt at omdanne værktøjet til typekontrol, så det kunne arbejde med syntaks og semantik fra Python. Dette gjorde det muligt at teste typekontrol med open source Python-kode. Derudover skrev jeg en transpiler til at konvertere koden skrevet i Alore til Python-kode og brugte den til at oversætte koden fra mit typekontrolværktøj. Nu havde jeg et system til typekontrol skrevet i Python, der understøttede et delmængde af Python, en slags variant af dette sprog! (Nogle arkitektoniske beslutninger, der gav mening for Alore, passede dårligt til Python; det ses stadig i nogle dele af mypy’s kodebase.)

Faktisk kunne sproget, der understøttes af mit typesystem, på dette tidspunkt ikke helt kaldes Python: det var en variant af Python på grund af visse syntaktiske begrænsninger i Python 3's typeannoteringer.

Det så ud som en blanding af Java og Python:

int fib(int n):
    if n <= 1:
        return n
    else:
        return fib(n - 1) + fib(n - 2)

En af mine idéer dengang var at bruge typeannoteringer til at forbedre ydeevnen ved at kompilere denne variant af Python til C eller måske til JVM bytekode. Jeg nåede til fasen med at skrive en prototype af kompilatoren, men opgav projektet, da typekontrol i sig selv virkede ret nyttig.

Jeg præsenterede mit projekt på PyCon 2013 i Santa Clara. Jeg talte også om det med Guido van Rossum, den velvillige livslange diktator for Python. Han overbeviste mig om at droppe min egen syntaks og holde mig til standard Python 3 syntaks. Python 3 understøtter funktionsannoteringer, hvilket gjorde det muligt for mig at omskrive mit eksempel, som vist nedenfor, og få et regulært Python-program:

def fib(n: int) -> int:
    if n <= 1:
        return n
    else:
        return fib(n - 1) + fib(n - 2)

Jeg var nødt til at gå på kompromis (først og fremmest vil jeg bemærke, at jeg opfandt min egen syntaks netop derfor). Især understøttede Python 3.3, den seneste version af sproget på det tidspunkt, ikke variableannoteringer. Jeg diskuterede med Guido via e-mail de forskellige muligheder for syntaktisk udformning af sådanne annoteringer. Vi besluttede at bruge kommentarer med typeangivelser for variabler. Dette nåede målet, men så noget klodset ud (Python 3.6 gav os en mere behagelig syntaks):

products = []  # type: List[str]  # Øv

Typekommentarer var desuden nyttige for at understøtte Python 2, som ikke har indbygget støtte til typeannoteringer:

def fib(n):
    # type: (int) -> int
    if n <= 1:
        return n
    else:
        return fib(n - 1) + fib(n - 2)

Det viste sig, at disse (og andre) kompromiser faktisk ikke betød så meget — fordelene ved statisk typification gjorde det muligt for brugerne hurtigt at glemme den ikke helt ideelle syntaks. Da der nu ikke blev brugt specielle syntaktiske konstruktioner i Python-kode, hvor typer blev kontrolleret, fortsatte eksisterende Python-værktøjer og kodebehandlingsprocesser med at fungere normalt, hvilket gjorde det meget lettere for udviklere at lære det nye værktøj.

Guido overbeviste også mig om at slutte mig til Dropbox efter jeg havde forsvaret mit speciale. Her begynder det mest interessante i historien om mypy.

Fortsætter…

Kære læsere! Hvis du bruger Python, hører vi gerne om, hvilke projekter af hvilken størrelse du udvikler på dette sprog.

Vejen til typekontrol af 4 millioner linjer Python-kode. Del 1
Vejen til typekontrol af 4 millioner linjer Python-kode. Del 1

Kilde: habr.com

Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere 🔥 Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere | ProHoster