REPL tidak berguna. Laporan Yandex

REPL (read-eval-print loop) tidak berguna dalam Python, bahkan jika itu IPython ajaib. Hari ini saya akan menawarkan salah satu solusi yang mungkin untuk masalah ini. Pertama-tama, laporan dan ekstensi saya TheREPL akan bermanfaat bagi mereka yang tertarik dalam pengembangan yang lebih cepat dan lebih efisien, serta mereka yang menulis sistem stateful.


- Nama saya Alexander, saya bekerja sebagai programmer di Yandex. Kami menulis di tim saya dengan Python, kami belum beralih ke Go. Tetapi di waktu senggang saya, anehnya, saya juga memprogram dan melakukannya dalam bahasa yang sangat dinamis - Common Lisp. Ini mungkin bahkan lebih dinamis daripada Python. Keunikannya terletak pada kenyataan bahwa proses pembangunan itu sendiri diatur agak berbeda. Ini lebih interaktif dan berulang, karena dalam REPL on Lisp Anda dapat melakukan segalanya: membuat modul baru dan menghapus modul lama, menambahkan metode, kelas dan menghapusnya, mendefinisikan kembali kelas, dll.



Dalam Python, ini semakin sulit. Ini memiliki IPython. Tentu saja, IPython meningkatkan REPL dalam beberapa cara, menambahkan pelengkapan otomatis, dan memungkinkan menggunakan ekstensi yang berbeda. Tetapi untuk pengembangan berulang, itu tidak cocok dengan baik. Di dalamnya Anda dapat mengunduh kode, mengujinya sedikit dan hanya itu. Dan kadang-kadang dia menginginkan lebih banyak interaktivitas sehingga Anda benar-benar dapat menggunakan REPL ini dalam pengembangan, beralih antar modul, mengubah fungsi dan kelas di dalamnya.

Itu terjadi pada saya - Anda menjalankan, misalnya, IPython REPL di lingkungan produksi dan Anda mulai menjalankan beberapa perintah di sana, menyelidiki sesuatu, dan kemudian ternyata ada kesalahan dalam modul, dan Anda ingin memperbaikinya dengan cepat. Tetapi ini tidak berhasil, karena Anda perlu mengumpulkan gambar Docker baru, menggulungnya menjadi produksi, masuk ke REPL ini lagi, mencapai keadaan yang diinginkan di sana lagi, memulai semua yang jatuh di atasnya lagi. Dan idealnya, saya harus memperbaiki fungsinya, segera jalankan dan langsung dapatkan hasilnya.

Apa yang bisa dilakukan tentang ini? Bagaimana saya bisa memuat ulang kode dalam IPython? Saya mencoba menggunakan autoreload dan saya tidak menyukainya karena beberapa alasan. Pertama-tama, ketika modul di-boot-ulang, ia kehilangan status yang ada di variabel global di dalam modul ini. Dan mungkin ada nilai yang di-cache dengan hasil dari beberapa fungsi. Atau saya bisa, misalnya, memuat data melalui jaringan di sana, sehingga nanti saya bisa bekerja dengannya lebih cepat. Yaitu, autoreload kehilangan status.

Karena itu, sebagai percobaan, saya membuat ekstensi sederhana untuk IPython dan menamainya TheREPL.

Saya datang kepada Anda dengan laporan ini sebagai gagasan tentang apa yang dapat dilakukan dengan REPL dengan Python. Dan saya sangat berharap Anda akan menyukai ide ini, Anda akan melakukannya di kepala Anda dan akan terus menghasilkan hal-hal yang akan membuat Python semakin efisien dan nyaman.

Apa itu TheREPL? Ini adalah ekstensi yang Anda unduh, setelah itu konsep seperti namespace muncul di IPython, dan Anda dapat mengambil dan beralih ke modul Python apa pun, melihat variabel, fungsi, dan sebagainya apa yang ada. Dan yang lebih penting, Anda dapat langsung menulis def, nama fungsi, mendefinisikan kembali fungsi atau kelas, dan itu akan berubah di semua modul di mana itu diimpor. Tetapi pada saat yang sama, modul itu sendiri tidak me-restart, sehingga negara disimpan. Selain itu, TheREPL memungkinkan Anda untuk menghindari lebih banyak artefak yang ada dalam pengisian otomatis dan yang sekarang akan kita lihat.



