解析一个.py文件,读取AST,修改AST,然后写回已修改的源代码。

215
我想要以编程的方式编辑 Python 源代码。基本上,我想要读取一个 .py 文件,生成 AST,然后将修改后的 Python 源代码写回(即另一个.py文件)。
有一些使用标准 Python 模块解析/编译 Python 源代码的方法,例如astcompiler。但是,我认为它们都不支持修改源代码的方式(例如删除此函数声明),然后将修改后的 Python 源代码写回。
更新:我想这样做的原因是我想为 Python 编写一个变异测试库,主要是通过删除语句 / 表达式、重新运行测试并查看哪些部分出错来实现的。

5
自版本2.6起被弃用:编译器包已在Python 3.0中移除。 - dfa
3
哇塞!我想用相同的技术(具体来说是创建一个 nose 插件)为 Python 制作一个变异测试器,你打算开源吗? - Ryan
2
@Ryan 是的,我会开源我创建的任何东西。我们应该在这方面保持联系。 - Amandasaurus
1
你在突变中运行任何遗传算法吗? :P - chiffa
1
Macropy提供了在导入时操作抽象语法树的语法糖。 - jfs
显示剩余2条评论
14个回答

86

Pythoscope 会对其自动生成的测试用例进行此操作,Python 2.6 版本的 2to3 工具也会这样做(它将 Python 2.x 源代码转换为 Python 3.x 源代码)。

这两个工具都使用了 lib2to3 库,它是 Python 解析器/编译器机制的一种实现,可以在源代码往返转换时保留注释 -> AST -> 源代码。

如果你想进行更多的重构转换,可以考虑使用 rope 项目

另外一个选择是使用 ast 模块,还有一个旧的示例来“反解析”语法树(使用 parser 模块)。但是 ast 模块更适用于对被转换为代码对象的 AST 进行变换。

还有一个名为 redbaron 项目(由 Xavier Combelle 提供的建议),它也可能非常适合你的需求。


6
未解析的示例仍在维护中,这是更新后的Py3k版本链接:http://hg.python.org/cpython/log/tip/Tools/parser/unparse.py。 - Janus Troelsen
2
关于 unparse.py 脚本 - 从另一个脚本中使用它可能会非常麻烦。但是,有一个名为 astunparse 的包(在 github 上在 pypi 上),它基本上是 unparse.py 的适当打包版本。 - mbdevpl
你能否更新你的答案,加入parso作为首选项?它非常好且更新。 - boxed
@Ryan。你能给我提供获取Python源代码的AST和CFG工具吗? - Avv
lib2to3 程序库似乎缺乏文档,尽管 http://python3porting.com/fixers.html 提供了一些注释。然而存在一个问题,API 被声称不稳定,并且正如 https://docs.python.org/3/library/2to3.html 中所解释的,该库无法解析某些 Python 3.10 语法并将很快被删除。 - user202729

62

内置的ast模块似乎没有将语法树转换回源代码的方法。不过,这里的codegen模块为ast提供了一个漂亮的打印机,使您能够进行此操作。

import ast
import codegen

expr="""
def foo():
   print("hello world")
"""
p=ast.parse(expr)

p.body[0].body = [ ast.parse("return 42").body[0] ] # Replace function body with "return 42"

print(codegen.to_source(p))

这将打印:

def foo():
    return 42
请注意,您可能会失去精确的格式和注释,因为它们未被保留。
但是,您可能并不需要这样做。如果您只需要执行替换后的AST,则可以简单地在ast上调用compile(),并执行生成的代码对象。

21
仅供以后使用的人参考,这段代码生成工具(codegen)已经过时且存在一些漏洞。我修复了其中的一些问题,我已将其发布在GitHub上:https://gist.github.com/791312 - mattbasta
请注意,最新的代码生成器更新于2012年,这是在上述评论之后,因此我猜测代码生成器已经更新了。@mattbasta - zjffdu
6
Astor似乎是codegen的一个维护后继者。 - medmunds

51

2
太棒了!这是当代Python版本的最佳解决方案。 - Kyle Carow
它会破坏一些代码格式。例如,删除一些空行或用撇号替换双引号。 - MMM

20

