当前位置:

首页 > 编程开发 > Python Ast抽象语法树的应用场景及使用方法

Python Ast抽象语法树的应用场景及使用方法

引言AbstractSyntaxTrees即抽象语法树。Ast是python源码到字节码的一种中间产物,借助ast模块可以从语法树的角度分析源码结构。此外,我们不仅可以修改和执行语法树,还可以将Source生成的语法树unparse成python源码。因此ast给python源码检查、语法分析、修改代码以及代码调试等留下了足够的发挥空间。1.AST简介Python官方提供的CPython解释器对python源码的处理过程如下:Parsesourcecodeintoaparsetree(Parser/pge

本文目录

    引言

    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, '', 'exec')
    >>> exec cm
    >>> 8

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

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

    compile(source, filename, mode, ast.PyCF_ONLY_AST)  <==> ast.parse(source, filename='', 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='add',
            args=arguments(
                args=[Name(id='x',ctx=Param()),Name(id='y',ctx=Param())],
                vararg=None,
                kwarg=None,
                defaults=[]),
            body=[Return(value=BinOp(
                left=Name(id='x',ctx=Load()),
                op=Add(),
                right=Name(id='y',ctx=Load())))],
            decorator_list=[]),
        Print(
            dest=None,
            values=[Call(
                    func=Name(id='add',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 'Function Name:%s'% node.name
              self.generic_visit(node)
              func_log_stmt = ast.Print(
                  dest = None,
                  values = [ast.Str(s = 'calling func: %s' % 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, '', 'exec')

     运行结果:

    Function Name:add
    def add(x, y):
        print 'calling func: add'
        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 == 'add':
                  node.name = 'sub'
              args_num = len(node.args.args)
              args = tuple([arg.id for arg in node.args.args])
              func_log_stmt = ''.join(["print 'calling func: %s', " % node.name, "'args:'", ", %s" * args_num % args])
              node.body.insert(0, ast.parse(func_log_stmt))
              return node
          def visit_Name(self, node):
              replace = {'add': 'sub', 'x': 'a', 'y': 'b'}
              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, '', 'exec')        # 新加入的node func_log_stmt 缺少lineno和col_offset属性
      exec compile(source, '', 'exec')
      exec compile(ast.parse(source), '', 'exec')

    结果:

    def sub(a, b):
        print 'calling func: sub', 'args:', 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 = ('utf-8', 'gbk')
          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'\u4e00' <= char <= u'\u9fff' 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'\u4e00' <= char <= u'\u9fff' for char in node.s.decode('utf-8')):
              if self.cn_checker.is_any_chinese(node.s, True):
                  print 'line no: %d, column offset: %d, CN_Str: %s' % (node.lineno, node.col_offset, node.s)
      project_dir = './your_project/script'
      for root, dirs, files in os.walk(project_dir):
          print root, dirs, files
          py_files = filter(lambda file: file.endswith('.py'), files)
          checker = CodeCheck()
          for file in py_files:
              file_path = os.path.join(root, file)
              print 'Checking: %s' % file_path
              with open(file_path, 'r') 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, 'line no:%s' % name_node.lineno, 'var:%s' % 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 = './your project/script'
      for root, dirs, files in os.walk(project_dir):
          py_files = filter(lambda file: file.endswith('.py'), 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, 'r') as f:
                  root_node = ast.parse(f.read())
                  checker.visit(root_node)
          res = '\n'.join([' ## '.join(info) for info in checker.illegal_args_list])
          print res

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

    本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
    作者最新文章
    编程开发 Python
    相关文章 更多
    python删除一个列表里的重复元素的方法与代码示例
    python删除一个列表里的重复元素的方法与代码示例

    学习如何使用Python高效删除列表中的重复元素。本文提供5种常见方法的代码示例,涵盖保持顺序与不保持顺序的场景,适合初学者和进阶开发者参考。

    Python写接口的步骤及完整代码示例
    Python写接口的步骤及完整代码示例

    详细讲解使用Python编写Web接口的核心步骤与完整代码示例,涵盖环境准备、路由定义、数据校验、服务启动及调试测试方法。

    codex怎么读取word文档及docx文件操作教程
    codex怎么读取word文档及docx文件操作教程

    介绍如何利用编程工具读取和解析Word文档(docx格式),涵盖基础文本提取代码示例、结构化数据处理逻辑,以及处理复杂排版时的技术局限与替代方案。

    怎么在Python中调用C语言代码提升性能
    怎么在Python中调用C语言代码提升性能

    详细讲解在Python中调用C语言代码提升性能的完整实践指南,包含原理分析、编译步骤、ctypes与CFFI具体实现、性能对比及常见陷阱。

    Python与C语言的性能差异及底层原理
    Python与C语言的性能差异及底层原理

    深入解析Python与C语言在执行效率上的数量级差异,从编译机制、变量内存布局、动态类型推断到GIL锁,系统阐明底层核心原理。

    Python和C语言哪个更适合编程入门
    Python和C语言哪个更适合编程入门

    客观对比Python与C语言在心智负担、底层机制与学习反馈上的核心差异,提供清晰有据的选择标准与切实的入门学习路径。

    selenium上传文件教程本地文件路径设置方法
    selenium上传文件教程本地文件路径设置方法

    深入解析Selenium处理文件上传的技术细节,涵盖标准HTML控件的路径直接注入法与非标准控件的系统级模拟方案,提供Windows与Linux环境下的路径设置最佳实践及常见异常排查。

    大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景
    大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景

    针对大数据工程场景,系统对比Java、Scala与Python在执行性能、生态结合、开发效率和维护成本上的核心差异,给出清晰的架构选型依据与落地方案。

    langgraph python guide中文入门教程详解
    langgraph python guide中文入门教程详解

    本篇LangGraph Python中文入门教程详解,系统梳理StateGraph状态机定义、节点编写、边与条件路由、检查点持久化(MemorySaver)等核心机制,提供从环境安装到完整循环智能体构建的代码示例与实操指导。

    Windows 安装 Python:从官方入口到虚拟环境的可复现实践
    Windows 安装 Python:从官方入口到虚拟环境的可复现实践

    面向后端开发者,基于 Python 官方文档说明如何在 Windows 上安装 Python、创建 venv 虚拟环境及使用 pip 管理依赖。包含可复制命令、验证方法及工具选择边界,避免常见配置误区。

    查看更多
    精品专题 更多
    装机必备
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows
    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    PDF教程
    PDF教程

    正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

    Mac软件 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    Aperty macOS版
    Aperty macOS版
    macOS

    Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

    WINDOWS 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    PixTV 网页版
    PixTV 网页版
    Windows

    PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。