• 9.24 解析与分析Python源码
    • 问题
    • 解决方案
    • 讨论

    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. >>>

    尽管如此,ast 模块能被用来将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.  
    10. >>> top = ast.parse('for i in range(10): print(i)', mode='exec')
    11. >>> top
    12. <_ast.Module object at 0x100747390>
    13. >>> ast.dump(top)
    14. "Module(body=[For(target=Name(id='i', ctx=Store()),
    15. iter=Call(func=Name(id='range', ctx=Load()), args=[Num(n=10)],
    16. keywords=[], starargs=None, kwargs=None),
    17. body=[Expr(value=Call(func=Name(id='print', ctx=Load()),
    18. args=[Name(id='i', ctx=Load())], keywords=[], starargs=None,
    19. kwargs=None))], orelse=[])])"
    20. >>>

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

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

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

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

    最后,AST可以通过 compile() 函数来编译并执行。例如:

    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. >>>

    讨论

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

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

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

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

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

    装饰器会将 countdown() 函数重写为类似下面这样子:

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

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

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

    本节受另外一个在 ActiveState 中处理Python字节码的章节的启示。使用AST是一个更加高级点的技术,并且也更简单些。参考下面一节获得字节码的更多信息。

    原文:

    http://python3-cookbook.readthedocs.io/zh_CN/latest/c09/p24_parse_and_analyzing_python_source.html