Macro untuk pythonist. Laporan Yandex

Bagaimana saya bisa memperpanjang sintaks Python dan menambahkan fitur yang diperlukan untuk itu? Musim panas lalu di PyCon saya mencoba memahami topik ini. Dari laporan ini Anda dapat mengetahui bagaimana perpustakaan pola, macropy, pytest diatur dan bagaimana mereka mencapai hasil yang menarik. Pada akhirnya ada contoh pembuatan kode menggunakan makro di HyLang, bahasa mirip Lisp yang berjalan di atas Python.


- Hai kawan. Pertama-tama, saya ingin mengucapkan terima kasih kepada penyelenggara PyCon. Saya seorang pengembang di Yandex. Laporan ini bukan tentang pekerjaan sama sekali, tetapi tentang hal-hal eksperimental. Mungkin mereka akan mengarahkan salah satu dari Anda pada gagasan bahwa dengan Python Anda dapat melakukan hal-hal keren yang bahkan tidak Anda ketahui sebelumnya, tidak berpikir ke arah ini.

Sedikit bagi mereka yang tidak mengetahui apa itu makro: ini adalah metode pembuatan kode ketika beberapa ekspresi dalam bahasa diperluas menjadi kode yang lebih kompleks. Apa barangnya untukmu? Bagi Anda, catatan makro singkat, itu mengungkapkan beberapa abstraksi, tetapi itu banyak pekerjaan di bawah tenda untuk Anda, dan Anda tidak perlu menulis semua kode ini dengan tangan Anda.

pytest


Kemungkinan besar, Anda menemukan kerangka uji pytest, banyak di sini hampir pasti menggunakannya. Saya tidak tahu apakah Anda pernah memperhatikan, tetapi di bawah tenda ia juga melakukan sihir.



Misalnya, Anda memiliki tes sederhana. Jika Anda menjalankannya tanpa pytest, maka ia akan melempar AssertionError dengan sederhana.



Sayangnya, contoh saya sedikit merosot, dan di sini segera jelas bahwa len diambil dari daftar tiga elemen. Tetapi jika beberapa fungsi dipanggil, maka Anda tidak akan pernah tahu dari AssertionError sedemikian rupa sehingga fungsi tersebut dikembalikan. Dia mengembalikan sesuatu yang tidak sama dengan seratus.



Namun, jika ini dijalankan di bawah pytest, maka itu akan menampilkan informasi debug tambahan. Bagaimana dia melakukannya di dalam?



Sihir ini bekerja sangat sederhana. Pytest membuat kait khusus sendiri yang menyala ketika modul dengan tes memuat. Setelah itu, pytest secara independen mem-parsing file Python ini, dan sebagai hasil parsing, representasi perantara diperoleh, yang disebut AST-tree. Pohon AST adalah konsep dasar yang memungkinkan Anda untuk mengubah kode Python dengan cepat.

Setelah menerima pohon seperti itu, pytest memaksakan transformasi di atasnya yang mencari semua ekspresi yang disebut assert. Dia mengubahnya dengan cara tertentu, dia mengkompilasi pohon AST baru yang dihasilkan, dan dia mendapat modul dengan tes, yang kemudian berjalan pada Mesin Virtual Python biasa.



Seperti inilah bentuk pohon AST asli yang tidak dikonversi menjadi pytest. Area merah yang disorot adalah Assert kami. Jika Anda melihat lebih dekat, Anda akan melihat bagian kiri dan kanannya, daftar itu sendiri.

Ketika pytest mengkonversi ini dan menghasilkan tahun baru, pohon itu mulai terlihat seperti ini.



Ada sekitar seratus baris kode yang dihasilkan pytest untuk Anda.



Jika Anda mengubah pohon AST ini kembali ke Python, itu akan terlihat seperti ini. Area yang disorot dengan warna merah di sini adalah tempat pytest menghitung bagian kiri dan kanan ekspresi, menghasilkan pesan kesalahan, dan memunculkan AssertionError jika ada yang salah dengan pesan kesalahan ini.

Pencocokan pola


Apa lagi yang bisa Anda lakukan dengan hal seperti itu? Anda dapat mengonversi kode Python apa pun. Dan ada satu perpustakaan indah yang saya temukan secara tidak sengaja di PyPI, menarik untuk digali di sana. Dia melakukan pencocokan pola.



