Ang landas sa pag-typecheck ng 4 na milyong linya ng Python code. Bahagi 2

Ngayon ay inilalathala namin ang ikalawang bahagi ng pagsasalin ng materyal tungkol sa kung paano inayos ng Dropbox ang uri ng kontrol para sa ilang milyong linya ng Python code.

Ang landas sa pag-typecheck ng 4 na milyong linya ng Python code. Bahagi 2

β†’ Basahin ang unang bahagi

Opisyal na suporta sa uri (PEP 484)

Isinagawa namin ang aming unang seryosong mga eksperimento sa mypy sa Dropbox sa panahon ng Hack Week 2014. Ang Hack Week ay isang isang linggong kaganapan na hino-host ng Dropbox. Sa panahong ito, maaaring magtrabaho ang mga empleyado sa anumang gusto nila! Nagsimula ang ilan sa mga pinakatanyag na proyekto ng teknolohiya ng Dropbox sa mga kaganapang tulad nito. Bilang resulta ng eksperimentong ito, napagpasyahan namin na ang mypy ay mukhang may pag-asa, kahit na ang proyekto ay hindi pa handa para sa malawakang paggamit.

Noong panahong iyon, ang ideya ng pag-standardize ng Python type hinting system ay nasa hangin. Tulad ng sinabi ko, dahil ang Python 3.0 ay posible na gumamit ng mga uri ng anotasyon para sa mga pag-andar, ngunit ito ay mga arbitrary na expression lamang, nang walang tinukoy na syntax at semantics. Sa panahon ng pagpapatupad ng programa, ang mga anotasyong ito, sa karamihan, ay binalewala lang. Pagkatapos ng Hack Week, nagsimula kaming magtrabaho sa pag-standardize ng semantics. Ang gawaing ito ay humantong sa paglitaw PEP 484 (Guido van Rossum, Łukasz Langa at ako ay nagtulungan sa dokumentong ito).

Ang aming mga motibo ay maaaring tingnan mula sa dalawang panig. Una, inaasahan namin na ang buong Python ecosystem ay maaaring magpatibay ng isang karaniwang diskarte sa paggamit ng mga pahiwatig ng uri (isang terminong ginamit sa Python bilang katumbas ng "mga uri ng anotasyon"). Ito, dahil sa mga posibleng panganib, ay magiging mas mahusay kaysa sa paggamit ng maraming magkasalungat na diskarte. Pangalawa, gusto naming hayagang talakayin ang mga mekanismo ng uri ng anotasyon sa maraming miyembro ng komunidad ng Python. Ang pagnanais na ito ay bahagyang idinidikta ng katotohanan na hindi namin nais na magmukhang "mga apostata" mula sa mga pangunahing ideya ng wika sa mga mata ng malawak na masa ng mga programmer ng Python. Ito ay isang wikang dynamic na na-type, na kilala bilang "pag-type ng pato". Sa komunidad, sa simula pa lang, ang isang medyo kahina-hinalang saloobin sa ideya ng static na pag-type ay hindi maaaring makatulong ngunit lumitaw. Ngunit ang damdaming iyon sa kalaunan ay humina pagkatapos na maging malinaw na ang static na pag-type ay hindi magiging sapilitan (at pagkatapos na matanto ng mga tao na ito ay talagang kapaki-pakinabang).

Ang uri ng pahiwatig na syntax na kalaunan ay pinagtibay ay halos kapareho sa kung ano ang suportado ng mypy noong panahong iyon. Inilabas ang PEP 484 kasama ang Python 3.5 noong 2015. Ang Python ay hindi na isang dynamic na na-type na wika. Gusto kong isipin ang kaganapang ito bilang isang makabuluhang milestone sa kasaysayan ng Python.

Simula ng migration

Sa pagtatapos ng 2015, lumikha ang Dropbox ng isang pangkat ng tatlong tao upang magtrabaho sa mypy. Kasama nila sina Guido van Rossum, Greg Price at David Fisher. Mula sa sandaling iyon, ang sitwasyon ay nagsimulang umunlad nang napakabilis. Ang unang hadlang sa paglaki ng mypy ay ang pagganap. Tulad ng ipinahiwatig ko sa itaas, sa mga unang araw ng proyekto naisip ko ang tungkol sa pagsasalin ng mypy na pagpapatupad sa C, ngunit ang ideyang ito ay naalis sa listahan sa ngayon. Natigil kami sa pagpapatakbo ng system gamit ang CPython interpreter, na hindi sapat na mabilis para sa mga tool tulad ng mypy. (Ang proyektong PyPy, isang alternatibong pagpapatupad ng Python na may JIT compiler, ay hindi rin nakatulong sa amin.)

