Как Pony (ORM) делает свои трюки?

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.

Я все еще хотел бы, чтобы кто-то объяснил это, источник выходит за рамки моего уровня волшебства.

Ответ 1

Автор Pony ORM здесь.

Pony преобразует генератор Python в SQL-запрос в три этапа:

  • Декомпиляция генератора байт-кода и генератора восстановления AST (абстрактное синтаксическое дерево)
  • Перевод Python AST на "абстрактный SQL" - универсальный представление SQL-запроса на основе списка
  • Преобразование абстрактного представления 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 |[email protected]   |***     |John Smith    |USA    |address 1
2 |[email protected]|***     |Matthew Reed  |USA    |address 2
4 |[email protected]|***     |Rebecca Lawson|USA    |address 4

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

>>> 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, который реализует шаблон посетителя. Экземпляр декомпилятора получает команды bytecode один за другим. Для каждой команды объект декомпилятора вызывает свой собственный метод. Имя этого метода равно имени текущей инструкции байт-кода.

Когда Python вычисляет выражение, он использует стек, который хранит промежуточный результат расчета. Объект декомпилятора также имеет свой собственный стек, но этот стек хранит не результат вычисления выражения, но AST node для выражения.

Когда вызывается метод декомпилятора для следующей инструкции байт-кода, он принимает узлы AST из стека, объединяет их в новый AST node, а затем помещает этот node в верхнюю часть стека.

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

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

Итак, объект декомпилятора делает следующее:

  • Вызов decompiler.LOAD_FAST('c'). Этот метод помещает Name('c') node в верхнюю часть стека декомпилятора.
  • Вызов decompiler.LOAD_ATTR('country'). Этот метод принимает Name('c') node из стека, создает Geattr(Name('c'), 'country') node и помещает его в верхнюю часть стека.
  • Вызов decompiler.LOAD_CONST('USA'). Этот метод помещает Const('USA') node поверх стека.
  • Вызов decompiler.COMPARE_OP('=='). Этот метод принимает два узла (Getattr и Const) из стека, и затем помещает Compare(Getattr(Name('c'), 'country'), [('==', Const('USA'))]) в верхней части стека.

После обработки инструкций байткода стек декомпилятора содержит один AST node, который соответствует всему выражению генератора.

Поскольку Pony ORM необходимо декомпилировать генераторы и только лямбда, это не так сложно, потому что поток команд для генератора относительно прост - это всего лишь куча вложенных циклов.

В настоящее время Pony ORM охватывает все команды генератора, за исключением двух вещей:

  • Inline if выражения: a if b else c
  • Сравнительные сравнения: a < b < c

Если Pony сталкивается с таким выражением, он вызывает исключение NotImplementedError. Но даже в в этом случае вы можете заставить его работать, передав выражение генератора в виде строки. Когда вы передаете генератор в виде строки, Pony не использует модуль декомпилятора. Вместо он получает AST, используя стандартную функцию Python compiler.parse.

Надеюсь, это ответит на ваш вопрос.