Jadi, dalam autoreload, peningkatan kode hanya terjadi ketika file disimpan. Tetapi pada saat yang sama, Anda perlu memasukkan sesuatu ke dalam REPL itu sendiri, dan baru kemudian autoreload akan mengambil perubahan ini. Ini adalah masalah nomor 1. Artinya, jika Anda memiliki semacam proses latar belakang di utas terpisah (misalnya, server sedang berjalan), Anda tidak bisa hanya mengambil dan memperbaiki kode. Autoreload tidak akan menerapkan perubahan ini sampai Anda memasukkan sesuatu ke dalam IPython REPL.

Dalam kasus ekstensi saya, Anda menekan pintasan tepat di editor, dan fungsi yang berada di bawah kursor segera diterapkan dan mulai berfungsi. Artinya, menggunakan TheREPL, Anda dapat mengubah kode lebih terperinci. Anda juga dapat menulis def di IPython.



Beralih antar modul, seperti yang saya katakan, pengisian otomatis tidak mendukung dengan cara apa pun. Anda hanya dapat menemukan file di sistem file, mengubahnya dan berharap bahwa autoreload akan menyelesaikan semua yang ada di sana.



Lebih jauh. Autoreload kehilangan variabel global, TheREPL menyimpan dan memungkinkan Anda untuk terus meneliti operasi aplikasi Anda, mengubah kode internalnya dan karenanya mengembangkannya dengan cepat.



Autoreload masih memiliki fitur ini. Dia dengan sangat licik menerapkan perubahan pada modul yang memuat ulang. Secara khusus, dia melakukan trik yang sangat menarik di sana. Jika fungsi dalam modul ini telah diperbarui, maka untuk mengubahnya di mana saja telah diimpor, ia menggunakan pengumpul sampah untuk menemukannya dan semua contoh fungsi ini dan mengubah kode di dalamnya. Lebih jauh kita akan melihat contoh bagaimana ini terjadi. Karena ini, kode fungsi berubah, bahkan jika itu masuk ke penutupan.

Apakah Anda tahu apa itu penutupan? Ini adalah hal yang sangat berguna. Pengembang JavaScript selalu menggunakan ini. Anda, kemungkinan besar, juga tidak pernah memperhatikan. Tetapi karena autoreload melakukan apa yang saya jelaskan di atas, Anda mungkin menemukan diri Anda dalam situasi di mana kode lama menggunakan kode baru yang mungkin bekerja secara berbeda. Misalnya, suatu fungsi dapat mengembalikan bukan hanya satu nilai, tetapi dua, tuple bukan string, dll. Kode lama akan merusak ini.

TheREPL tidak melakukan hal rumit seperti itu secara khusus untuk memastikan bahwa semuanya lebih konsisten. Yaitu, itu mengubah fungsi atau kelas dalam modul di mana ia didefinisikan. Temukan kelas ini di semua modul lain, dan ubah juga di sana. Setelah itu, semuanya bekerja dengan cara yang baru.



Bagaimana cara mengganti fungsi yang dimuat secara otomatis? Kami memiliki dua fungsi, satu dan dua. Setiap fungsi memiliki seperangkat atribut: dokumentasi, kode, argumen, dll. Di sini, di slide adalah contoh mengganti atribut di mana bytecode disimpan.

Setelah autoreload mengubahnya, fungsi yang dipanggil mulai bekerja secara berbeda. Tapi ini adalah contoh sintetis yang baru saja saya ulangi dengan tangan saya sehingga Anda mengerti apa yang terjadi. Fungsi ini dipanggil dalam satu cara, tetapi kode di sana sebenarnya berbeda. Dan jika Anda membongkar, itu juga menunjukkan bahwa ia mengembalikan deuce. Apa yang menyebabkan ini?



Ini adalah contoh penutupan. Pada baris kedua, kami membuat penutupan di mana kami menangkap fungsi foo. Penutupan itu sendiri mengharapkan bahwa fungsi yang kita lewati ini mengembalikan sebuah string, mengkodekannya dalam utf-8 dan semuanya berfungsi.



Tapi misalkan Anda mengubah modul di mana foo didefinisikan, dan memuat secara otomatis mengambil perubahan. Dan Anda mengubahnya sehingga tidak mengembalikan string, tetapi angka. Maka penutupan sudah akan bekerja secara salah, karena fungsi di dalamnya telah berubah di dalam, tetapi penutupan tidak mengharapkan ini, itu tidak berubah. Dan masalah dengan autoreload seperti itu dapat "menembak" di tempat yang tidak terduga.



Bagaimana cara autoreload kelas pembaruan? Sangat sederhana. Ini memperbarui semua metode kelas dengan cara yang sama seperti fungsi, dan juga memperbarui atribut __class__ untuk semua contoh sehingga resolusi metode (menentukan metode mana yang harus dipanggil) mulai bekerja dengan cara baru.

Semuanya sedikit lebih rumit di TheREPL, karena ketika Anda memperbarui _class_, mungkin ternyata memiliki beberapa turunan, kelas anak, yang juga perlu diperbarui, karena ada sesuatu yang berubah dalam daftar kelas dasar.

Untuk mengatasi masalah ini, Anda dapat membangun kembali kelas. Tapi pertama mari kita lihat apa yang terjadi dengan autoreload ketika memuat ulang modul.



Ini adalah contoh yang bagus. Ada dua modul - a dan b. Dalam modul a, kelas induk didefinisikan, dalam modul b kelas anak, dan kami membuat turunan dari kelas anak. Dan baris 10 menunjukkan bahwa ya, ini adalah turunan dari kelas Foo, induknya.



Selanjutnya, kita ambil dan ganti modul a. Misalnya, tambahkan dokumentasi ke kelas Foo. Kemudian autoreload mengambil perubahan ini. Menurut Anda apa dalam hal ini ia akan kembali dari Bar?



Dan itu kembali salah, karena autoreload telah mengubah kelas Foo, dan sekarang ini adalah kelas yang sama sekali berbeda, bukan yang dari mana kelas Bar diwarisi.



Dan sebuah kejutan! Dalam dua modul a dan b, kelas Foo adalah kelas yang berbeda, dan Bar mewarisi dari salah satunya. Karena tiang tembok seperti itu, sangat sulit untuk memprediksi bagaimana kode Anda akan berfungsi setelah pengisian otomatis dilakukan sesuatu di dalamnya.



Sesuatu seperti ini, memperbarui kelas. Saya akan mengomentari gambar. Awalnya, kelas Foo diimpor ke modul b, dan tetap ada di sana. Saat mengganti pengisian otomatis, modul ini akan dipindahkan, dan kelas baru muncul di sana, dan di modul b tidak diperbarui.



TheREPL memang sedikit berbeda. Dia menyuntikkan kelas yang dimodifikasi ke setiap modul tempat dia diimpor. Karena itu, semuanya bekerja dengan benar di sana. Apalagi, jika ada benda di kelas, mereka akan dilestarikan.



Dan inilah cara TheREPL memecahkan masalah dengan kelas anak. Yaitu, ketika kelas induk telah berubah, ia mendefinisikan daftar kelas dasar melalui mro atribut sulap (urutan resolusi metode). Atribut ini berisi daftar kelas dalam urutan di mana Anda ingin mencari metode atau atribut di dalamnya. Dan setiap kali Anda memanggil metode get_name pada objek Anda, misalnya, Python pertama-tama akan memeriksanya di kelas Bar, lalu di kelas Foo, lalu di kelas objek, jika tidak menemukannya. Kerjanya sesuai dengan prosedur urutan resolusi metode.

TheREPL menggunakan chip ini. Dibutuhkan daftar kelas dasar, perubahan di sana kelas yang baru saja Anda ubah ke yang baru. Menciptakan tipe anak baru, ini adalah langkah kedua. Dengan fungsi type, Anda sebenarnya dapat membuat kelas. Jika Anda belum pernah menggunakannya - cobalah, itu menyenangkan.

Anda hanya mengatakan nama kelas, katakan apa kelas dasarnya. Dalam kasus paling sederhana, misalnya, objek. Dan - kamus dengan metode dan atribut kelas. Semuanya, Anda memiliki kelas baru yang dapat Anda instantiate, seperti biasa. TheREPL memanfaatkan chip ini. Ini menghasilkan kelas anak dan mengubah pointer ke dalamnya di semua objek dari kelas Bar lama.

Saya masih punya demo, mari kita lihat cara kerjanya. Pertama, mari kita lihat hal yang sangat sederhana.

Demo pertama

Saya mengatakan bahwa Anda dapat mengubah kode di dalam modul. Misalkan kita memiliki server. Saya akan menjalankannya sekarang. Pada titik tertentu, kami menemukan bahwa karena alasan tertentu ia membuat direktori sementara. Atau dia mulai menciptakan, tetapi sebelum itu dia tidak menciptakan. Kemudian kita dapat terhubung ke server ini dan, menduga bahwa itu mungkin membuat direktori ini menggunakan fungsi mkdtemp dari modul file, Anda dapat langsung menuju ke modul Python ini.

Lihat - di sudut nama modul saat ini telah berubah. Sekarang katanya tempfile. Dan saya bisa melihat fitur apa saja yang ada. Kami melihat mereka, dan kami dapat, penting, mendefinisikan kembali mereka. Saya telah menyiapkan pembungkus khusus yang memungkinkan Anda untuk menghias fungsi apa pun sehingga dengan semua panggilannya Anda dapat melihat jejak dari tempat namanya. Sekarang kita akan mengimpor dan menerapkannya.

Artinya, saya membungkus fungsi Python standar, bahkan tidak memiliki akses ke kode sumber untuk modul ini. Saya bisa mengambil dan membungkusnya. Dan pada output selanjutnya, kita akan melihat Traceback dan menemukan dari mana asalnya.

Dengan cara yang sama, perubahan ini dapat diputar kembali sehingga tidak mengirim spam kepada kami. Yaitu, kita melihat bahwa server ini di dalam pekerja pada baris kedelapan memanggil mkdtemp dan terus memproduksi direktori sementara untuk kita, mengacaukan sistem file. Ini adalah satu aplikasi.

Mari kita lihat contoh lain mengapa autoreload terkadang tidak bekerja dengan baik. Saya memiliki bot telegram yang disiapkan:

Demo kedua

Sekarang kami mengaktifkan pengisian otomatis dan melihat bagaimana ini membantu kami. Itu saja, sekarang Anda dapat memulai bot dan berbicara dengannya. Agar Anda dapat melihat dengan lebih baik, kami akan memulai dialog dengannya. Mengenal bot. Begitu. Ada semacam kesalahan. Kesalahan yang sama sekali berbeda dikandung, dan saya memutuskan untuk membuat perubahan pada saat terakhir. Tapi itu tidak masalah. Sekarang kita akan memperbaikinya, autoreload akan membantu kita dengan ini.

Kami beralih ke bot. Dan sekarang saya akan sementara mengomentari ini, jika demikian. Saya menyimpan file. autoreload, secara teori, harus menangkap perubahan ini. Mulai bot lagi. Bot mengenali saya. Mari kita bicara dengannya.

Kesalahan lain Dia sudah dikandung. Ayo kita perbaiki. Saya akan meninggalkan bot, itu akan bekerja di latar belakang, saya akan beralih ke editor, dan di editor kita akan menemukan kesalahan ini. Ini hanya kesalahan ketik, dan saya lupa bahwa variabel saya disebut user_name. Saya menyimpan file. autoreload seharusnya menangkapnya, dan sekarang kita akan melihatnya.

Tapi autoreload, seperti yang sudah saya sebutkan, tidak tahu apa-apa tentang fakta bahwa file telah berubah sampai Anda memasukkan sesuatu ke dalamnya. Dengan proses yang panjang ... Itu perlu disela, dimulai kembali. Selesai Kembali ke bot kami, tulis padanya. Begini, bot itu lupa bahwa nama saya Sasha. Mengapa? autoreload membuatnya kembali karena memuat ulang seluruh modul sepenuhnya. Dan saya perlu menulis ke bot lagi, untuk memulihkan kondisinya.

Dan jika Anda men-debug beberapa jenis kesalahan yang terjadi di negara tertentu, maka negara tidak dapat hilang, karena jika tidak, Anda akan menghabiskan banyak waktu lagi untuk mencapai keadaan ini. TheREPL membantu hanya dalam kasus-kasus seperti itu.

Mari kita lihat bagaimana bot akan diperbarui jika menggunakan TheREPL. Untuk kemurnian percobaan, saya akan me-restart IPython dan kami akan mengulanginya lagi.

Dan sekarang saya mengunduh TheREPL. Dia segera mulai mendengarkan pada port tertentu sehingga Anda dapat mengirim kode di dalamnya. By the way, ini dapat dilakukan bahkan jika IPython berjalan di suatu tempat di server dan editor berjalan secara lokal, yang juga dapat membantu Anda dalam beberapa kasus.

