Cara mengetik memeriksa 4 juta baris kode Python. Bagian 2

Hari ini kami menerbitkan bagian kedua dari terjemahan materi tentang bagaimana Dropbox mengatur kontrol jenis beberapa juta baris kode Python.



Baca bagian pertama

Dukungan Tipe Formal (PEP 484)


Kami melakukan percobaan serius pertama dengan mypy di Dropbox selama Hack Week 2014. Hack Week adalah acara yang diadakan oleh Dropbox selama satu minggu. Saat ini, karyawan dapat mengerjakan apa saja! Beberapa proyek teknologi paling terkenal Dropbox dimulai pada acara serupa. Sebagai hasil dari percobaan ini, kami sampai pada kesimpulan bahwa mypy terlihat menjanjikan, meskipun proyek ini belum siap untuk digunakan secara luas.

Pada saat itu, gagasan standardisasi sistem isyarat untuk tipe Python sedang mengemuka. Seperti yang saya katakan, dimulai dengan Python 3.0, Anda bisa menggunakan anotasi jenis untuk fungsi, tetapi ini hanya ekspresi sewenang-wenang, tanpa sintaks dan semantik tertentu. Selama pelaksanaan program, penjelasan ini, sebagian besar, hanya diabaikan. Setelah Hack Week, kami mulai mengerjakan standardisasi semantik. Pekerjaan ini menyebabkan munculnya PEP 484 (Guido van Rossum, Lukas Langa dan saya berkolaborasi dalam dokumen ini).

Motif kami bisa dilihat dari dua sisi. Pertama, kami berharap bahwa seluruh ekosistem Python dapat mengambil pendekatan umum untuk menggunakan petunjuk jenis (tip petunjuk adalah istilah yang digunakan dalam Python sebagai analog dari "anotasi tipe"). Ini, mengingat kemungkinan risikonya, akan lebih baik daripada menggunakan banyak pendekatan yang tidak kompatibel satu sama lain. Kedua, kami ingin secara terbuka membahas mekanisme anotasi jenis dengan banyak anggota komunitas Python. Sebagian, keinginan ini didikte oleh fakta bahwa kita tidak ingin terlihat seperti "murtad" dari ide-ide dasar bahasa di mata massa luas programmer Python. Ini adalah bahasa yang diketik secara dinamis yang dikenal sebagai "mengetik bebek." Di komunitas, pada awalnya, sikap yang agak mencurigakan terhadap gagasan mengetik statis tidak bisa tidak muncul. Tapi sikap ini akhirnya melemah - setelah menjadi jelas bahwa pengetikan statis tidak direncanakan wajib (dan setelah orang menyadari bahwa itu benar-benar berguna).

Sintaks yang dihasilkan untuk petunjuk jenis sangat mirip dengan yang mypy didukung pada waktu itu. PEP 484 keluar dengan Python 3.5 pada 2015. Python bukan lagi bahasa yang hanya mendukung pengetikan dinamis. Saya suka menganggap acara ini sebagai tonggak penting dalam sejarah Python.

Mulai migrasi


Pada akhir 2015, tim yang terdiri dari tiga orang dibuat di Dropbox untuk mengerjakan mypy. Itu termasuk Guido van Rossum, Greg Price dan David Fisher. Sejak saat itu situasinya mulai berkembang dengan sangat cepat. Hambatan pertama untuk pertumbuhan mypy adalah kinerja. Seperti yang sudah saya sebutkan di atas, pada periode awal pengembangan proyek, saya berpikir untuk menerjemahkan implementasi mypy ke dalam C, tetapi ide ini telah dihapus dari daftar sejauh ini. Kami terjebak dengan fakta bahwa kami menggunakan juru bahasa CPython untuk memulai sistem, yang tidak cukup cepat untuk alat-alat seperti mypy. (Proyek PyPy, implementasi alternatif Python dengan kompiler JIT, juga tidak membantu kami.)

