小马(ORM)如何做其伎俩?

时间:2013-04-20 01:53:16

标签: python orm metaprogramming dsl ponyorm

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内置了精彩的内省和元编程,但是这个库如何能够在不进行预处理的情况下转换生成器表达式?它看起来很神奇。

[更新]

Blender写道:

  

Here is the file你要追求的。它似乎使用一些内省魔法重建了生成器。我不确定它是否支持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 个答案:

答案 0 :(得分:202)

Pony ORM作者在这里。

Pony通过三个步骤将Python生成器转换为SQL查询:

  1. 反编译生成器字节码并重建生成器AST (抽象语法树)
  2. 将Python AST翻译成“抽象SQL” - 通用 基于列表的SQL查询表示
  3. 将抽象SQL表示转换为特定表示 数据库相关的SQL方言
  4. 最复杂的部分是Pony必须的第二步 理解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()函数接受python生成器作为参数,然后分析其字节码。 我们可以使用标准的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对象,该对象实现了访问者模式。 反编译器实例逐个获取字节码指令。 对于每条指令,反编译器对象都会调用自己的方法。 此方法的名称等于当前字节码指令的名称。

    当Python计算表达式时,它使用stack,它存储一个中间体 计算结果。反编译器对象也有自己的堆栈, 但是这个堆栈不存储表达式计算的结果, 但表达式的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'))]) 在堆栈的顶部。
    5. 处理完所有字节码指令后,反编译器堆栈包含 单个AST节点,对应于整个生成器表达式。

      由于Pony ORM需要反编译生成器 而且只有lambdas,这并不复杂,因为 生成器的指令流相对简单 - 它只是一堆嵌套循环。

      目前Pony ORM涵盖了整个生成器指令集,除了两件事:

      1. 内联if表达式:a if b else c
      2. 复合比较:a < b < c
      3. 如果Pony遇到这样的表达式,它会引发NotImplementedError异常。但即使在 在这种情况下,您可以通过将生成器表达式作为字符串传递来使其工作。 当您将生成器作为字符串传递时,Pony不使用反编译器模块。代替 它使用标准的Python compiler.parse函数获取AST。

        希望这能回答你的问题。