V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
abersheeran
V2EX  ›  Python

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

  •  
  •   abersheeran · 2021-09-25 23:26:00 +08:00 · 3129 次点击
    这是一个创建于 1181 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

    • inspect.getsource:这玩意只能获取第一行,比如定义一个多行的 lambda 它也只能拿到第一行。并且如果 lambda 前面还有东西,它会一并拿回来,这部满足我的需求,我只想要从 lambda 关键词开始到整个 lambda 结束的定义,是不是原有格式我不在乎,只要完整且不多余就行。
    • lambda_object.__code__.co_firstlineno:这个同上,实际上 inspect.getsource 就是用这个值去拿的。这个值只能标识第一行所在,却不能标识开始的横轴位置以及最后的坐标。

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

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

    18 条回复    2021-09-26 11:07:16 +08:00
    chinvo
        1
    chinvo  
       2021-09-25 23:51:31 +08:00   ❤️ 1
    python 不清楚, C# 里面的 lambda 会被编译成 expression, 就能直接用了.

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

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

    可能对你有帮助
    hsfzxjy
        4
    hsfzxjy  
       2021-09-26 01:16:21 +08:00 via Android
    @hsfzxjy #3 这个在 3.5-3.10 应该都可以,2.7.没测过
    penguinWWY
        5
    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
        6
    penguinWWY  
       2021-09-26 02:16:30 +08:00
    @hsfzxjy
    @abersheeran

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


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


    @chenxytw 这个办法我也想过,问题在于重载运算符不能把 and 、or 、not 运算给重载了……
    penguinWWY
        11
    penguinWWY  
       2021-09-26 10:17:44 +08:00
    @abersheeran 当然是使用 C API 辣
    O5oz6z3
        12
    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
        13
    hsfzxjy  
       2021-09-26 10:33:17 +08:00 via Android
    @penguinWWY co_linetable 应该只存了行号,co_columntable 能拿到列号,但这是 3.10 新加的,兼容性不太好
    hsfzxjy
        14
    hsfzxjy  
       2021-09-26 10:35:24 +08:00 via Android
    @abersheeran 我当时的解决方法是强制用户给同一行的 lambda 加不同的前缀,当然这个就比较丑了。同期待更好的方法
    abersheeran
        15
    abersheeran  
    OP
       2021-09-26 10:41:58 +08:00
    @O5oz6z3 一语惊醒梦中人,只要对比源码编译后的 __code__ 就行了。一行最多也就几个 lambda 。


    @hsfzxjy 好家伙,不同前缀有点暴力了
    O5oz6z3
        16
    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
        17
    hsfzxjy  
       2021-09-26 10:59:28 +08:00   ❤️ 1
    @O5oz6z3 #16 co_code 不可靠,一个简单的反例

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

    这是因为变量名一类的不存在于字节码中,而是在 __code__.co_names 里
    hsfzxjy
        18
    hsfzxjy  
       2021-09-26 11:07:16 +08:00   ❤️ 2
    还有另一个问题是你要考虑 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 有很多很多
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   4344 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 10:06 · PVG 18:06 · LAX 02:06 · JFK 05:06
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.