Как 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, но это довольно круто.
Blender
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. Встроенные выражения if: 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 08
2
@ randomsurfer_123, наверное, нет, нам просто нужно время, чтобы его реализовать (может, неделя), и есть другие задачи, которые для нас важнее.
Александр Козловский