搜索
首页后端开发Python教程Python Ast抽象语法树应该如何使用?

引言

Abstract Syntax Trees即抽象语法树。Ast是python源码到字节码的一种中间产物,借助ast模块可以从语法树的角度分析源码结构。

此外,我们不仅可以修改和执行语法树,还可以将Source生成的语法树unparse成python源码。因此ast给python源码检查、语法分析、修改代码以及代码调试等留下了足够的发挥空间。

1. AST简介 

Python官方提供的CPython解释器对python源码的处理过程如下:

Parse source code into a parse tree (Parser/pgen.c)

Transform parse tree into an Abstract Syntax Tree (Python/ast.c)

Transform AST into a Control Flow Graph (Python/compile.c)

Emit bytecode based on the Control Flow Graph (Python/compile.c)

即实际python代码的处理过程如下:

源代码解析 --> 语法树 --> 抽象语法树(AST) --> 控制流程图 --> 字节码

上述过程在python2.5之后被应用。python源码首先被解析成语法树,随后又转换成抽象语法树。在抽象语法树中我们可以看到源码文件中的python的语法结构。

大部分时间编程可能都不需要用到抽象语法树,但是在特定的条件和需求的情况下,AST又有其特殊的方便性。

下面是一个抽象语法的简单实例。

Module(body=[
    Print(
          dest=None,
          values=[BinOp( left=Num(n=1),op=Add(),right=Num(n=2))],
          nl=True,
 )])

2. 创建AST

2.1 Compile函数

先简单了解一下compile函数。

compile(source, filename, mode[, flags[, dont_inherit]]) 

  • source -- 字符串或者AST(Abstract Syntax Trees)对象。一般可将整个py文件内容file.read()传入。

  • filename -- 代码文件名称,如果不是从文件读取代码则传递一些可辨认的值。

  • mode -- 指定编译代码的种类。可以指定为 exec, eval, single。

  • flags -- 变量作用域,局部命名空间,如果被提供,可以是任何映射对象。

  • flags和dont_inherit是用来控制编译源码时的标志。

func_def = \
"""
def add(x, y):
    return x + y
print add(3, 5)
"""

使用Compile编译并执行:

