Pony (ORM) 如何使用它的技巧?

Posted

技术标签:

【中文标题】Pony (ORM) 如何使用它的技巧?【英文标题】:How Pony (ORM) does its tricks? 【发布时间】:2013-04-13 12:02:24 【问题描述】:

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 具有出色的内省和元编程内置功能,但是这个库如何能够在不进行预处理的情况下翻译生成器表达式?看起来很神奇。

[更新]

搅拌机写道:

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 抽象语法语法树。

我还是希望有人解释一下,来源远远超出了我的魔法水平。

【问题讨论】:

推测p 对象是 Pony 实现的类型的对象,它查看正在访问的方法/属性(例如,namestartswith)并将它们转换为SQL。 Here 是您需要的文件。它似乎使用一些自省魔法来重建生成器。我不确定它是否支持 100% 的 Python 语法,但这很酷。 @Blender:我在 LISP 中看到过这种技巧 - 在 Python 中实现这种特技简直是病态! 【参考方案1】:

Pony ORM 作者在这里。

Pony 分三步将 Python 生成器转换为 SQL 查询:

    生成器字节码的反编译和重建生成器 AST (抽象语法树) 将 Python AST 翻译成“抽象 SQL”——通用 SQL 查询的基于列表的表示 将抽象 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 生成器作为参数,然后分析它的字节码。 我们可以使用标准 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 在模块pony.orm.decompiling 中有函数decompile(),它可以 从字节码中恢复 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 (==)

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

    致电decompiler.LOAD_FAST('c')。 此方法将Name('c') 节点放在反编译器堆栈的顶部。 致电decompiler.LOAD_ATTR('country')。 此方法从堆栈中获取Name('c') 节点, 创建 Geattr(Name('c'), 'country') 节点并将其放在堆栈的顶部。 致电decompiler.LOAD_CONST('USA')。 此方法将Const('USA') 节点置于堆栈顶部。 致电decompiler.COMPARE_OP('==')。 此方法从堆栈中获取两个节点(Getattr 和 Const), 然后放Compare(Getattr(Name('c'), 'country'), [('==', Const('USA'))]) 在堆栈的顶部。

处理完所有字节码指令后,反编译器堆栈包含 对应于整个生成器表达式的单个 AST 节点。

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

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

    内联 if 表达式:a if b else c 复合比较:a &lt; b &lt; c

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

希望这能回答你的问题。

【讨论】:

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

以上是关于Pony (ORM) 如何使用它的技巧?的主要内容,如果未能解决你的问题,请参考以下文章

Django ORM 类似于 Pony ORM 中的选择查询

如何做类似 Django 模型的元类技巧

iPhone如何使用Linux编程来改善它的存在

如何将Django ORM连接到mongo atlas?

谈ENTITYFRAMEWORK数据更新之技巧

Propel ORM:如果这样的对象不存在,如何仅保存对象