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

Pau*_*ine 108 python orm dsl metaprogramming 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]]
>>>
Run Code Online (Sandbox Code Playgroud)

我知道Python内置了精彩的内省和元编程,但是这个库如何能够在不进行预处理的情况下转换生成器表达式?它看起来像魔术.

[更新]

Blender写道:

这是你要追求的文件.它似乎使用一些内省魔法重建了生成器.我不确定它是否支持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
>>>
Run Code Online (Sandbox Code Playgroud)

看起来他们正在做更多神秘的咒语,比如检查select函数调用并动态处理Python抽象语法语法树.

我仍然希望看到有人解释它,来源远远超出了我的魔法水平.

Ale*_*sky 207

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()
Run Code Online (Sandbox Code Playgroud)

哪个将被翻译成以下SQL:

SELECT "c"."id", "c"."email", "c"."password", "c"."name", "c"."country", "c"."address"
FROM "Customer" "c"
WHERE "c"."country" = 'USA'
Run Code Online (Sandbox Code Playgroud)

以下是此查询的结果,将打印出来:

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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

POM ORM具有decompile()模块内的功能pony.orm.decompiling,可以从字节码恢复AST:

>>> from pony.orm.decompiling import decompile
>>> ast, external_names = decompile(gen)
Run Code Online (Sandbox Code Playgroud)

在这里,我们可以看到AST节点的文本表示:

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

现在让我们看看该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 (==)
Run Code Online (Sandbox Code Playgroud)

因此,反编译器对象执行以下操作:

  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只需要反编译生成器和lambdas,因此这并不复杂,因为生成器的指令流相对简单 - 它只是一堆嵌套循环.

目前Pony ORM涵盖了整个发电机指令集,除了两件事:

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

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

希望这能回答你的问题.

  • 非常高效:(1)字节码反编译速度非常快.(2)由于每个查询都有对应的代码对象,因此该代码对象可以用作缓存键.因此,Pony ORM只翻译每个查询一次,而Django和SQLAlchemy必须一次又一次地翻译同一个查询.(3)由于Pony ORM使用IdentityMap模式,它在同一事务中缓存查询结果.有一篇文章(俄语)作者声称,即使没有查询结果缓存,Pony ORM也比Django和SQLAlchemy快1.5-3倍:http://habrahabr.ru/post/188842/ (25认同)
  • SQLAlchemy具有查询缓存,ORM广泛使用此功能.默认情况下它不是打开的,因为它的真实性我们没有一个功能来将SQL表达式的构造链接到声明它的源代码中的位置,这是代码对象真正给你的.我们可以使用堆栈框架检查来获得相同的结果,但这对我的口味来说有点过于苛刻.在任何情况下,生成SQL都是最不重要的性能领域; 获取行和簿记更改是. (9认同)
  • 这与pypy JIT编译器兼容吗? (3认同)
  • 我没有对它进行测试,但一些Reddit评论者表示它兼容:http://tinyurl.com/ponyorm-pypy (2认同)
  • @ randomsurfer_123可能不是,我们只需要一些时间来实现它(可能一周),还有其他任务对我们来说更重要. (2认同)