Vandaag presenteren we het eerste deel van de vertaling van het materiaal over hoe Dropbox typecontrole uitvoert op Python-code.
Bij Dropbox wordt er veel in Python geschreven. Het is de taal die we extreem breed gebruiken — zowel voor backend-diensten als voor desktopclient-applicaties. We maken ook op grote schaal gebruik van Go, TypeScript en Rust, maar Python is onze hoofdtaal. Gezien onze omvang, die miljarden regels Python-code omvat, bleek dat dynamische typisering de begrijpelijkheid van deze code onterecht ingewikkeld maakte en een ernstige impact had op de productiviteit. Om dit probleem te verlichten, zijn we begonnen met de geleidelijke overgang van onze code naar statische typecontrole met behulp van mypy. Dit is waarschijnlijk het populairste op zichzelf staande typecontrolesysteem voor Python. Mypy is een open-sourceproject waarvan de belangrijkste ontwikkelaars bij Dropbox werken.
Dropbox was een van de eerste bedrijven die statische typecontrole in Python-code op dergelijke schaal implementeerde. Tegenwoordig wordt mypy in duizenden projecten gebruikt. Dit instrument is talloze keren, zeg maar, 'in de strijd getest'. We hebben een lange weg afgelegd om te komen waar we nu zijn. Onderweg zijn er veel mislukte pogingen en gefaalde experimenten geweest. Dit materiaal vertelt het verhaal van statische typecontrole in Python — van het moeilijke begin als onderdeel van mijn onderzoekproject, tot het heden, wanneer typecontrole en typehinting gebruikelijk zijn voor talloze ontwikkelaars die in Python schrijven. Deze mechanismen worden nu ondersteund door veel tools — zoals IDE's en code-analyzers.
→
Waarom is typecontrole nodig?
Als je ooit dynamisch getypeerd Python hebt gebruikt, begrijp je misschien niet waarom er de laatste tijd zoveel gedoe is rond statische typen en mypy. Misschien vind je Python juist leuk vanwege de dynamische typering, en maakt het je gewoonweg frustrerend wat er gaande is. De sleutel tot de waarde van statische typechecking is de schaal van de oplossingen: hoe groter je project, hoe meer je geneigd bent tot statische typering, en uiteindelijk hoe meer je het echt nodig hebt.
Stel je voor dat een project de omvang van tienduizenden regels code heeft bereikt en dat er verschillende programmeurs aan werken. Bij het bekijken van een dergelijk project kunnen we op basis van onze ervaring zeggen dat het begrijpen van de code de sleutel zal zijn tot het handhaven van de productiviteit van ontwikkelaars. Zonder type-annotaties is het soms moeilijk te achterhalen, bijvoorbeeld, welke argumenten aan een functie moeten worden doorgegeven, of wat voor types waarden een bepaalde functie kan retourneren. Dit zijn typische vragen die vaak lastig te beantwoorden zijn zonder type-annotaties:
- Kan deze functie teruggeven
None? - Wat zou dit argument moeten zijn
items? - Wat is het type van het attribuut
id:intis dit,str, of misschien een gebruikersgedefinieerd type? - Moet dit argument een lijst zijn? Kan er een tuple aan worden doorgegeven?
Als je kijkt naar het volgende codefragment met type-annotaties en probeert dergelijke vragen te beantwoorden, blijkt dit een eenvoudige taak te zijn:
class Resource:
id: bytes
...
def read_metadata(self,
items: Sequence[str]) -> Dict[str, MetadataItem]:
...read_metadatageeft niets terug,Noneaangezien het geretourneerde type niet isOptional[…].- Het argument
itemsis een reeks strings. Het kan niet in een willekeurige volgorde worden geïtereerd. - Het attribuut
idis een byte-string.
In een ideale wereld zou je verwachten dat al deze nuances worden beschreven in de ingebouwde documentatie (docstring). Maar de ervaring biedt genoeg voorbeelden dat dergelijke documentatie in de code waar mee wordt gewerkt, vaak ontbreekt. Zelfs als deze documentatie aanwezig is, kan je niet rekenen op absolute nauwkeurigheid. Deze documentatie kan onduidelijk, onnauwkeurig zijn en veel ruimte laten voor verkeerde interpretatie. In grote teams of bij grote projecten kan dit probleem zeer acuut worden.
Hoewel Python uitstekend presteert in de vroege of tussenfasen van projecten, kunnen succesvolle projecten en bedrijven die Python gebruiken op een bepaald moment voor een cruciale vraag komen te staan: 'Moeten we alles herschrijven in een statisch getypeerde taal?'.
Type-checking systemen zoals mypy lossen het bovenstaande probleem op door ontwikkelaars een formele taal te bieden voor het beschrijven van types, en door te controleren of de typebeschrijvingen overeenkomen met de implementaties van programma's (en, niet verplicht, controleren op hun bestaan). Over het algemeen kunnen we zeggen dat deze systemen ons iets geven dat lijkt op zorgvuldig gecontroleerde documentatie.
Het gebruik van dergelijke systemen heeft ook andere voordelen, die helemaal niet triviaal zijn:
- Een type-checkingsysteem kan enkele kleine (en ook niet zo kleine) fouten ontdekken. Een typisch voorbeeld is wanneer men vergeet om een waarde te behandelen,
Noneof een andere bijzondere voorwaarde. - Refactoren van code wordt aanzienlijk eenvoudiger, aangezien het type-checkingsysteem vaak zeer nauwkeurig aangeeft welke code moet worden gewijzigd. Hierbij hoeven we niet te vertrouwen op 100% codecoverage met tests, wat in ieder geval meestal niet haalbaar is. We hoeven de diepten van stack trace-rapporten niet te bestuderen om de oorzaak van een probleem te achterhalen.
- Zelfs in grote projecten kan mypy vaak binnen enkele seconden een complete typecontrole uitvoeren. Het uitvoeren van tests duurt meestal tientallen seconden of zelfs minuten. Het typesysteem biedt programmeurs directe feedback, waardoor ze sneller hun werk kunnen doen. Ze hoeven geen kwetsbare en moeilijk te onderhouden unittests meer te schrijven, die echte entiteiten vervangen door mocks en patches, enkel om sneller resultaten van code-experimenten te krijgen.
IDE's en editors zoals PyCharm of Visual Studio Code maken gebruik van type-annotaties om ontwikkelaars functies zoals automatische code-aanvulling, foutmarkering en ondersteuning voor veelgebruikte taalconstructies te bieden. Dit zijn slechts enkele van de voordelen die type-analyse biedt. Voor sommige programmeurs is dit het belangrijkste argument voor type-analyse. Dit brengt direct voordelen met zich mee zodra het in de workflow wordt geïmplementeerd. Dit gebruik van typen vereist geen aparte type-controlesysteem zoals mypy, hoewel het belangrijk is op te merken dat mypy helpt om de consistentie van type-annotaties en code te waarborgen.
Achtergrond van mypy
De geschiedenis van mypy begon in het Verenigd Koninkrijk, in Cambridge, enkele jaren voordat ik me bij Dropbox voegde. Tijdens mijn doctoraatsonderzoek hield ik me bezig met de unificatie van statisch getypeerde en dynamische talen. Ik werd geïnspireerd door een artikel over graduele typificatie van Jeremy Siek en Walid Taha, evenals het Typed Racket-project. Ik zocht naar manieren om dezelfde programmeertaal voor verschillende projecten te gebruiken — van kleine scripts tot codebases van meerdere miljoenen regels. Daarbij wilde ik vermijden dat ik voor projecten van welke omvang dan ook teveel compromissen hoefde te sluiten. Een belangrijk onderdeel daarvan was het idee van een geleidelijke overgang van een niet-getypeerd prototype naar een volledig getest, statisch getypeerd eindproduct. Tegenwoordig worden deze ideeën grotendeels als vanzelfsprekend beschouwd, maar in 2010 was dit een probleem dat nog steeds actief werd onderzocht.
Mijn oorspronkelijke werk op het gebied van typecontrole was niet gericht op Python. In plaats daarvan gebruikte ik een kleine 'zelfgebouwde' taal. . Dit is een voorbeeld dat je helpt te begrijpen waar het om gaat (typeannotaties zijn hier optioneel):
def Fib(n as Int) as Int
if n <= 1
return n
else
return Fib(n - 1) + Fib(n - 2)
end
endHet gebruik van een vereenvoudigde zelfontwikkelde taal is een gebruikelijke aanpak in wetenschappelijk onderzoek. Dit is niet in de laatste plaats omdat dit snelle experimenten mogelijkmaakt en omdat dingen die niet relevant voor het onderzoek zijn, onbelemmerd genegeerd kunnen worden. Werkelijk gebruikte programmeertalen zijn meestal grootschalige fenomenen met complexe implementaties, wat experimenten vertraagt. Echter, resultaten die zijn gebaseerd op een vereenvoudigde taal lijken enigszins verdacht, omdat de onderzoeker bij het verkrijgen van deze resultaten mogelijk heeft ingeleverd op overwegingen die belangrijk zijn voor de praktische toepassing van talen.
Mijn typechecktool voor Alore leek veelbelovend, maar ik wilde het testen door experimenten uit te voeren met echte code, die, laten we zeggen, niet op Alore was geschreven. Gelukkig was de Alore-taal in grote mate gebaseerd op dezelfde ideeën als Python. Het was relatief eenvoudig om de typechecktool zo aan te passen dat deze werkte met de syntaxis en semantiek van Python. Dit maakte het mogelijk om typechecking uit te proberen op open-source Python-code. Daarnaast schreef ik een transpiler om code geschreven in Alore om te zetten naar Python-code en gebruikte deze om de code van mijn typechecktool te vertalen. Nu had ik een typechecksysteem geschreven in Python dat een subset van Python ondersteunde, een soort variant van deze taal! (Bepaalde architecturale beslissingen die logisch leken voor Alore, pasten slecht bij Python, wat nog steeds merkbaar is in sommige delen van de mypy-codebase.)
Eigenlijk was de taal die door mijn typesysteem werd ondersteund op dat moment niet helemaal Python te noemen: het was een variant van Python vanwege bepaalde beperkingen van de syntaxis van typeannotaties in Python 3.
Het leek een mix van Java en Python:
int fib(int n):
if n <= 1:
return n
else:
return fib(n - 1) + fib(n - 2)Een van mijn ideeën destijds was om type-annotaties te gebruiken om de prestaties te verbeteren door deze variant van Python naar C of misschien naar JVM-bytencode te compileren. Ik kwam zover dat ik een prototype van een compiler schreef, maar ik liet dit idee varen omdat typechecking op zich al vrij nuttig leek.
Uiteindelijk presenteerde ik mijn project op de PyCon 2013 in Santa Clara. Ook sprak ik hierover met Guido van Rossum, de genereuze levenslange dictator van Python. Hij overtuigde me om mijn eigen syntaxis op te geven en vast te houden aan de standaard syntaxis van Python 3. Python 3 ondersteunt functie-annotaties, waardoor mijn voorbeeld hieronder opnieuw kon worden geschreven, met als resultaat een normale Python-programma:
def fib(n: int) -> int:
if n <= 1:
return n
else:
return fib(n - 1) + fib(n - 2)Ik moest enkele compromissen sluiten (het is belangrijk om te vermelden dat ik mijn eigen syntaxis heb uitgevonden om deze reden). In het bijzonder ondersteunde Python 3.3, de meest recente versie van de taal op dat moment, geen variabele annotaties. Ik besprak met Guido via e-mail verschillende mogelijkheden voor de syntactische opmaak van dergelijke annotaties. We besloten om typecommentaar voor variabelen te gebruiken. Dit bereikte het beoogde doel, maar zag er een beetje omslachtig uit (Python 3.6 gaf ons een aangenamere syntaxis):
products = [] # type: List[str] # EwwTypecommentaren kwamen bovendien van pas voor de ondersteuning van Python 2, dat geen ingebouwde ondersteuning voor type-annotaties heeft:
f fib(n):
# type: (int) -> int
if n <= 1:
return n
else:
return fib(n - 1) + fib(n - 2)Het bleek dat deze (en andere) compromissen in feite niet veel betekenden — de voordelen van statische typing zorgden ervoor dat gebruikers al snel de minder perfecte syntaxis vergaten. Aangezien er nu in Python-code, waarin types werden gecontroleerd, geen speciale syntactische constructies werden gebruikt, bleven bestaande Python-tools en -processen normaal functioneren, wat het voor ontwikkelaars veel gemakkelijker maakte om het nieuwe hulpmiddel te leren.
Guido overtuigde me bovendien om me aan te sluiten bij Dropbox nadat ik mijn afstudeerscriptie had verdedigd. Hier begint het interessante deel van het mypy-verhaal.
Wordt vervolgd…
Geachte lezers! Als je Python gebruikt, laat ons dan weten welke projecten van welke omvang je in deze taal ontwikkelt.
Bron: habr.com
