ホームページ >バックエンド開発 >Python チュートリアル >Python Ast 抽象構文ツリーはどのように使用すればよいですか?

Python Ast 抽象構文ツリーはどのように使用すればよいですか?

WBOY
WBOY転載
2023-05-09 12:49:081264ブラウズ

はじめに

抽象構文ツリーは抽象構文ツリーです。 Ast は Python のソース コードからバイトコードへの中間生成物であり、ast モジュールを利用することで、ソース コードの構造を構文ツリーの観点から解析できます。

さらに、構文ツリーを変更して実行するだけでなく、Source によって生成された構文ツリーを解析して Python ソース コードにすることもできます。したがって、ast には、Python ソース コードのチェック、構文分析、コードの変更、およびコードのデバッグのための十分な余地が残されています。

1. AST の概要

Python が公式に提供する CPython インタープリターは、Python ソース コードを次のように処理します:

ソース コードを解析ツリー (Parser/ pgen.c)

解析ツリーを抽象構文ツリーに変換する (Python/ast.c)

AST を制御フロー グラフに変換する (Python/compile.c)

制御フローグラフに基づいてバイトコードを出力する (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(source, filename, mode[, flags[, dont_inherit]])

  • #source -- 文字列または AST (抽象構文ツリー)物体。一般に、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)
    """
コンパイルを使用してコンパイルして実行します。

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

上記の func_def は、バイトコードを取得するためにコンパイルによってコンパイルされます。cm はコード オブジェクトです。

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

compile(source, filename, mode, ast.PyCF_ONLY_AST) 209861d5cd2975725c730f519ed6ad71 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=&#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 を除いて、astunparse、codegen、unparse など、ast をダンプするためのサードパーティ ライブラリが多数あります。これらのサードパーティ ライブラリは、AST 構造をより適切な方法で表示できるだけでなく、AST を Python ソース コードに逆にエクスポートすることもできます。

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)
}

上記は公式 Web サイトから抜粋した抽象文法の一部ですが、実際の ast ノードのトラバース中に、そのプロパティはノードのタイプに従ってアクセスされます。

3. AST を走査する

Python には、抽象構文ツリー全体を走査する 2 つの方法が用意されています。

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 内の AST を置き換えます。 .ノード。

func_def で定義されていた加算が減算関数に変更されたため、より徹底して関数名、パラメータ、呼び出される関数を ast に変更し、追加された関数の呼び出しをログに記録します。記述はさらに複雑になります。 、そして私はそれを認識できないほど変更しようとします:-)

  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

2 つの違いはコードで明確に見ることができます。ここでは詳細には触れません。

4.AST アプリケーション

AST モジュールは実際のプログラミングではほとんど使用されませんが、構文チェック、エラーのデバッグ、特殊フィールドの検出待機などの補助的なソース コード チェック方法として非常に有意義です。

上記の関数への呼び出しログ情報の追加は、Python ソース コードをデバッグする方法ですが、実際には、Python ファイル全体を解析することでソース コードを走査し、変更します。

4.1 漢字の検出

次は、中国語、日本語、および韓国語の文字の Unicode エンコード範囲です。

CJK 統一表意文字

範囲 : 4E00— 9FFF

文字数 : 20992

言語 : 中国語、日本語、韓国語、ベトナム語

Unicode 範囲を使用します
\u4e00 - \u9fff

中国語の文字を識別するには、この範囲に中国語の文字が含まれていないことに注意してください (例: 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 には 2 つの判定モードがあり、厳密な検出は中国語の文字列が含まれていればチェックでき、非厳密な検出はすべての中国語の文字が含まれている必要があります。

下面我们利用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 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はyisu.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。