Kami mengimpor bot, memulainya, menulis lagi. Di sini jelas - kami memulai ulang Python, jadi ia tidak ingat siapa saya. Periksa apakah ada kesalahan di dalamnya. Ya, ada kesalahan. Baiklah, mari kita selesaikan.

Saya beralih kembali ke editor, memperbaiki kesalahan. Kami bahkan tidak harus menyimpan file, saya tekan Ctrl-C, Ctrl-C, ini adalah jalan pintas yang digunakan Emacs untuk mengambil deskripsi fungsi saat ini yang berada tepat di bawah kursor dan mengirimkannya ke proses Python yang terhubung. Itu saja, sekarang kita bisa melihat dan memeriksa bagaimana bot kami menanggapi pesan saya di sana. Sekarang, dia ingat bahwa saya Sasha, dan dengan jujur ​​menjawab bahwa dia tidak tahu caranya.

Mari kita coba tambahkan fungsionalitas baru secara langsung di sana. Untuk melakukan ini, kembali ke editor. Misalnya, tambahkan perintah bantuan. Untuk saat ini, biarkan dia menjawab bahwa dia tidak tahu apa-apa tentang bantuan. Sekali lagi, tekan Ctrl-C, Ctrl-C, kode diterapkan. Kami pergi ke bot. Lihat apakah dia mengerti perintah ini. Ya, tim telah mendaftar.

Ngomong-ngomong, dia masih memiliki hal seperti itu, sekarang kita akan melihat bagaimana kelas akan berubah. Dia memiliki perintah status, perintah debugging khusus untuk melihat status bot. Jadi, beberapa Oleg terhubung. Menarik.

Ketika bot mengeksekusi perintah ini, ia memanggil reply untuk melihat representasi dari bot. Kita dapat pergi dan memperbaiki, misalnya, balasan ini dengan sesuatu yang lain. Misalnya, buat sedemikian rupa sehingga hanya nama yang dimasukkan. Anda bisa melakukannya. Kami kembali ke messenger kami, sekali lagi mengeksekusi negara. Dan itu saja. Sekarang balasan berfungsi dengan cara baru, tetapi objeknya sama, ia telah mempertahankan kondisinya, karena ia mengingat kita semua - Oleg, Sasha, kek dan "DROP TABLE Users, Alex"!

Dengan demikian, Anda dapat menulis dan men-debug kode secara langsung, tanpa beralih ke siklus ini, ketika Anda perlu mengumpulkan paket, gulung di suatu tempat. Anda dapat dengan cepat menguji sesuatu, mengubah semua yang Anda butuhkan, dan hanya kemudian semua perubahan ini harus dikemas dengan benar dan digunakan.

Tentu, Anda tidak boleh melakukan ini dalam produksi nyata, karena dengan pendekatan ini masalah apa yang bisa terjadi. Anda mungkin lupa bahwa kode yang baru Anda mulai di server harus disimpan dan kemudian digunakan sebagaimana mestinya. Pendekatan ini membutuhkan disiplin. Tetapi dalam proses pengembangan dan debugging pada beberapa jenis pengujian, ini adalah hal yang hebat.

Pastikan untuk membuat plugin untuk PyCharm. Jika ada sukarelawan yang akan membantu saya dengan Kotlin dan plugin PyCharm, saya akan senang berbicara. Tulis saya dalam surat atau telegram .

* * *

Terhubunguntuk pengembangan TheREPL. Ada banyak lagi chip yang bisa Anda pikirkan. Sebagai contoh, Anda dapat menemukan cara untuk memperbarui instance kelas ketika mereka memutakhirkan, menambahkan atribut baru di sana atau memutakhirkan status mereka dengan cara apa pun. Demikian pula, kami akan memutakhirkan basis data. Sekarang ini bukan.

Anda dapat membuat kode hot-reload untuk produksi sehingga ketika perubahan baru datang kepada Anda, Anda tidak harus me-restart server. Anda dapat menghasilkan lebih banyak. Ini hanya sebuah ide, dan saya ingin Anda keluar dari sini. Kita harus menyesuaikan semuanya untuk diri kita sendiri dan membuatnya nyaman. Itu saja untuk saya.

All Articles