Sa kabutihang palad, ang ilang algorithmic na pagpapabuti ay dumating sa aming tulong dito. Ang unang makapangyarihang "accelerator" ay ang pagpapatupad ng incremental checking. Ang ideya sa likod ng pagpapahusay na ito ay simple: kung ang lahat ng mga dependency ng module ay hindi nagbago mula noong nakaraang run ng mypy, pagkatapos ay maaari naming gamitin ang data na naka-cache sa nakaraang pagtakbo habang nagtatrabaho sa mga dependency. Kailangan lang naming magsagawa ng type checking sa mga binagong file at sa mga file na nakadepende sa kanila. Lumayo pa ng kaunti ang Mypy: kung hindi nagbago ang panlabas na interface ng isang module, ipinapalagay ni mypy na hindi na kailangang suriing muli ang ibang mga module na nag-import ng module na ito.

Malaki ang naitulong sa amin ng incremental checking kapag nag-annotate ng malalaking halaga ng umiiral na code. Ang punto ay ang prosesong ito ay karaniwang nagsasangkot ng maraming umuulit na pagpapatakbo ng mypy habang ang mga anotasyon ay unti-unting idinaragdag sa code at unti-unting napabuti. Ang unang pagtakbo ng mypy ay napakabagal pa rin dahil marami itong mga dependency na susuriin. Pagkatapos, upang mapabuti ang sitwasyon, nagpatupad kami ng isang remote na mekanismo ng pag-cache. Kung nakita ng mypy na malamang na luma na ang lokal na cache, dina-download nito ang kasalukuyang snapshot ng cache para sa buong codebase mula sa sentralisadong repositoryo. Pagkatapos ay nagsasagawa ito ng incremental check gamit ang snapshot na ito. Nagdala ito sa amin ng isa pang malaking hakbang patungo sa pagtaas ng pagganap ng mypy.

Ito ay isang panahon ng mabilis at natural na paggamit ng uri ng checking sa Dropbox. Sa pagtatapos ng 2016, mayroon na kaming humigit-kumulang 420000 linya ng Python code na may mga uri ng anotasyon. Maraming mga gumagamit ang masigasig tungkol sa pagsuri ng uri. Parami nang parami ang mga development team na gumagamit ng Dropbox mypy.

Maganda ang lahat noon, ngunit marami pa kaming dapat gawin. Nagsimula kaming magsagawa ng mga pana-panahong panloob na mga survey ng user upang matukoy ang mga lugar ng problema ng proyekto at maunawaan kung anong mga isyu ang kailangang lutasin muna (ang kasanayang ito ay ginagamit pa rin sa kumpanya ngayon). Ang pinakamahalaga, tulad ng naging malinaw, ay dalawang gawain. Una, kailangan namin ng mas maraming uri ng saklaw ng code, pangalawa, kailangan namin ang mypy upang gumana nang mas mabilis. Ito ay ganap na malinaw na ang aming trabaho upang pabilisin ang mypy at ipatupad ito sa mga proyekto ng kumpanya ay malayo pa sa kumpleto. Kami, na lubos na nababatid ang kahalagahan ng dalawang gawaing ito, ay nagtakdang lutasin ang mga ito.

Higit pang produktibo!

Pinabilis ng mga incremental na pagsusuri ang mypy, ngunit hindi pa rin sapat ang bilis ng tool. Maraming incremental na pagsusuri ang tumagal nang halos isang minuto. Ang dahilan nito ay cyclical imports. Malamang na hindi ito magugulat sa sinumang nagtrabaho sa malalaking codebase na nakasulat sa Python. Mayroon kaming mga hanay ng daan-daang mga module, na ang bawat isa ay hindi direktang nag-import ng lahat ng iba pa. Kung binago ang anumang file sa isang import loop, kailangang iproseso ng mypy ang lahat ng file sa loop na iyon, at kadalasan ang anumang mga module na nag-import ng mga module mula sa loop na iyon. Ang isang ganoong cycle ay ang kasumpa-sumpa na "dependency tangle" na nagdulot ng maraming problema sa Dropbox. Sa sandaling ang istraktura na ito ay naglalaman ng ilang daang mga module, habang ito ay na-import, direkta o hindi direkta, maraming mga pagsubok, ginamit din ito sa code ng produksyon.