Untungnya, di sini beberapa perbaikan algoritmik membantu kami. "Akselerator" kuat pertama adalah penerapan verifikasi tambahan. Gagasan peningkatan ini sederhana: jika semua dependensi modul tidak berubah sejak peluncuran mypy sebelumnya, maka kita dapat menggunakan data yang di-cache selama sesi sebelumnya saat bekerja dengan dependensi. Yang harus kami lakukan adalah mengetikkan centang pada file yang dimodifikasi dan pada file-file yang bergantung padanya. Mypy bahkan melangkah lebih jauh: jika antarmuka eksternal modul tidak berubah - mypy berpikir bahwa modul lain yang mengimpor modul ini tidak boleh diperiksa lagi.

Validasi tambahan telah sangat membantu kami dalam memberikan anotasi dalam volume besar kode yang ada. Faktanya adalah bahwa proses ini biasanya melibatkan banyak iterasi berjalan mypy, karena penjelasan secara bertahap ditambahkan ke kode dan secara bertahap ditingkatkan. Peluncuran pertama mypy masih sangat lambat, karena ketika Anda menjalankannya, Anda harus memeriksa banyak dependensi. Kemudian, demi memperbaiki situasi, kami menerapkan mekanisme caching jarak jauh. Jika mypy mendeteksi bahwa cache lokal mungkin kedaluwarsa, ia mengunduh snapshot cache saat ini untuk seluruh basis kode dari repositori terpusat. Dia kemudian melakukan pemeriksaan tambahan menggunakan snapshot ini. Ini adalah langkah besar lain yang telah menggerakkan kami menuju peningkatan produktivitas mypy.

Ini adalah periode pengenalan sistem pemeriksaan jenis Dropbox yang cepat dan alami. Pada akhir 2016, kami sudah memiliki sekitar 420.000 baris kode Python dengan anotasi jenis. Banyak pengguna yang antusias memeriksa jenis. Dropbox mypy telah digunakan oleh semakin banyak tim pengembangan.

Segalanya tampak bagus saat itu, tetapi kami masih harus melakukan banyak hal. Kami mulai melakukan survei pengguna internal berkala untuk mengidentifikasi area masalah proyek dan memahami masalah apa yang perlu diatasi terlebih dahulu (praktik ini digunakan di perusahaan saat ini). Yang paling penting, seperti yang sudah jelas, adalah dua tugas. Yang pertama - Anda membutuhkan lebih banyak cakupan kode dengan tipe, yang kedua - perlu bagi mypy untuk bekerja lebih cepat. Sangat jelas bahwa pekerjaan kami dalam mempercepat mypy dan implementasinya dalam proyek-proyek perusahaan masih jauh dari selesai. Kami, menyadari sepenuhnya pentingnya kedua tugas ini, mengambil solusi mereka.

Lebih banyak kinerja!


Pemeriksaan tambahan mempercepat mypy, tetapi alat ini masih belum cukup cepat. Banyak pemeriksaan tambahan berlangsung sekitar satu menit. Alasan untuk ini adalah impor siklus. Ini mungkin tidak akan mengejutkan siapa pun yang telah bekerja dengan basis kode besar yang ditulis dengan Python. Kami memiliki ratusan set modul, yang masing-masing secara tidak langsung mengimpor semua modul lainnya. Jika ada file dalam siklus impor yang diubah, mypy harus memproses semua file yang termasuk dalam siklus ini, dan seringkali juga setiap modul yang mengimpor modul dari siklus ini. Salah satu siklus seperti itu adalah "kusut ketergantungan," yang menyebabkan banyak masalah di Dropbox. Setelah struktur ini berisi beberapa ratus modul, sementara itu diimpor, secara langsung atau tidak langsung, banyak tes, itu juga digunakan dalam kode produksi.