>>> cm = compile(func_def, &#39;<string>&#39;, &#39;exec&#39;)
>>> exec cm
>>> 8

上面func_def经过compile编译得到字节码,cm即code对象,

True == isinstance(cm, types.CodeType)。

compile(source, filename, mode, ast.PyCF_ONLY_AST)  209861d5cd2975725c730f519ed6ad71 ast.parse(source, filename='e12ba19319d03b5626da3e95daebf10a', mode='exec')

2.2 生成ast

使用上面的func_def生成ast.

r_node = ast.parse(func_def)
print astunparse.dump(r_node)    # print ast.dump(r_node)

 下面是func_def对应的ast结构:

Module(body=[
    FunctionDef(
        name=&#39;add&#39;,
        args=arguments(
            args=[Name(id=&#39;x&#39;,ctx=Param()),Name(id=&#39;y&#39;,ctx=Param())],
            vararg=None,
            kwarg=None,
            defaults=[]),
        body=[Return(value=BinOp(
            left=Name(id=&#39;x&#39;,ctx=Load()),
            op=Add(),
            right=Name(id=&#39;y&#39;,ctx=Load())))],
        decorator_list=[]),
    Print(
        dest=None,
        values=[Call(
                func=Name(id=&#39;add&#39;,ctx=Load()),
                args=[Num(n=3),Num(n=5)],
                keywords=[],
                starargs=None,
                kwargs=None)],
        nl=True)
  ])

 除了ast.dump,有很多dump ast的第三方库,如astunparse, codegen, unparse等。这些第三方库不仅能够以更好的方式展示出ast结构,还能够将ast反向导出python source代码。

module Python version "$Revision$"
{
  mod = Module(stmt* body)| Expression(expr body)
  stmt = FunctionDef(identifier name, arguments args, stmt* body, expr* decorator_list)
        | ClassDef(identifier name, expr* bases, stmt* body, expr* decorator_list)
        | Return(expr? value)
        | Print(expr? dest, expr* values, bool nl)| For(expr target, expr iter, stmt* body, stmt* orelse)
  expr = BoolOp(boolop op, expr* values)
       | BinOp(expr left, operator op, expr right)| Lambda(arguments args, expr body)| Dict(expr* keys, expr* values)| Num(object n) -- a number as a PyObject.
       | Str(string s) -- need to specify raw, unicode, etc?| Name(identifier id, expr_context ctx)
       | List(expr* elts, expr_context ctx) 
        -- col_offset is the byte offset in the utf8 string the parser uses
        attributes (int lineno, int col_offset)
  expr_context = Load | Store | Del | AugLoad | AugStore | Param
  boolop = And | Or 
  operator = Add | Sub | Mult | Div | Mod | Pow | LShift | RShift | BitOr | BitXor | BitAnd | FloorDiv
  arguments = (expr* args, identifier? vararg, identifier? kwarg, expr* defaults)
}

上面是部分摘自官网的 Abstract Grammar,实际遍历ast Node过程中根据Node的类型访问其属性。

 3. 遍历AST

python提供了两种方式来遍历整个抽象语法树。

3.1 ast.NodeTransfer

将func_def中的add函数中的加法运算改为减法,同时为函数实现添加调用日志。

  class CodeVisitor(ast.NodeVisitor):
      def visit_BinOp(self, node):
          if isinstance(node.op, ast.Add):
              node.op = ast.Sub()
          self.generic_visit(node)
      def visit_FunctionDef(self, node):
          print &#39;Function Name:%s&#39;% node.name
          self.generic_visit(node)
          func_log_stmt = ast.Print(
              dest = None,
              values = [ast.Str(s = &#39;calling func: %s&#39; % node.name, lineno = 0, col_offset = 0)],
              nl = True,
              lineno = 0,
              col_offset = 0,
          )
          node.body.insert(0, func_log_stmt)
  r_node = ast.parse(func_def)
  visitor = CodeVisitor()
  visitor.visit(r_node)
  # print astunparse.dump(r_node)
  print astunparse.unparse(r_node)
  exec compile(r_node, &#39;<string>&#39;, &#39;exec&#39;)

 运行结果:

Function Name:add
def add(x, y):
    print &#39;calling func: add&#39;
    return (x - y)
print add(3, 5)
calling func: add
-2

3.2 ast.NodeTransformer

使用NodeVisitor主要是通过修改语法树上节点的方式改变AST结构,NodeTransformer主要是替换ast中的节点。

既然func_def中定义的add已经被改成一个减函数了,那么我们就彻底一点,把函数名和参数以及被调用的函数都在ast中改掉,并且将添加的函数调用log写的更加复杂一些,争取改的面目全非:-)

  class CodeTransformer(ast.NodeTransformer):
      def visit_BinOp(self, node):
          if isinstance(node.op, ast.Add):
              node.op = ast.Sub()
          self.generic_visit(node)
          return node
      def visit_FunctionDef(self, node):
          self.generic_visit(node)
          if node.name == &#39;add&#39;:
              node.name = &#39;sub&#39;
          args_num = len(node.args.args)
          args = tuple([arg.id for arg in node.args.args])
          func_log_stmt = &#39;&#39;.join(["print &#39;calling func: %s&#39;, " % node.name, "&#39;args:&#39;", ", %s" * args_num % args])
          node.body.insert(0, ast.parse(func_log_stmt))
          return node
      def visit_Name(self, node):
          replace = {&#39;add&#39;: &#39;sub&#39;, &#39;x&#39;: &#39;a&#39;, &#39;y&#39;: &#39;b&#39;}
          re_id = replace.get(node.id, None)
          node.id = re_id or node.id
          self.generic_visit(node)
          return node
  r_node = ast.parse(func_def)
  transformer = CodeTransformer()
  r_node = transformer.visit(r_node)
  # print astunparse.dump(r_node)
  source = astunparse.unparse(r_node)
  print source
  # exec compile(r_node, &#39;<string>&#39;, &#39;exec&#39;)        # 新加入的node func_log_stmt 缺少lineno和col_offset属性
  exec compile(source, &#39;<string>&#39;, &#39;exec&#39;)
  exec compile(ast.parse(source), &#39;<string>&#39;, &#39;exec&#39;)

结果:

def sub(a, b):
    print &#39;calling func: sub&#39;, &#39;args:&#39;, a, b
    return (a - b)
print sub(3, 5)
calling func: sub args: 3 5
-2
calling func: sub args: 3 5
-2

 代码中能够清楚的看到两者的区别。这里不再赘述。

 4.AST应用

AST模块实际编程中很少用到,但是作为一种源代码辅助检查手段是非常有意义的;语法检查,调试错误,特殊字段检测等。

上面通过为函数添加调用日志的信息是一种调试python源代码的一种方式,不过实际中我们是通过parse整个python文件的方式遍历修改源码。

4.1 汉字检测

下面是中日韩字符的unicode编码范围

CJK Unified Ideographs 

Range: 4E00— 9FFF

Number of characters: 20992

Languages: chinese, japanese, korean, vietnamese

使用 unicode 范围 \u4e00 - \u9fff 来判别汉字,注意这个范围并不包含中文字符(e.g. u';' == u'\uff1b') .

下面是一个判断字符串中是否包含中文字符的一个类CNCheckHelper:

  class CNCheckHelper(object):
      # 待检测文本可能的编码方式列表
      VALID_ENCODING = (&#39;utf-8&#39;, &#39;gbk&#39;)
      def _get_unicode_imp(self, value, idx = 0):
          if idx < len(self.VALID_ENCODING):
              try:
                  return value.decode(self.VALID_ENCODING[idx])
              except:
                  return self._get_unicode_imp(value, idx + 1)
      def _get_unicode(self, from_str):
          if isinstance(from_str, unicode):
              return None
          return self._get_unicode_imp(from_str)
      def is_any_chinese(self, check_str, is_strict = True):
          unicode_str = self._get_unicode(check_str)
          if unicode_str:
              c_func = any if is_strict else all
              return c_func(u&#39;\u4e00&#39; <= char <= u&#39;\u9fff&#39; for char in unicode_str)
          return False

 接口is_any_chinese有两种判断模式,严格检测只要包含中文字符串就可以检查出,非严格必须全部包含中文。

下面我们利用ast来遍历源文件的抽象语法树,并检测其中字符串是否包含中文字符。

  class CodeCheck(ast.NodeVisitor):
      def __init__(self):
          self.cn_checker = CNCheckHelper()
      def visit_Str(self, node):
          self.generic_visit(node)
          # if node.s and any(u&#39;\u4e00&#39; <= char <= u&#39;\u9fff&#39; for char in node.s.decode(&#39;utf-8&#39;)):
          if self.cn_checker.is_any_chinese(node.s, True):
              print &#39;line no: %d, column offset: %d, CN_Str: %s&#39; % (node.lineno, node.col_offset, node.s)
  project_dir = &#39;./your_project/script&#39;
  for root, dirs, files in os.walk(project_dir):
      print root, dirs, files
      py_files = filter(lambda file: file.endswith(&#39;.py&#39;), files)
      checker = CodeCheck()
      for file in py_files:
          file_path = os.path.join(root, file)
          print &#39;Checking: %s&#39; % file_path
          with open(file_path, &#39;r&#39;) as f:
              root_node = ast.parse(f.read())
              checker.visit(root_node)

 上面这个例子比较的简单,但大概就是这个意思。

关于CPython解释器执行源码的过程可以参考官网描述:PEP 339

4.2 Closure 检查

一个函数中定义的函数或者lambda中引用了父函数中的local variable,并且当做返回值返回。特定场景下闭包是非常有用的,但是也很容易被误用。

关于python闭包的概念可以参考我的另一篇文章:理解Python闭包概念

这里简单介绍一下如何借助ast来检测lambda中闭包的引用。代码如下:

  class LambdaCheck(ast.NodeVisitor):
      def __init__(self):
          self.illegal_args_list = []
          self._cur_file = None
          self._cur_lambda_args = []
      def set_cur_file(self, cur_file):
          assert os.path.isfile(cur_file), cur_file
          self._cur_file = os.path.realpath(cur_file)
      def visit_Lambda(self, node):
          """
          lambda 闭包检查原则:
          只需检测lambda expr body中args是否引用了lambda args list之外的参数
          """
          self._cur_lambda_args =[a.id for a in node.args.args]
          print astunparse.unparse(node)
          # print astunparse.dump(node)
          self.get_lambda_body_args(node.body)
          self.generic_visit(node)
      def record_args(self, name_node):
          if isinstance(name_node, ast.Name) and name_node.id not in self._cur_lambda_args:
              self.illegal_args_list.append((self._cur_file, &#39;line no:%s&#39; % name_node.lineno, &#39;var:%s&#39; % name_node.id))
      def _is_args(self, node):
          if isinstance(node, ast.Name):
              self.record_args(node)
              return True
          if isinstance(node, ast.Call):
              map(self.record_args, node.args)
              return True
          return False
      def get_lambda_body_args(self, node):
          if self._is_args(node): return
          # for cnode in ast.walk(node):
          for cnode in ast.iter_child_nodes(node):
              if not self._is_args(cnode):
                  self.get_lambda_body_args(cnode)

 遍历工程文件:

  project_dir = &#39;./your project/script&#39;
  for root, dirs, files in os.walk(project_dir):
      py_files = filter(lambda file: file.endswith(&#39;.py&#39;), files)
      checker = LambdaCheck()
      for file in py_files:
          file_path = os.path.join(root, file)
          checker.set_cur_file(file_path)
          with open(file_path, &#39;r&#39;) as f:
              root_node = ast.parse(f.read())
              checker.visit(root_node)
      res = &#39;\n&#39;.join([&#39; ## &#39;.join(info) for info in checker.illegal_args_list])
      print res

由于Lambda(arguments args, expr body)中的body expression可能非常复杂,上面的例子中仅仅处理了比较简单的body expr。可根据自己工程特点修改和扩展检查规则。为了更加一般化可以单独写一个visitor类来遍历lambda节点。

以上是Python Ast抽象语法树应该如何使用?的详细内容。更多信息请关注PHP中文网其他相关文章!

声明
本文转载于:亿速云。如有侵权,请联系admin@php.cn删除
Python的主要目的:灵活性和易用性Python的主要目的:灵活性和易用性Apr 17, 2025 am 12:14 AM

Python的灵活性体现在多范式支持和动态类型系统,易用性则源于语法简洁和丰富的标准库。1.灵活性:支持面向对象、函数式和过程式编程,动态类型系统提高开发效率。2.易用性:语法接近自然语言,标准库涵盖广泛功能,简化开发过程。

Python:多功能编程的力量Python:多功能编程的力量Apr 17, 2025 am 12:09 AM

Python因其简洁与强大而备受青睐,适用于从初学者到高级开发者的各种需求。其多功能性体现在:1)易学易用,语法简单;2)丰富的库和框架,如NumPy、Pandas等;3)跨平台支持,可在多种操作系统上运行;4)适合脚本和自动化任务,提升工作效率。

每天2小时学习Python:实用指南每天2小时学习Python:实用指南Apr 17, 2025 am 12:05 AM

可以,在每天花费两个小时的时间内学会Python。1.制定合理的学习计划,2.选择合适的学习资源,3.通过实践巩固所学知识,这些步骤能帮助你在短时间内掌握Python。

Python与C:开发人员的利弊Python与C:开发人员的利弊Apr 17, 2025 am 12:04 AM

Python适合快速开发和数据处理,而C 适合高性能和底层控制。1)Python易用,语法简洁,适用于数据科学和Web开发。2)C 性能高,控制精确,常用于游戏和系统编程。

Python:时间投入和学习步伐Python:时间投入和学习步伐Apr 17, 2025 am 12:03 AM

学习Python所需时间因人而异,主要受之前的编程经验、学习动机、学习资源和方法及学习节奏的影响。设定现实的学习目标并通过实践项目学习效果最佳。

Python:自动化,脚本和任务管理Python:自动化,脚本和任务管理Apr 16, 2025 am 12:14 AM

Python在自动化、脚本编写和任务管理中表现出色。1)自动化:通过标准库如os、shutil实现文件备份。2)脚本编写:使用psutil库监控系统资源。3)任务管理:利用schedule库调度任务。Python的易用性和丰富库支持使其在这些领域中成为首选工具。

Python和时间:充分利用您的学习时间Python和时间:充分利用您的学习时间Apr 14, 2025 am 12:02 AM

要在有限的时间内最大化学习Python的效率,可以使用Python的datetime、time和schedule模块。1.datetime模块用于记录和规划学习时间。2.time模块帮助设置学习和休息时间。3.schedule模块自动化安排每周学习任务。

Python:游戏,Guis等Python:游戏,Guis等Apr 13, 2025 am 12:14 AM

Python在游戏和GUI开发中表现出色。1)游戏开发使用Pygame,提供绘图、音频等功能,适合创建2D游戏。2)GUI开发可选择Tkinter或PyQt,Tkinter简单易用,PyQt功能丰富,适合专业开发。

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
1 个月前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳图形设置
1 个月前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您听不到任何人,如何修复音频
1 个月前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.聊天命令以及如何使用它们
1 个月前By尊渡假赌尊渡假赌尊渡假赌

热工具

MinGW - 适用于 Windows 的极简 GNU

MinGW - 适用于 Windows 的极简 GNU

这个项目正在迁移到osdn.net/projects/mingw的过程中,你可以继续在那里关注我们。MinGW:GNU编译器集合(GCC)的本地Windows移植版本,可自由分发的导入库和用于构建本地Windows应用程序的头文件;包括对MSVC运行时的扩展,以支持C99功能。MinGW的所有软件都可以在64位Windows平台上运行。

EditPlus 中文破解版

EditPlus 中文破解版

体积小,语法高亮,不支持代码提示功能

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

SublimeText3 Linux新版

SublimeText3 Linux新版

SublimeText3 Linux最新版

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境