118

Pony ORM does the nice trick of converting a generator expression into SQL. Example:

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

I know Python has wonderful introspection and metaprogramming builtin, but how this library is able to translate the generator expression without preprocessing? It looks like magic.

[update]

Blender wrote:

Here is the file that you're after. It seems to reconstruct the generator using some introspection wizardry. I'm not sure if it supports 100% of Python's syntax, but this is pretty cool. – Blender

I was thinking they were exploring some feature from the generator expression protocol, but looking this file, and seeing the ast module involved... No, they are not inspecting the program source on the fly, are they? Mind-blowing...

@BrenBarn: If I try to call the generator outside the select function call, the result is:

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

Seems like they are doing more arcane incantations like inspecting the select function call and processing the Python abstract syntax grammar tree on the fly.

I still would like to see someone explaining it, the source is way beyond my wizardry level.

4

1 回答 1

224

Pony ORM 作者在这里。

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

  1. 生成器字节码的反编译和重建生成器 AST(抽象语法树)
  2. 将 Python AST 翻译成“抽象 SQL”——基于列表的通用 SQL 查询表示
  3. 将抽象 SQL 表示转换为特定的数据库相关 SQL 方言

最复杂的部分是第二步,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 生成器作为参数,然后分析它的字节码。我们可以使用标准 pythondis模块获取这个生成器的字节码指令:

>>> 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 ORMdecompile()在模块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 计算表达式时,它使用堆栈,堆栈存储计算的中间结果。反编译器对象也有自己的栈,但是这个栈存储的不是表达式计算的结果,而是表达式的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 只需要反编译生成器和 lambda,这并不复杂,因为生成器的指令流相对简单——它只是一堆嵌套循环。

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

  1. 内联 if 表达式:a if b else c
  2. 复合比较:a < b < c

如果 Pony 遇到这样的表达式,它会引发NotImplementedError异常。但即使在这种情况下,您也可以通过将生成器表达式作为字符串传递来使其工作。当您将生成器作为字符串传递时,Pony 不使用反编译器模块。相反,它使用标准 Pythoncompiler.parse函数获取 AST。

希望这能回答你的问题。

于 2013-04-20T09:32:10.340 回答