Як Pony (ORM) робить свої хитрощі?


111

Pony ORM робить приємний трюк перетворення виразу генератора в SQL. Приклад:

>>> 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]]
>>>

Я знаю, що в Python вбудована чудова інтроспекція та метапрограмування, але як ця бібліотека здатна переводити вираз генератора без попередньої обробки? Це схоже на магію.

[оновлення]

Блендер написав:

Ось файл, який ви шукаєте. Здається, реконструюйте генератор за допомогою певного майстра самоаналізу. Я не впевнений, чи підтримує він 100% синтаксису Python, але це досить круто. - Блендер

Я думав, що вони вивчають якусь функцію з протоколу вираження генератора, але дивлячись на цей файл і бачачи astмодуль, що займається ... Ні, вони не перевіряють джерело програми на льоту, чи не так? Роздум ...

@BrenBarn: Якщо я спробую викликати генератор поза selectвикликом функції, результат:

>>> 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
>>>

Схоже, вони роблять більше прихованих закликів, як-от перевірка selectвиклику функції та обробка дерева граматики абстрактного синтаксису Python.

Я все ще хотів би, щоб хтось пояснював це, джерело виходить за рамки мого рівня майстра.


Імовірно pоб'єкт є об'єктом типу реалізованого Поні , який дивиться на те , що методи / властивість в даний час доступні на ньому (наприклад, name, startswith) і перетворює їх в SQL.
BrenBarn

3
Ось файл, який ви шукаєте. Здається, реконструюйте генератор за допомогою певного майстра самоаналізу. Я не впевнений, чи підтримує він 100% синтаксису Python, але це досить круто.
Блендер

1
@Blender: Я бачив подібний трюк у LISP - витягнути цей трюк у Python просто неприємно!
Пауло Скардін

Відповіді:


209

Автор Pony ORM тут.

Pony перекладає генератор Python у SQL-запит у три етапи:

  1. Декомпіляція байт-коду генератора та відновлення генератора AST (абстрактне синтаксичне дерево)
  2. Переклад Python AST на "абстрактний SQL" - універсальне представлення списку SQL-запиту на основі списку
  3. Перетворення абстрактного представлення SQL в конкретний діалект SQL, що залежить від бази даних

Найскладніша частина - другий крок, де Поні повинен зрозуміти "значення" виразів Python. Здається, вас найбільше цікавить перший крок, тому дозвольте мені пояснити, як працює декомпіляція.

Розглянемо цей запит:

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

Що буде переведено у наступний SQL:

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

А нижче - результат цього запиту, який буде роздруковано:

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

select()Функція приймає пітона генератор в якості аргументу, а потім аналізує його байт - код. Ми можемо отримати інструкції щодо байт-коду цього генератора, використовуючи стандартний disмодуль python :

>>> 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 має функцію decompile()в модулі, pony.orm.decompilingякий може відновити AST з байтового коду:

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

Тут ми можемо побачити текстове представлення вузлів AST:

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

Давайте тепер подивимося, як decompile()функціонує функція.

decompile()Функція створює Decompilerоб'єкт, який реалізує шаблон Visitor. Екземпляр декомпілятора отримує інструкції байт-коду по одному. Для кожної інструкції об'єкт декомпілятора викликає свій власний метод. Назва цього методу дорівнює назві поточної інструкції байт-коду.

Коли Python обчислює вираз, він використовує стек, який зберігає проміжний результат обчислення. Об'єкт декомпілятора також має власний стек, але цей стек зберігає не результат обчислення вираження, а вузол AST для виразу.

Коли викликається метод декомпілятора для наступної інструкції байт-коду, він бере вузли AST зі стека, об'єднує їх у новий вузол AST, а потім розміщує цей вузол у верхній частині стека.

Наприклад, давайте подивимось, як c.country == 'USA'обчислюється підвираз . Відповідним фрагментом байт-коду є:

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

Отже, об’єкт декомпілятора робить наступне:

  1. Дзвінки decompiler.LOAD_FAST('c'). Цей метод ставить Name('c')вузол у верхній частині стека декомпілятора.
  2. Дзвінки decompiler.LOAD_ATTR('country'). Цей метод бере Name('c')вузол зі стека, створює Geattr(Name('c'), 'country')вузол і ставить його у верхній частині стека.
  3. Дзвінки decompiler.LOAD_CONST('USA'). Цей метод ставить Const('USA')вузол поверх стека.
  4. Дзвінки decompiler.COMPARE_OP('=='). Цей метод бере два вузли (Getattr і Const) зі стека, а потім ставить Compare(Getattr(Name('c'), 'country'), [('==', Const('USA'))]) у верхній частині стека.

Після обробки всіх інструкцій байт-коду стек декомпілятора містить єдиний вузол AST, який відповідає всьому виразу генератора.

Оскільки Pony ORM потрібно декомпілювати лише генератори та лямбда, це не так вже й складно, тому що потік інструкцій для генератора порівняно простий - це лише купа вкладених циклів.

В даний час Pony ORM охоплює весь набір генераторів, крім двох речей:

  1. Вбудовані, якщо вирази: a if b else c
  2. Складні порівняння: a < b < c

Якщо Поні стикається з таким виразом, це викликає NotImplementedErrorвиняток. Але навіть у цьому випадку ви можете змусити його працювати, передаючи вираз генератора як рядок. Коли ви передаєте генератор як рядок, Pony не використовує модуль декомпілятора. Натомість він отримує AST за допомогою стандартної compiler.parseфункції Python .

Сподіваюся, що це відповість на ваше запитання.


26
Дуже добре: (1) Байт-код розкладається дуже швидко. (2) Оскільки кожен запит має відповідний об'єкт коду, цей об'єкт коду може використовуватися як кеш-ключ. Через це Pony ORM перекладає кожен запит лише один раз, тоді як Django та SQLAlchemy повинні переводити один і той же запит знову і знову. (3) Оскільки Pony ORM використовує шаблон IdentityMap, він кешує результати запитів у межах однієї транзакції. Є публікація (російською мовою), де автор заявляє, що Pony ORM виявився в 1,5-3 рази швидшим за Django та SQLAlchemy навіть без кешування результатів запитів: habrahabr.ru/post/188842
Олександр Козловський,

3
Чи сумісний це з компілятором pypy JIT?
Mzzl

2
Я його не перевіряв, але хтось із коментаторів Reddit каже, що сумісний: tinyurl.com/ponyorm-pypy
Олександр Козловський,

9
SQLAlchemy має кешування запитів, і ORM широко використовує цю функцію. Він за замовчуванням не включений, оскільки його правда у нас немає функції, яка б пов'язувала побудову вираження SQL з позицією у вихідному коді, яку він оголосив, що саме вам дає об'єкт коду. Ми могли б використовувати перевірку кадру стека, щоб отримати той самий результат, але це трохи надто хекічно на мій смак. Генерація SQL - найменш критична область продуктивності в будь-якому випадку; отримання рядків та змін у бухгалтерії.
zzzeek

2
@ randomsurfer_123, мабуть, ні, нам просто потрібен певний час для його реалізації (можливо, тиждень), і є інші завдання, які для нас важливіші.
Олександр Козловський
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.