Mungkin kode ini tidak asing bagi seseorang. Dia menganggap faktorial secara rekursif. Mari kita lihat bagaimana hal itu dapat direkam menggunakan pencocokan pola.



Untuk melakukan ini, cukup gantung dekorator pada fungsi. Harap dicatat: di dalam tubuh, fungsinya sudah bekerja secara berbeda. Masing-masing ifs adalah aturan untuk pencocokan pola, yang mem-parsing ekspresi yang merupakan input ke fungsi dan entah bagaimana mengubahnya. Selain itu, bahkan tidak ada pengembalian eksplisit dari hasilnya. Karena pustaka pola, ketika ia mentransformasikan fungsi tubuh, pertama, ia memeriksa bahwa ia hanya berisi jika, dan kedua, ia menambahkan pengembalian implisit dari hasilnya, sehingga mengubah semantik bahasa. Artinya, dia membuat DSL baru, yang bekerja sedikit berbeda. Dan berkat ini, Anda dapat menuliskan beberapa hal secara deklaratif.


Fungsi sebelumnya seolah ditulis dalam tiga baris.





Dan sisa baris menambahkan fungsionalitas tambahan yang memungkinkan, misalnya, membaca faktorial dari daftar nilai atau melewatkannya melalui fungsi sewenang-wenang.

Bagaimana cara menulis konversi sendiri? macropy!


Sekarang Anda mungkin bertanya-tanya, tetapi bagaimana Anda bisa menerapkannya sendiri? Karena itu membosankan untuk dilakukan, seperti pytest: file parse secara manual, cari kode yang perlu dikonversi. Dalam pytest, ini dilakukan oleh modul terpisah untuk seribu atau lebih baris.

Agar tidak melakukan ini sendiri, beberapa orang pintar sudah membuat modul untuk kami yang disebut macropy.

Versi modul ini adalah untuk Python kedua dan ketiga. Mereka menulisnya kembali pada zaman Python kedua. Kemudian orang-orang punya lelucon untuk mencari tahu apa yang bisa dilakukan dengan Python, dan perpustakaan menyertakan berbagai contoh. Mari kita lihat mereka, mereka akan memberi Anda gambaran tentang apa yang dapat Anda lakukan dengan teknik ini. Hal keren pertama yang mereka jelaskan dalam tutorial adalah makro yang mengimplementasikan string format untuk Python kedua, seperti yang ketiga.



Ekspresi yang disorot dengan warna merah hanyalah sintaks dari panggilan makro. Huruf S adalah nama makro, dan kemudian dalam tanda kurung siku adalah ekspresi yang dikonversi. Akibatnya, variabel diganti di sini. Ini berfungsi dalam Python kedua, tetapi yang ketiga tidak lagi diperlukan dalam makro seperti itu. Jadi, misalnya, Anda dapat membuat makro Anda sendiri, yang mengimplementasikan semantik yang lebih kompleks dan melakukan lebih banyak hal menyenangkan daripada string format standar.



Ketika makro berkembang, dan ini terjadi pada saat memuat modul, itu hanya mengkonversi ke kode itu. Placeholder dimasukkan ke dalam format string dan prosedur substitusi diterapkan padanya. Selanjutnya Python sudah dengan cara standar mengkompilasi semua ini. Dalam runtime, tidak ada ekspansi makro yang terjadi. Semua itu terjadi ketika modul dimuat. Oleh karena itu, pada hal seperti itu, Anda bahkan dapat membuat optimasi atau perhitungan yang akan terjadi pada saat memuat modul dan menghasilkan bytecode yang lebih optimal.



Contoh kedua juga menarik. Ini adalah notasi singkat untuk menulis lambda. Makro f mengambil serangkaian argumen dan mengembalikan fungsi sebagai gantinya. Setiap ekspresi dimulai dengan nama makro "f", tanda kurung, dan kemudian benar-benar ekspresi apa pun dikonversi ke lambda.



Menurut pendapat saya, ini juga keren, terutama bagi mereka yang suka mengembangkan dan menulis kode dalam gaya fungsional dan menggunakan MapReduce.


Ini adalah contoh lain yang sudah dikenal. Fungsi ini mempertimbangkan faktorial, kode disorot dengan warna merah. Apa yang akan terjadi ketika dia dipanggil?



Ini akan melempar kesalahan dalam Python, karena akan berjalan ke batas tumpukan dan akan ada RecursionError yang jelek.