在另一个答案中,我建议使用astor包,但是我后来发现了一个更为更新的AST解析包,名为astunparse

>>> import ast
>>> import astunparse
>>> print(astunparse.unparse(ast.parse('def foo(x): return 2 * x')))


def foo(x):
    return (2 * x)

我已在Python 3.5上测试过此代码。


19

你可能不需要重新生成源代码。当然,我这么说有点危险,因为你并没有解释为什么需要生成一个充满代码的.py文件;但是:

  • 如果你想生成一个实际使用的 .py 文件,比如让人们填写表单并获取有用的 .py 文件插入到他们的项目中,那么你不希望将其更改为 AST,然后再转换回来,因为你会失去注释以及所有格式(请考虑通过将相关的一组行分组在一起使 Python 变得易读的空白行)(ast 节点具有 lineno 和 col_offset 属性)。相反,你可能需要使用模板引擎(例如 Django 模板语言,旨在轻松地创建模板化文本文件)来自定义 .py 文件,或者使用 Rick Copeland 的 MetaPython 扩展。

  • 如果你尝试在模块编译时进行更改,请注意你不必回退到文本;你可以直接编译 AST,而无需将其重新变成 .py 文件。

  • 但在几乎任何情况下,你可能都试图做一些动态的事情,而像 Python 这样的语言实际上使这些事情非常容易,而不需要编写新的 .py 文件!如果你扩展你的问题,让我们知道你真正想要实现什么,新的 .py 文件可能根本不会涉及到答案;我见过数百个 Python 项目实际完成了数百个现实世界的任务,其中没有一个需要编写 .py 文件。所以,我必须承认,我有点怀疑你是否找到了第一个好的用例。 :-)

更新: 现在你已经解释了你想要做什么,我会倾向于直接在AST上操作。你需要通过删除整个语句来进行变异,而不是文件的行(这可能会导致一些只用SyntaxError死亡的半语句),那么在AST中进行操作何其更好?


可能解决方案和可能的替代方案的良好概述。 - Ryan
1
代码生成的真实世界用例:Kid和Genshi(我相信)从XML模板生成Python,以便快速渲染动态页面。 - Rick Copeland

13
使用 ast 模块可以解析和修改代码结构,我将在稍后的示例中展示。但是,仅使用 ast 模块无法写回修改后的源代码。有其他模块可用于此工作,例如这里
注意:下面的示例可以视为关于使用 ast 模块的入门教程,但更全面的使用指南可在Green Tree snakes教程官方文档中了解。

ast 简介:

>>> import ast
>>> tree = ast.parse("print 'Hello Python!!'")
>>> exec(compile(tree, filename="<ast>", mode="exec"))
Hello Python!!

您可以通过调用API ast.parse() 来解析 Python 代码(以字符串表示)。这将返回抽象语法树(AST)结构的句柄。有趣的是,您可以编译回这个结构并像上面展示的那样执行它。
另一个非常有用的 API 是 ast.dump(),它以字符串形式转储整个 AST。它可用于检查树结构,并在调试中非常有帮助。例如, 在 Python 2.7 上:
>>> import ast
>>> tree = ast.parse("print 'Hello Python!!'")
>>> ast.dump(tree)
"Module(body=[Print(dest=None, values=[Str(s='Hello Python!!')], nl=True)])"

在Python 3.5版本中:

>>> import ast
>>> tree = ast.parse("print ('Hello Python!!')")
>>> ast.dump(tree)
"Module(body=[Expr(value=Call(func=Name(id='print', ctx=Load()), args=[Str(s='Hello Python!!')], keywords=[]))])"

请注意Python 2.7和Python 3.5中print语句的语法差异以及各自树中AST节点类型的差异。
如何使用ast修改代码: 现在,让我们来看一个使用ast模块修改Python代码的示例。修改AST结构的主要工具是ast.NodeTransformer类。每当需要修改AST时,他/她需要从中进行子类化,并相应地编写节点转换。
对于我们的示例,让我们尝试编写一个简单的实用程序,将Python 2的print语句转换为Python 3函数调用。 Print语句转Fun调用转换器实用程序:print2to3.py:
#!/usr/bin/env python
'''
This utility converts the python (2.7) statements to Python 3 alike function calls before running the code.

USAGE:
     python print2to3.py <filename>
'''
import ast
import sys

