加载中...

9.24 解析与分析Python源码


问题

你想写解析并分析Python源代码的程序。

解决方案

大部分程序员知道Python能够计算或执行字符串形式的源代码。例如:

  1. >>> x = 42
  2. >>> eval('2 + 3*4 + x')
  3. 56
  4. >>> exec('for i in range(10): print(i)')
  5. 0
  6. 1
  7. 2
  8. 3
  9. 4
  10. 5
  11. 6
  12. 7
  13. 8
  14. 9
  15. >>>

尽管如此,<span class="pre" style="box-sizing: border-box;">ast</span> 模块能被用来将Python源码编译成一个可被分析的抽象语法树(AST)。例如:

  1. >>> import ast
  2. >>> ex = ast.parse('2 + 3*4 + x', mode='eval')
  3. >>> ex
  4. <_ast.Expression object at 0x1007473d0>
  5. >>> ast.dump(ex)
  6. "Expression(body=BinOp(left=BinOp(left=Num(n=2), op=Add(),
  7. right=BinOp(left=Num(n=3), op=Mult(), right=Num(n=4))), op=Add(),
  8. right=Name(id='x', ctx=Load())))"
  9. >>> top = ast.parse('for i in range(10): print(i)', mode='exec')
  10. >>> top
  11. <_ast.Module object at 0x100747390>
  12. >>> ast.dump(top)
  13. "Module(body=[For(target=Name(id='i', ctx=Store()),
  14. iter=Call(func=Name(id='range', ctx=Load()), args=[Num(n=10)],
  15. keywords=[], starargs=None, kwargs=None),
  16. body=[Expr(value=Call(func=Name(id='print', ctx=Load()),
  17. args=[Name(id='i', ctx=Load())], keywords=[], starargs=None,
  18. kwargs=None))], orelse=[])])"
  19. >>>

分析源码树需要你自己更多的学习,它是由一系列AST节点组成的。 分析这些节点最简单的方法就是定义一个访问者类,实现很多 <span class="pre" style="box-sizing: border-box;">visit_NodeName()</span> 方法, <span class="pre" style="box-sizing: border-box;">NodeName()</span> 匹配那些你感兴趣的节点。下面是这样一个类,记录了哪些名字被加载、存储和删除的信息。

  1. import ast
  2. class CodeAnalyzer(ast.NodeVisitor):
  3. def __init__(self):
  4. self.loaded = set()
  5. self.stored = set()
  6. self.deleted = set()
  7. def visit_Name(self, node):
  8. if isinstance(node.ctx, ast.Load):
  9. self.loaded.add(node.id)
  10. elif isinstance(node.ctx, ast.Store):
  11. self.stored.add(node.id)
  12. elif isinstance(node.ctx, ast.Del):
  13. self.deleted.add(node.id)
  14. # Sample usage
  15. if __name__ == '__main__':
  16. # Some Python code
  17. code = '''
  18. for i in range(10):
  19. print(i)
  20. del i
  21. '''
  22. # Parse into an AST
  23. top = ast.parse(code, mode='exec')
  24. # Feed the AST to analyze name usage
  25. c = CodeAnalyzer()
  26. c.visit(top)
  27. print('Loaded:', c.loaded)
  28. print('Stored:', c.stored)
  29. print('Deleted:', c.deleted)

如果你运行这个程序,你会得到下面这样的输出:

  1. Loaded: {'i', 'range', 'print'}
  2. Stored: {'i'}
  3. Deleted: {'i'}

最后,AST可以通过 <span class="pre" style="box-sizing: border-box;">compile()</span> 函数来编译并执行。例如:

  1. >>> exec(compile(top,'<stdin>', 'exec'))
  2. 0
  3. 1
  4. 2
  5. 3
  6. 4
  7. 5
  8. 6
  9. 7
  10. 8
  11. 9
  12. >>>

讨论

当你能够分析源代码并从中获取信息的时候,你就能写很多代码分析、优化或验证工具了。 例如,相比盲目的传递一些代码片段到类似 <span class="pre" style="box-sizing: border-box;">exec()</span> 函数中,你可以先将它转换成一个AST, 然后观察它的细节看它到底是怎样做的。 你还可以写一些工具来查看某个模块的全部源码,并且在此基础上执行某些静态分析。

需要注意的是,如果你知道自己在干啥,你还能够重写AST来表示新的代码。 下面是一个装饰器例子,可以通过重新解析函数体源码、 重写AST并重新创建函数代码对象来将全局访问变量降为函数体作用范围,

  1. # namelower.py
  2. import ast
  3. import inspect
  4. # Node visitor that lowers globally accessed names into
  5. # the function body as local variables.
  6. class NameLower(ast.NodeVisitor):
  7. def __init__(self, lowered_names):
  8. self.lowered_names = lowered_names
  9. def visit_FunctionDef(self, node):
  10. # Compile some assignments to lower the constants
  11. code = '__globals = globals()\n'
  12. code += '\n'.join("{0} = __globals['{0}']".format(name)
  13. for name in self.lowered_names)
  14. code_ast = ast.parse(code, mode='exec')
  15. # Inject new statements into the function body
  16. node.body[:0] = code_ast.body
  17. # Save the function object
  18. self.func = node
  19. # Decorator that turns global names into locals
  20. def lower_names(*namelist):
  21. def lower(func):
  22. srclines = inspect.getsource(func).splitlines()
  23. # Skip source lines prior to the @lower_names decorator
  24. for n, line in enumerate(srclines):
  25. if '@lower_names' in line:
  26. break
  27. src = '\n'.join(srclines[n+1:])
  28. # Hack to deal with indented code
  29. if src.startswith((' ','\t')):
  30. src = 'if 1:\n' + src
  31. top = ast.parse(src, mode='exec')
  32. # Transform the AST
  33. cl = NameLower(namelist)
  34. cl.visit(top)
  35. # Execute the modified AST
  36. temp = {}
  37. exec(compile(top,'','exec'), temp, temp)
  38. # Pull out the modified code object
  39. func.__code__ = temp[func.__name__].__code__
  40. return func
  41. return lower

为了使用这个代码,你可以像下面这样写:

  1. INCR = 1
  2. @lower_names('INCR')
  3. def countdown(n):
  4. while n > 0:
  5. n -= INCR

装饰器会将 <span class="pre" style="box-sizing: border-box;">countdown()</span> 函数重写为类似下面这样子:

  1. def countdown(n):
  2. __globals = globals()
  3. INCR = __globals['INCR']
  4. while n > 0:
  5. n -= INCR

在性能测试中,它会让函数运行快20%

现在,你是不是想为你所有的函数都加上这个装饰器呢?或许不会。 但是,这却是对于一些高级技术比如AST操作、源码操作等等的一个很好的演示说明

本节受另外一个在 <span class="pre" style="box-sizing: border-box;">ActiveState</span> 中处理Python字节码的章节的启示。 使用AST是一个更加高级点的技术,并且也更简单些。参考下面一节获得字节码的更多信息。


还没有评论.