Isinasaalang-alang namin ang posibilidad ng "pagkakalat" ng mga circular dependencies, ngunit wala kaming mga mapagkukunan upang gawin ito. Napakaraming code na hindi namin pamilyar. Bilang resulta, nakabuo kami ng isang alternatibong diskarte. Napagpasyahan naming gawing mabilis ang mypy kahit na may "dependency tangles". Nakamit namin ang layuning ito gamit ang mypy daemon. Ang isang daemon ay isang proseso ng server na nagpapatupad ng dalawang kawili-wiling tampok. Una, nag-iimbak ito ng impormasyon tungkol sa buong codebase sa memorya. Nangangahulugan ito na sa tuwing magpapatakbo ka ng mypy, hindi mo kailangang mag-load ng naka-cache na data na nauugnay sa libu-libong mga na-import na dependency. Pangalawa, maingat niyang sinusuri, sa antas ng maliliit na yunit ng istruktura, ang mga dependency sa pagitan ng mga function at iba pang mga entity. Halimbawa, kung ang function foo tumatawag sa isang function bar, tapos may dependence foo mula sa bar. Kapag nagbago ang isang file, ang daemon muna, sa paghihiwalay, ay nagpoproseso lamang ng binagong file. Pagkatapos ay tinitingnan nito ang mga panlabas na nakikitang pagbabago sa file na iyon, tulad ng mga binagong pirma ng function. Gumagamit ang daemon ng detalyadong impormasyon tungkol sa mga pag-import upang i-double check ang mga function na aktwal na gumagamit ng binagong function. Karaniwan, sa diskarteng ito, kailangan mong suriin ang napakakaunting mga function.

Ang pagpapatupad ng lahat ng ito ay hindi madali, dahil ang orihinal na pagpapatupad ng mypy ay lubos na nakatuon sa pagproseso ng isang file sa isang pagkakataon. Kinailangan naming harapin ang maraming mga sitwasyon sa hangganan, ang paglitaw nito ay nangangailangan ng paulit-ulit na pagsusuri sa mga kaso kung saan may nagbago sa code. Halimbawa, ito ay nangyayari kapag ang isang klase ay nakatalaga ng isang bagong baseng klase. Kapag nagawa na namin ang gusto namin, nagawa naming bawasan ang oras ng pagpapatupad ng karamihan sa mga incremental na pagsusuri sa ilang segundo lang. Ito ay tila isang malaking tagumpay sa amin.

Mas productivity pa!

Kasama ang malayuang pag-cache na tinalakay ko sa itaas, halos ganap na nalutas ng mypy daemon ang mga problemang lumitaw kapag ang isang programmer ay madalas na nagpapatakbo ng pagsuri ng uri, na gumagawa ng mga pagbabago sa isang maliit na bilang ng mga file. Gayunpaman, ang pagganap ng system sa hindi gaanong kanais-nais na kaso ng paggamit ay malayo pa rin sa pinakamainam. Ang isang malinis na pagsisimula ng mypy ay maaaring tumagal ng higit sa 15 minuto. At ito ay higit pa sa magiging masaya sana kami. Bawat linggo ay lumalala ang sitwasyon habang ang mga programmer ay patuloy na nagsusulat ng bagong code at nagdaragdag ng mga anotasyon sa umiiral na code. Gutom pa rin ang aming mga user para sa higit pang performance, ngunit masaya kaming nakilala sila sa kalagitnaan.

Nagpasya kaming bumalik sa isa sa mga naunang ideya tungkol sa mypy. Lalo na, upang i-convert ang Python code sa C code. Ang pag-eksperimento sa Cython (isang system na nagpapahintulot sa iyo na isalin ang code na nakasulat sa Python sa C code) ay hindi nagbigay sa amin ng anumang nakikitang bilis, kaya nagpasya kaming buhayin ang ideya ng pagsulat ng aming sariling compiler. Dahil ang mypy codebase (nakasulat sa Python) ay naglalaman na ng lahat ng kinakailangang uri ng anotasyon, naisip namin na sulit na subukang gamitin ang mga anotasyong ito upang mapabilis ang system. Mabilis akong gumawa ng prototype para subukan ang ideyang ito. Nagpakita ito ng higit sa 10 beses na pagtaas sa pagganap sa iba't ibang micro-benchmark. Ang aming ideya ay i-compile ang mga module ng Python sa mga C module gamit ang Cython, at gawing mga tseke ng uri ng run-time ang mga uri ng anotasyon (karaniwan ay binabalewala ang mga anotasyon ng uri sa run-time at ginagamit lamang ng mga sistema ng pagsuri ng uri ). Talagang pinlano naming isalin ang pagpapatupad ng mypy mula sa Python sa isang wika na idinisenyo upang maging statically type, na magmukhang (at, para sa karamihan, gagana) nang eksakto tulad ng Python. (Ang ganitong uri ng cross-language migration ay naging isang tradisyon ng mypy project. Ang orihinal na pagpapatupad ng mypy ay isinulat sa Alore, pagkatapos ay mayroong syntactic hybrid ng Java at Python).