class P2to3(ast.NodeTransformer):
    def visit_Print(self, node):
        new_node = ast.Expr(value=ast.Call(func=ast.Name(id='print', ctx=ast.Load()),
            args=node.values,
            keywords=[], starargs=None, kwargs=None))
        ast.copy_location(new_node, node)
        return new_node

def main(filename=None):
    if not filename:
        return

    with open(filename, 'r') as fp:
        data = fp.readlines()
    data = ''.join(data)
    tree = ast.parse(data)

    print "Converting python 2 print statements to Python 3 function calls"
    print "-" * 35
    P2to3().visit(tree)
    ast.fix_missing_locations(tree)
    # print ast.dump(tree)

    exec(compile(tree, filename="p23", mode="exec"))

if __name__ == '__main__':
    if len(sys.argv) <=1:
        print ("\nUSAGE:\n\t print2to3.py <filename>")
        sys.exit(1)
    else:
        main(sys.argv[1])

这个实用程序可以在小的示例文件上进行尝试,比如下面的一个,它应该能够正常工作。

测试输入文件:py2.py

class A(object):
    def __init__(self):
        pass

def good():
    print "I am good"

main = good

if __name__ == '__main__':
    print "I am in main"
    main()

请注意,以上转换仅用于ast教程目的,在实际情况下,人们需要考虑所有不同的场景,例如print " x is %s" % ("Hello Python")

这并不展示如何打印,它执行了吗? - mathtick

7
如果您在2019年查看此内容,则可以使用libcst包。它的语法类似于ast。它能够完美地工作,并保留代码结构。它主要用于需要保留注释、空格、换行等项目的项目中。
如果您不需要考虑保留注释、空格和其他内容,则可以使用ast和astor组合。

6

我最近创建了一段非常稳定(核心经过了很好的测试)且可扩展的代码,它可以从ast树生成代码:https://github.com/paluh/code-formatter

我正在将我的项目作为小型vim插件的基础(我每天都在使用),因此我的目标是生成非常漂亮和易读的Python代码。

P.S. 我曾尝试扩展codegen,但它的架构基于ast.NodeVisitor接口,因此格式化程序(visitor_方法)只是函数。我发现这种结构非常受限制,难以优化(在长而嵌套的表达式的情况下,保留对象树并缓存一些部分结果更容易 - 否则您可能会遇到指数复杂度,如果您想搜索最佳布局)。但是,像mitsuhiko的每个作品一样,codegen非常精简且写得很好。


5
其他答案之一推荐使用codegen,但似乎已被astor取代。当前astor版本(截至本文撰写时为0.5)也有点过时,您可以按照以下方式安装astor的开发版本。
pip install git+https://github.com/berkerpeksag/astor.git#egg=astor

然后您可以使用astor.to_source将Python AST转换为易于阅读的Python源代码:

>>> import ast
>>> import astor
>>> print(astor.to_source(ast.parse('def foo(x): return 2 * x')))
def foo(x):
    return 2 * x

我已在Python 3.5上进行了测试。


3
很不幸,上面的答案都没有同时满足以下两个条件:
  • 保留源代码周围的句法完整性(例如保留注释、其它格式)
  • 实际使用AST(而非CST)。

最近我写了一个小工具箱来进行基于纯AST的重构,叫做refactor。例如,如果你想用42替换所有的placeholder,你可以简单地编写如下规则:

class Replace(Rule):
    
    def match(self, node):
        assert isinstance(node, ast.Name)
        assert node.id == 'placeholder'
        
        replacement = ast.Constant(42)
        return ReplacementAction(node, replacement)

它将找到所有可接受的节点,用新节点替换它们并生成最终表单;

--- test_file.py
+++ test_file.py

@@ -1,11 +1,11 @@

 def main():
-    print(placeholder * 3 + 2)
-    print(2 +               placeholder      + 3)
+    print(42 * 3 + 2)
+    print(2 +               42      + 3)
     # some commments
-    placeholder # maybe other comments
+    42 # maybe other comments
     if something:
         other_thing
-    print(placeholder)
+    print(42)
 
 if __name__ == "__main__":
     main()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接