Kami mempertimbangkan kemungkinan dependensi siklik yang "terurai", tetapi kami tidak memiliki sumber daya untuk melakukan ini. Terlalu banyak kode yang tidak kami kenal. Sebagai hasilnya, kami mengambil pendekatan alternatif. Kami memutuskan untuk membuat mypy bekerja cepat bahkan jika ada "bola ketergantungan". Kami menyelesaikan ini dengan daemon mypy. Daemon adalah proses server yang mengimplementasikan dua fitur menarik. Pertama, ia menyimpan informasi memori tentang seluruh basis kode. Ini berarti bahwa setiap kali Anda menjalankan mypy, Anda tidak perlu mengunduh data cache yang terkait dengan ribuan dependensi yang diimpor. Kedua, ia dengan hati-hati, pada tingkat unit struktural kecil, menganalisis hubungan antara fungsi dan entitas lainnya. Misalnya, jika fungsi foo memanggil bar fungsi, maka ada ketergantungan foo pada bar . Ketika file diubah, daemon pertama, secara terpisah, hanya memproses file yang diubah. Kemudian dia melihat perubahan pada file ini yang terlihat dari luar, seperti tanda tangan fungsi yang diubah. Daemon menggunakan informasi impor terperinci hanya untuk memeriksa ulang fungsi-fungsi yang benar-benar menggunakan fungsi yang diubah. Biasanya, dengan pendekatan ini, sangat sedikit fungsi yang harus diperiksa.

Menerapkan semua ini tidaklah mudah, karena implementasi asli mypy sangat terfokus pada pemrosesan satu file pada satu waktu. Kami harus berurusan dengan banyak situasi perbatasan, kejadian yang memerlukan pemeriksaan ulang dalam kasus-kasus ketika sesuatu berubah dalam kode. Sebagai contoh, ini terjadi ketika kelas dasar baru ditugaskan ke kelas. Setelah kami melakukan apa yang kami inginkan, kami dapat mengurangi waktu pelaksanaan sebagian besar pemeriksaan tambahan menjadi beberapa detik. Bagi kami, itu merupakan kemenangan besar.

Lebih banyak kinerja!


Bersama dengan caching jarak jauh, yang saya jelaskan di atas, daemon mypy hampir sepenuhnya menyelesaikan masalah yang muncul ketika programmer sering menjalankan pengecekan tipe, membuat perubahan pada sejumlah kecil file. Namun, kinerja sistem dalam varian yang paling tidak menguntungkan penggunaannya masih jauh dari optimal. Awal mypy yang bersih bisa memakan waktu lebih dari 15 menit. Dan itu jauh lebih dari yang kita inginkan. Setiap minggu, situasinya memburuk, karena programmer terus menulis kode baru dan menambahkan anotasi ke kode yang ada. Pengguna kami masih merindukan kinerja yang lebih tinggi, tetapi kami senang siap untuk bertemu dengan mereka.

Kami memutuskan untuk kembali ke salah satu ide awal saya tentang mypy. Yakni, konversi kode Python ke kode C. Percobaan dengan Cython (ini adalah sistem yang memungkinkan Anda untuk menerjemahkan kode Python ke dalam kode C) tidak memberi kami akselerasi yang terlihat, jadi kami memutuskan untuk menghidupkan kembali ide untuk menulis kompiler kami sendiri. Karena basis kode mypy (ditulis dengan Python) sudah berisi semua jenis anotasi yang diperlukan, upaya untuk menggunakan anotasi ini untuk mempercepat sistem tampaknya bermanfaat. Saya segera membuat prototipe untuk menguji ide ini. Dia menunjukkan pada berbagai tolok ukur mikro peningkatan produktivitas lebih dari 10 kali lipat. Gagasan kami adalah mengkompilasi modul Python menjadi modul-C menggunakan Cython, dan untuk mengubah anotasi tipe menjadi pemeriksaan tipe yang dilakukan pada saat run time (biasanya anotasi tipe diabaikan pada saat run time dan hanya digunakan oleh sistem pengecekan tipe ) Kami sebenarnya berencana untuk menerjemahkan implementasi mypy dari Python ke bahasa yang dibuat secara statis, yang akan terlihat (dan, sebagian besar, berfungsi) persis seperti Python. (Migrasi lintas-bahasa semacam ini telah menjadi semacam tradisi proyek mypy. Implementasi awal mypy ditulis dalam bahasa Alore, kemudian ada hibrida sintaksis antara Jawa dan Python).