Bagaimana ini bisa diperbaiki? Menggunakan macropy, memperbaiki masalah sangat sederhana.



Anda menggantung dekorator, dibutuhkan fungsi tubuh dan mengubahnya dengan cara ajaib. Anda tidak perlu mengubah apa pun dalam fungsi itu sendiri, makropi akan melakukan segalanya untuk Anda.



Dan fungsinya akan kembali ke hasil yang cukup normal, pergi jauh ke bawah tanah.


Bagaimana macropy melakukannya?



Ini menggantikan semua panggilan ke fungsi itu sendiri dengan objek TailCall khusus, yang kemudian dipanggil secara loop oleh dekorator TCO.



Rangkaiannya terlihat seperti ini. Dekorator dalam loop memanggil fungsi hingga mengembalikan beberapa hasil normal alih-alih TailCall. Dan jika dia kembali, maka kembalikan. Dan itu saja. Hal-hal keren ini bisa dilakukan dengan makro!

Makropi juga termasuk contoh lain. Saya harap mereka yang penasaran dengan Anda pergi dan melihatnya sendiri. Katakanlah ada beberapa hal yang berguna untuk debugging.



Saya akan bercerita tentang hal keren lainnya. Salah satu contoh adalah makro kueri ini. Apa yang dia lakukan? Di dalamnya, Anda menulis kode Python biasa, yang kemudian dapat Anda gunakan sebagai hasil biasa dari mengeksekusi ungkapan ini. Tapi di dalam, makropi mengubah kode ini dan membuatnya menjadi kode bahasa query Alchemy SQL.



Dia menulis ulang untukmu, membuat ekspresi yang mengerikan ini. Itu dapat ditulis ulang dengan tangan, maka akan lebih pendek. Saya melakukannya.



Inilah ekspresi aslinya. Setelah memperluas makro, dibutuhkan sesuatu seperti ini.



Mungkin seseorang tertarik untuk menulis kode yang lebih mirip dengan Python, dan tidak memaksa pengembang mereka untuk menulis kueri pada DSL Alchemy SQL.

Dengan cara yang sama, Anda dapat menghasilkan apa pun dari Python - SQL murni, JavaScript - dan simpan di suatu tempat di sebelah file, lalu gunakan di frontend.



Sekarang mari kita lihat bagaimana membuat makro Anda sendiri. Dengan macropy, itu sangat sederhana.

Makro adalah fungsi yang mengambil pohon AST pada input dan, entah bagaimana mengubahnya, mengembalikan yang baru. Berikut adalah contoh makro yang menambahkan deskripsi ke panggilan tegas yang berisi ekspresi sumber sehingga kita bisa memahami mengapa kesalahan AssertionError terjadi.

Di sini, fungsi replace_assert internal adalah pembantu. Dia melakukan keturunan rekursif di pohon untuk Anda. Di dalam replace_assert, elemen subtree dilewatkan.



Karena ini, Anda dapat memeriksa jenis dan dalam? jika ini panggilan yang tegas, lakukan sesuatu dengannya. Di sini saya akan memberikan contoh sintetis sederhana yang mengambil bagian kiri, bagian kanan, membuat pesan kesalahan dari mereka, dan menulis semuanya ke atribut msg. Ini adalah pesan yang perlu dikembalikan.







Saat menggunakannya, Anda melampirkan makro seperti itu ke blok kode menggunakan dengan konteks konteks, dan semua kode yang masuk ke dalam konteks konteks melewati transformasi ini. Terlihat di bawah ini bahwa pesan kesalahan kami telah ditambahkan ke AssertionError, yang kami bentuk dari ekspresi len ([1, 2, 3]).



Namun, metode ini memiliki satu keterbatasan yang membuat saya pribadi sedih. Saya mencoba sebagai percobaan untuk membuat desain baru yang akan berfungsi dalam bahasa tersebut. Sebagai contoh, beberapa orang menyukai switch atau konstruksi kondisional seperti kecuali. Tetapi sayangnya, ini tidak mungkin: makropi dan alat lain yang bekerja dengan pohon AST digunakan ketika kode sumber sudah dibaca dan dipecah menjadi token. Kode ini dibaca oleh parser Python, yang tata bahasanya diperbaiki pada interpreter. Untuk mengubahnya, Anda perlu mengkompilasi ulang Python. Tentu saja, Anda bisa melakukan ini, tetapi itu sudah menjadi garpu dari Python, dan bukan perpustakaan yang bisa diletakkan di PyPI. Oleh karena itu, konstruksi seperti itu tidak mungkin menggunakan makropi.

