Bagaimana trik Pony (ORM)?

111

Pony ORM melakukan trik bagus untuk mengubah ekspresi generator menjadi SQL. Contoh:

>>> select(p for p in Person if p.name.startswith('Paul'))
        .order_by(Person.name)[:2]

SELECT "p"."id", "p"."name", "p"."age"
FROM "Person" "p"
WHERE "p"."name" LIKE "Paul%"
ORDER BY "p"."name"
LIMIT 2

[Person[3], Person[1]]
>>>

Saya tahu Python memiliki introspeksi dan metaprogramming yang luar biasa, tetapi bagaimana pustaka ini dapat menerjemahkan ekspresi generator tanpa preprocessing? Sepertinya ajaib.

[memperbarui]

Blender menulis:

Ini file yang Anda cari. Tampaknya merekonstruksi generator menggunakan beberapa sihir introspeksi. Saya tidak yakin apakah itu mendukung 100% sintaks Python, tetapi ini cukup keren. - Blender

Saya berpikir mereka menjelajahi beberapa fitur dari protokol ekspresi generator, tetapi melihat file ini, dan melihat astmodul yang terlibat ... Tidak, mereka tidak memeriksa sumber program dengan cepat, bukan? Menakjubkan...

@BrenBarn: Jika saya mencoba memanggil generator di luar selectpemanggilan fungsi, hasilnya adalah:

>>> x = (p for p in Person if p.age > 20)
>>> x.next()
Traceback (most recent call last):
  File "<interactive input>", line 1, in <module>
  File "<interactive input>", line 1, in <genexpr>
  File "C:\Python27\lib\site-packages\pony\orm\core.py", line 1822, in next
    % self.entity.__name__)
  File "C:\Python27\lib\site-packages\pony\utils.py", line 92, in throw
    raise exc
TypeError: Use select(...) function or Person.select(...) method for iteration
>>>

Sepertinya mereka melakukan lebih banyak mantra rahasia seperti memeriksa selectpemanggilan fungsi dan memproses pohon tata bahasa sintaks abstrak Python dengan cepat.

Saya masih ingin melihat seseorang menjelaskannya, sumbernya jauh melampaui tingkat sihir saya.

Paulo Scardine
sumber
Agaknya pobjek tersebut adalah objek dari tipe yang diimplementasikan oleh Pony yang melihat metode / properti apa yang sedang diakses padanya (misalnya name, startswith) dan mengubahnya menjadi SQL.
BrenBarn
3
Ini file yang Anda cari. Tampaknya merekonstruksi generator menggunakan beberapa sihir introspeksi. Saya tidak yakin apakah itu mendukung 100% sintaks Python, tetapi ini cukup keren.
Blender
1
@ Blender: Saya telah melihat trik semacam ini di LISP - melakukan aksi ini dengan Python benar-benar sakit!
Paulo Scardine

Jawaban:

209

Penulis Pony ORM ada di sini.

Pony menerjemahkan generator Python ke dalam kueri SQL dalam tiga langkah:

  1. Mendekompilasi bytecode generator dan membangun kembali AST generator (pohon sintaks abstrak)
  2. Terjemahan Python AST ke dalam "SQL abstrak" - representasi berbasis daftar universal dari kueri SQL
  3. Mengubah representasi SQL abstrak menjadi dialek SQL yang bergantung pada database tertentu

Bagian paling kompleks adalah langkah kedua, di mana Pony harus memahami "arti" dari ekspresi Python. Sepertinya Anda paling tertarik pada langkah pertama, jadi izinkan saya menjelaskan cara kerja dekompilasi.

Mari pertimbangkan kueri ini:

>>> from pony.orm.examples.estore import *
>>> select(c for c in Customer if c.country == 'USA').show()

Yang akan diterjemahkan ke dalam SQL berikut:

SELECT "c"."id", "c"."email", "c"."password", "c"."name", "c"."country", "c"."address"
FROM "Customer" "c"
WHERE "c"."country" = 'USA'

Dan di bawah ini adalah hasil dari query ini yang akan dicetak:

id|email              |password|name          |country|address  
--+-------------------+--------+--------------+-------+---------
1 |john@example.com   |***     |John Smith    |USA    |address 1
2 |matthew@example.com|***     |Matthew Reed  |USA    |address 2
4 |rebecca@example.com|***     |Rebecca Lawson|USA    |address 4

The select()Fungsi menerima generator python sebagai argumen, dan kemudian menganalisa bytecode nya. Kita bisa mendapatkan instruksi bytecode dari generator ini menggunakan dismodul python standar :

>>> gen = (c for c in Customer if c.country == 'USA')
>>> import dis
>>> dis.dis(gen.gi_frame.f_code)
  1           0 LOAD_FAST                0 (.0)
        >>    3 FOR_ITER                26 (to 32)
              6 STORE_FAST               1 (c)
              9 LOAD_FAST                1 (c)
             12 LOAD_ATTR                0 (country)
             15 LOAD_CONST               0 ('USA')
             18 COMPARE_OP               2 (==)
             21 POP_JUMP_IF_FALSE        3
             24 LOAD_FAST                1 (c)
             27 YIELD_VALUE         
             28 POP_TOP             
             29 JUMP_ABSOLUTE            3
        >>   32 LOAD_CONST               1 (None)
             35 RETURN_VALUE

Pony ORM memiliki fungsi di decompile()dalam modul pony.orm.decompilingyang dapat mengembalikan AST dari bytecode:

>>> from pony.orm.decompiling import decompile
>>> ast, external_names = decompile(gen)

Di sini, kita dapat melihat representasi tekstual dari node AST:

>>> ast
GenExpr(GenExprInner(Name('c'), [GenExprFor(AssName('c', 'OP_ASSIGN'), Name('.0'),
[GenExprIf(Compare(Getattr(Name('c'), 'country'), [('==', Const('USA'))]))])]))

Sekarang mari kita lihat bagaimana decompile()fungsinya bekerja.

The decompile()fungsi menciptakan Decompilerobjek, yang menerapkan pola pengunjung. Instance decompiler mendapatkan instruksi bytecode satu per satu. Untuk setiap instruksi, objek decompiler memanggil metodenya sendiri. Nama metode ini sama dengan nama instruksi bytecode saat ini.

Saat menghitung ekspresi, Python menggunakan tumpukan, yang menyimpan hasil penghitungan antara. Objek decompiler juga memiliki tumpukannya sendiri, tetapi tumpukan ini tidak menyimpan hasil kalkulasi ekspresi, tetapi node AST untuk ekspresi tersebut.

Ketika metode decompiler untuk instruksi bytecode berikutnya dipanggil, ia mengambil node AST dari stack, menggabungkannya menjadi node AST baru, dan kemudian meletakkan node ini di atas stack.

Misalnya, mari kita lihat bagaimana subekspresi c.country == 'USA'dihitung. Fragmen bytecode terkait adalah:

              9 LOAD_FAST                1 (c)
             12 LOAD_ATTR                0 (country)
             15 LOAD_CONST               0 ('USA')
             18 COMPARE_OP               2 (==)

Jadi, objek decompiler melakukan hal berikut:

  1. Panggilan decompiler.LOAD_FAST('c'). Metode ini menempatkan Name('c')node di atas tumpukan dekompiler.
  2. Panggilan decompiler.LOAD_ATTR('country'). Metode ini mengambil Name('c')node dari tumpukan, membuat Geattr(Name('c'), 'country')node dan meletakkannya di atas tumpukan.
  3. Panggilan decompiler.LOAD_CONST('USA'). Metode ini menempatkan Const('USA')node di atas tumpukan.
  4. Panggilan decompiler.COMPARE_OP('=='). Metode ini mengambil dua node (Getattr dan Const) dari tumpukan, dan kemudian meletakkannya Compare(Getattr(Name('c'), 'country'), [('==', Const('USA'))]) di atas tumpukan.

Setelah semua instruksi bytecode diproses, tumpukan dekompiler berisi satu node AST yang sesuai dengan ekspresi generator secara keseluruhan.

Karena Pony ORM perlu mendekompilasi generator dan lambda saja, ini tidak terlalu rumit, karena aliran instruksi untuk generator relatif mudah - ini hanya sekumpulan loop bersarang.

Saat ini Pony ORM mencakup seluruh set instruksi generator kecuali dua hal:

  1. Sejajar jika ekspresi: a if b else c
  2. Perbandingan senyawa: a < b < c

Jika Pony menemukan ekspresi seperti itu, itu akan menimbulkan NotImplementedErrorpengecualian. Tetapi bahkan dalam kasus ini Anda dapat membuatnya bekerja dengan meneruskan ekspresi generator sebagai string. Ketika Anda melewatkan generator sebagai string, Pony tidak menggunakan modul decompiler. Sebagai gantinya, ia mendapatkan AST menggunakan compiler.parsefungsi Python standar .

Semoga ini menjawab pertanyaan Anda.

Alexander Kozlovsky
sumber
26
Sangat berkinerja: (1) Penguraian bytecode sangat cepat. (2) Karena setiap kueri memiliki objek kode yang sesuai, objek kode ini dapat digunakan sebagai kunci cache. Karena ini, Pony ORM menerjemahkan setiap kueri hanya sekali, sedangkan Django dan SQLAlchemy harus menerjemahkan kueri yang sama berulang kali. (3) Karena Pony ORM menggunakan pola IdentityMap, ia menyimpan hasil kueri dalam transaksi yang sama. Ada sebuah posting (dalam bahasa Rusia) di mana penulis menyatakan bahwa Pony ORM ternyata 1,5-3 kali lebih cepat dari Django dan SQLAlchemy bahkan tanpa cache hasil query: habrahabr.ru/post/188842
Alexander Kozlovsky
3
Apakah ini kompatibel dengan kompiler JIT pypy?
Mzzl
2
Saya tidak mengujinya, tetapi beberapa pemberi komentar Reddit mengatakan itu kompatibel: tinyurl.com/ponyorm-pypy
Alexander Kozlovsky
9
SQLAlchemy memiliki caching kueri dan ORM memanfaatkan fitur ini secara ekstensif. Ini tidak aktif secara default karena sebenarnya kami tidak memiliki fitur untuk menautkan konstruksi ekspresi SQL ke posisi dalam kode sumber yang dideklarasikan, yang sebenarnya diberikan oleh objek kode kepada Anda. Kami dapat menggunakan inspeksi frame stack untuk mendapatkan hasil yang sama tetapi itu sedikit terlalu hack untuk selera saya. Generasi SQL adalah area kinerja paling kritis dalam hal apapun; mengambil baris dan perubahan pembukuan.
zzzeek
2
@ randomsurfer_123 mungkin tidak, kita hanya perlu waktu untuk mengimplementasikannya (mungkin seminggu), dan ada tugas lain yang lebih penting bagi kita.
Alexander Kozlovsky