Ang pagtutok sa CPython extension API ay naging susi upang hindi mawala ang mga kakayahan sa pamamahala ng proyekto. Hindi namin kinailangang magpatupad ng virtual machine o anumang library na kailangan ng mypy. Bilang karagdagan, magkakaroon pa rin kami ng access sa buong Python ecosystem at lahat ng mga tool (tulad ng pytest). Nangangahulugan ito na maaari naming ipagpatuloy ang paggamit ng na-interpret na Python code sa panahon ng pag-unlad, na nagpapahintulot sa amin na magpatuloy sa pagtatrabaho sa napakabilis na pattern ng paggawa ng mga pagbabago sa code at pagsubok nito, sa halip na maghintay para sa pag-compile ng code. Mukhang mahusay kaming nakaupo sa dalawang upuan, wika nga, at nagustuhan namin ito.

Ang compiler, na tinawag naming mypyc (dahil ginagamit nito ang mypy bilang front-end para sa pagsusuri ng mga uri), naging isang napaka-matagumpay na proyekto. Sa pangkalahatan, nakamit namin ang humigit-kumulang 4x na bilis para sa madalas na pagtakbo ng mypy nang walang pag-cache. Ang pagbuo ng core ng mypyc project ay kinuha ng isang maliit na team nina Michael Sullivan, Ivan Levkivsky, Hugh Hahn, at ang aking sarili nang humigit-kumulang 4 na buwan ng kalendaryo. Ang dami ng trabahong ito ay mas maliit kaysa sa kung ano ang kinakailangan upang muling isulat ang mypy, halimbawa, sa C++ o Go. At kailangan naming gumawa ng mas kaunting mga pagbabago sa proyekto kaysa sa kailangan naming gawin kapag muling isinulat ito sa ibang wika. Inaasahan din namin na maaari naming dalhin ang mypyc sa ganoong antas na magagamit ito ng ibang mga programmer ng Dropbox upang i-compile at pabilisin ang kanilang code.

Upang makamit ang antas ng pagganap na ito, kinailangan naming maglapat ng ilang kawili-wiling solusyon sa engineering. Kaya, ang compiler ay maaaring pabilisin ang maraming mga operasyon sa pamamagitan ng paggamit ng mabilis, mababang antas na mga konstruksyon ng C. Halimbawa, ang isang pinagsama-samang function na tawag ay isinalin sa isang C function na tawag. At ang gayong tawag ay mas mabilis kaysa sa pagtawag sa isang na-interpret na function. Ang ilang mga operasyon, tulad ng mga paghahanap sa diksyunaryo, ay kasangkot pa rin sa paggamit ng mga regular na C-API na tawag mula sa CPython, na bahagyang mas mabilis kapag pinagsama-sama. Nagawa naming alisin ang karagdagang pag-load sa system na nilikha ng interpretasyon, ngunit ito sa kasong ito ay nagbigay lamang ng maliit na pakinabang sa mga tuntunin ng pagganap.

Upang matukoy ang pinakakaraniwang "mabagal" na mga operasyon, nagsagawa kami ng code profiling. Gamit ang data na ito, sinubukan naming i-tweak ang mypyc upang makabuo ito ng mas mabilis na C code para sa mga naturang operasyon, o muling isulat ang kaukulang Python code gamit ang mas mabilis na mga operasyon (at kung minsan ay wala kaming simpleng solusyon para doon o iba pang problema) . Ang muling pagsusulat ng Python code ay madalas na isang mas madaling solusyon sa problema kaysa sa pagkakaroon ng compiler na awtomatikong gumanap ng parehong pagbabago. Sa pangmatagalan, gusto naming i-automate ang marami sa mga pagbabagong ito, ngunit sa panahong iyon ay nakatuon kami sa pagpapabilis ng mypy na may kaunting pagsisikap. At sa paglipat patungo sa layuning ito, pinutol namin ang ilang mga sulok.

Upang patuloy ...

Minamahal na mambabasa! Ano ang iyong mga impression sa mypy project nang malaman mo ang pagkakaroon nito?

Ang landas sa pag-typecheck ng 4 na milyong linya ng Python code. Bahagi 2
Ang landas sa pag-typecheck ng 4 na milyong linya ng Python code. Bahagi 2

Pinagmulan: www.habr.com

Magdagdag ng komento