HyLang


Untungnya, untuk umur panjang saya, saya menulis tidak hanya dengan Python dan tertarik pada berbagai bahasa alternatif lainnya. Ada sintaksis yang banyak tidak suka, tetapi lebih sederhana dan fleksibel. Ini adalah ekspresi-s.

Untungnya bagi kami, ada add-in Python yang disebut HyLang. Hal ini agak mengingatkan pada Clojure, hanya Clojure yang berjalan di atas JVM, dan HyLang berjalan di atas Python Virtual Machine. Artinya, ini memberi Anda sintaks baru untuk menulis kode. Tetapi pada saat yang sama, semua kode yang Anda tulis akan sepenuhnya kompatibel dengan pustaka Python yang ada, dan dapat digunakan dari pustaka Python.



Itu terlihat seperti ini.



Bagian di sebelah kiri ditulis dengan Python, di sebelah kanan - di HyLang. Dan dari bawah untuk keduanya adalah bytecode, yang hasilnya. Anda mungkin memperhatikan bahwa itu persis sama, hanya sintaks yang berubah. Ekspresi HyLang, yang tidak disukai banyak orang. Penentang "kurung" tidak mengerti bahwa sintaksis seperti itu memberikan kekuatan yang luar biasa pada bahasa karena memberikan keseragaman pada konstruksi bahasa. Dan keseragaman memungkinkan Anda menggunakan makro untuk mengimplementasikan desain apa pun.

Ini dicapai karena fakta bahwa di dalam setiap ekspresi elemen pertama selalu semacam tindakan. Dan kemudian argumennya pergi.

Dan semua kode terdiri dari ekspresi bersarang yang mudah dikonversi dan membuka makro di sana. Karena ini, tentu saja konstruksi apa pun dapat dibuat di HyLang, baru, yang sama sekali tidak dapat dibedakan dalam kode dari fitur standar bahasa.



Mari kita lihat bagaimana makro sederhana bekerja di HyLang. Untuk melakukan hal yang sama yang kami lakukan dengan Assert menggunakan macropy, Anda hanya perlu kode ini.

Makro HyLang kami menerima input, yaitu kode. Lebih lanjut, makro dapat dengan mudah menggunakan bagian mana pun dari kode ini untuk membuat kode baru. Perbedaan utama antara makro dan fungsi: ekspresi adalah input, bukan nilai. Jika kami menyebut makro kami sebagai (is (= 1 2)) maka ia akan menerima ekspresi (= 1 2) alih-alih Salah.



Jadi kami dapat membuat pesan kesalahan bahwa ada sesuatu yang salah.



Dan kemudian kembalikan kode yang baru. Sintaks backtick dan tilde ini memiliki arti seperti berikut ini. Kutipan belakang mengatakan: ambil ungkapan ini apa adanya dan kembalikan seperti apa adanya. Dan tilde mengatakan: gantilah nilai variabel di sini.



Oleh karena itu, ketika kami menulis ini, makro pada ekspansi akan kembali kepada kami ekspresi baru, yang karenanya akan ditegaskan dengan pesan kesalahan tambahan.

Hyang adalah hal yang keren. Benar, sementara kita tidak menggunakannya. Mungkin kita tidak akan pernah melakukannya. Semua item ini bersifat eksperimental. Saya ingin Anda pergi dari sini dengan perasaan bahwa dengan Python Anda dapat melakukan beberapa hal yang mungkin tidak pernah terpikirkan sebelumnya. Dan mungkin beberapa dari mereka akan menemukan aplikasi praktis dalam pekerjaan Anda yang sedang berlangsung.

Itu semua untuk saya. Anda dapat melihat tautannya:

  • Pola ,
  • MakroPy ,
  • HyLang ,
  • Buku OnLisp - untuk studi lanjutan tentang kemampuan makro. Ini untuk mereka yang sangat tertarik. Benar, buku ini tidak sepenuhnya didasarkan pada Python, tetapi pada Common Lisp. Tetapi untuk studi yang lebih dalam, ini bahkan akan menarik.

All Articles