Berfokus pada API ekstensi CPython adalah kunci untuk tidak kehilangan kemampuan manajemen proyek. Kami tidak perlu menerapkan mesin virtual atau pustaka yang diperlukan mypy. Selain itu, seluruh ekosistem Python masih akan tersedia untuk kita, semua alat (seperti pytest) akan tersedia. Ini berarti bahwa kami dapat terus menggunakan kode Python yang ditafsirkan selama pengembangan, yang akan memungkinkan kami untuk terus bekerja menggunakan skema yang sangat cepat untuk membuat perubahan pada kode dan mengujinya, daripada menunggu kode dikompilasi. Sepertinya kami sangat mampu, jadi untuk berbicara, duduk di dua kursi, dan kami menyukainya.

Compiler, yang kami beri nama mypyc (karena menggunakan mypy sebagai frontend untuk analisis tipe), ternyata merupakan proyek yang sangat sukses. Semua dalam semua, kami mencapai sekitar 4x percepatan lebih cepat dari sering berjalan mypy tanpa caching. Pengembangan inti dari proyek mypyc memakan waktu sekitar 4 bulan kalender dari sebuah tim kecil yang mencakup Michael Sullivan, Ivan Levkivsky, Hugh Han dan saya. Jumlah pekerjaan ini jauh lebih tidak ambisius daripada apa yang diperlukan untuk menulis ulang mypy, misalnya, dalam C ++ atau Go. Dan kami harus membuat lebih sedikit perubahan pada proyek daripada yang harus kami lakukan ketika menulis ulang dalam bahasa lain. Kami juga berharap bahwa kami dapat membawa mypyc ke tingkat yang dapat digunakan oleh programmer Dropbox lainnya untuk mengkompilasi dan mempercepat kode mereka.

Untuk mencapai tingkat kinerja ini, kami harus menerapkan beberapa solusi rekayasa yang menarik. Jadi, kompiler dapat mempercepat banyak operasi dengan menggunakan konstruksi tingkat rendah cepat C. Misalnya, panggilan ke fungsi yang dikompilasi diterjemahkan menjadi panggilan ke fungsi C. Dan panggilan semacam itu dibuat jauh lebih cepat daripada memanggil fungsi yang ditafsirkan. Beberapa operasi, seperti pencarian kamus, masih dirubah untuk menggunakan panggilan C-API biasa dari CPython, yang setelah kompilasi ternyata hanya sedikit lebih cepat. Kami dapat menyingkirkan beban tambahan pada sistem yang dibuat oleh interpretasi, tetapi dalam hal ini hanya memberikan sedikit keuntungan dalam hal kinerja.

Untuk mengidentifikasi operasi "lambat" yang paling umum, kami melakukan profiling kode. Berbekal data, kami mencoba untuk men-tweak mypyc sehingga akan menghasilkan kode C lebih cepat untuk operasi seperti itu, atau menulis ulang kode Python yang sesuai menggunakan operasi yang lebih cepat (dan kadang-kadang kami tidak punya solusi yang cukup sederhana untuk itu atau masalah lain). Menulis ulang kode Python sering terbukti menjadi solusi yang lebih mudah untuk masalah daripada mengimplementasikan transformasi yang sama secara otomatis di kompiler. Dalam jangka panjang, kami ingin mengotomatiskan banyak dari transformasi ini, tetapi pada saat itu kami bertujuan untuk mempercepat mypy dengan upaya minimal. Dan kami, bergerak menuju tujuan ini, memotong beberapa sudut.

Dilanjutkan ...

Pembaca yang budiman! Apa kesan Anda tentang proyek mypy ketika Anda mengetahui tentang keberadaannya?


Source: https://habr.com/ru/post/id468235/


All Articles