求一个获取 lambda 对象源代码的方法

2021-09-25 23:26:00 +08:00
 abersheeran

我先说一下我试过的方法,以及为什么不行:

我能想到的解决方式是直接拿到 lambda 对象的字节码,从字节码反编译到 Python 源代码,但是我需要 2.7 和 3.9 、3.10 三个版本同时兼容的……我对字节码反编译不太熟悉,目前找到的都是以文件为单位的反编译,不知道有没有以对象为代码的反编译库。

我想做一个把类似于 User.filter(lambda user: user.age > 18) 这样的语句翻译到 SQL 的玩意。但是卡在了这里。

3115 次点击
所在节点    Python
18 条回复
chinvo
2021-09-25 23:51:31 +08:00
python 不清楚, C# 里面的 lambda 会被编译成 expression, 就能直接用了.

python 大概也有类似机制?
joApioVVx4M4X6Rf
2021-09-26 00:55:54 +08:00
同问
hsfzxjy
2021-09-26 01:14:04 +08:00
我之前实现一个拿 lambda 的 AST 的功能,但这个有个限制就是需要保证前置的 token 是固定的,具体可参考 https://github.com/hsfzxjy/lambdex/blob/master/lambdex/utils/ast.py#L97

比如要求用户写成 def_(lambda: ...) ,然后将这个 lambda 对象以及字符串 'def_' 传入这个函数,就可以拿到 AST

可能对你有帮助
hsfzxjy
2021-09-26 01:16:21 +08:00
@hsfzxjy #3 这个在 3.5-3.10 应该都可以,2.7.没测过
penguinWWY
2021-09-26 02:14:52 +08:00
先说这个问题,瞎猜一下

一个方法是通过这个 lambda 反向拿到 module,然后把这个 py 文件编译到 ast 再做遍历

另一个是 PyCodeObject 对象中有一个属性是 co_linetable,这个属性的类型是一个 PyBytesObject,可以看 cpython 中对它的解析方法,应该可以拿到起始行列和终止行列
https://github.com/python/cpython/blob/main/Include/cpython/code.h#L77
penguinWWY
2021-09-26 02:16:30 +08:00
@hsfzxjy
@abersheeran

再借楼说下,https://www.v2ex.com/t/804224#reply4
二位有没有兴趣
chenxytw
2021-09-26 04:54:44 +08:00
你的问题本身我不是很了解....但从你要做的事情来看,我在想,是不是没必要用你题目中提出的方法。而是通过实现 User 里 age 的 `__gt__` 之类的魔术方法,在这之中保存一些状态,然后将 Model 本身传给这个 lambda 就能做到你想要的事情了...这应该是最常见的实现类似事情的做法了....当然因为你要做的事情没有详细描述,所以不知道是不是有什么需求导致了你不采用这种方案....
fgwmlhdkkkw
2021-09-26 07:32:37 +08:00
@chenxytw Python 的 orm 都是这么做的。
2i2Re2PLMaDnghL
2021-09-26 09:40:36 +08:00
参考下 PyMacro ?
abersheeran
2021-09-26 09:44:31 +08:00
@hsfzxjy 你这个思路我也想到过,但是有一个问题我不知道该如何解决,比如同一行出现两个 lambda……


@penguinWWY 在 CPython 运行时用 Python 拿 PyBytesObject 的原始指针做不到的吧?


@chenxytw 这个办法我也想过,问题在于重载运算符不能把 and 、or 、not 运算给重载了……
penguinWWY
2021-09-26 10:17:44 +08:00
@abersheeran 当然是使用 C API 辣
O5oz6z3
2021-09-26 10:29:52 +08:00
本质上也许是寻找一个表达式的源码位置:
1. lambda 有多少行?
2. 同一行里有几个 lambda ?
3. 是否嵌套 lambda ?
4. 是否有'lambda'字面字符串?
http://xion .io/post/code/python-get-lambda-code.html
看到这篇文章和#3 楼的实现,想到一个未验证的思路:对 inspect.getsource() 获取的源码进行修剪并编译成 ast,遍历 ast 提取所有 lambda 节点,用 Python3.9 的 ast.unparse() 获取近似的源码,用 co_code 判断源码编译后是否等价。
hsfzxjy
2021-09-26 10:33:17 +08:00
@penguinWWY co_linetable 应该只存了行号,co_columntable 能拿到列号,但这是 3.10 新加的,兼容性不太好
hsfzxjy
2021-09-26 10:35:24 +08:00
@abersheeran 我当时的解决方法是强制用户给同一行的 lambda 加不同的前缀,当然这个就比较丑了。同期待更好的方法
abersheeran
2021-09-26 10:41:58 +08:00
@O5oz6z3 一语惊醒梦中人,只要对比源码编译后的 __code__ 就行了。一行最多也就几个 lambda 。


@hsfzxjy 好家伙,不同前缀有点暴力了
O5oz6z3
2021-09-26 10:53:35 +08:00
@abersheeran #15 我指的是 __code__.co_code 字节码,是从那篇文章中学来的,虽然我也不确定这个字节码比较是否可靠。顺便写了两个 demo 。
简单的情况:
source_text = inspect.getsourcelines(lambda_func)[0][0]
source_ast = ast.parse(source_text)
lambda_node = next((node for node in ast.walk(source_ast) if isinstance(node, ast.Lambda)), None)
lambda_text = ast.unparse(lambda_node)
复杂的情况:
text = 'lambda' + inspect.getsource(lambda_func).partition('lambda')[2].rstrip()
while text:
... try:
... ... tree = ast.parse('({})'.format(text))
... ... srcs = [ast.unparse(node) for node in ast.walk(tree) if isinstance(node, ast.Lambda)]
... ... break
... except SyntaxError:
... ... text = text[:-1]
test = lambda src: compile(src,'','eval').co_consts[0].co_code==lambda_func.__code__.co_code
hits = list(filter(test, srcs))
hsfzxjy
2021-09-26 10:59:28 +08:00
@O5oz6z3 #16 co_code 不可靠,一个简单的反例

(lambda: print(1)).__code__.co_code == (lambda: sum(1)).__code__.co_code # True

这是因为变量名一类的不存在于字节码中,而是在 __code__.co_names 里
hsfzxjy
2021-09-26 11:07:16 +08:00
还有另一个问题是你要考虑 lambda 所在的闭包,看一个例子

def f():
... a = 1
... return lambda: a + 1
a = 1
f().__code__.co_code == (lambda: a + 1).__code__.co_code # False

这两个 lambda 虽然代码相同但是他们字节码不一样。原因是 f() 中的 a 是个 local 变量,读取时会使用 LOAD_DEREF ;而后一个是 global 变量,读取时会使用 LOAD_GLOBAL 。总而言之 corner cases 有很多很多

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/804220

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX