Maison  >  Article  >  développement back-end  >  Comment utiliser l’arbre de syntaxe abstraite Python Ast ?

Comment utiliser l’arbre de syntaxe abstraite Python Ast ?

WBOY
WBOYavant
2023-05-09 12:49:081164parcourir

Introduction

Les arbres de syntaxe abstraite sont des arbres de syntaxe abstraite. Ast est un produit intermédiaire du code source Python au bytecode. À l'aide du module ast, la structure du code source peut être analysée du point de vue d'un arbre syntaxique.

De plus, nous pouvons non seulement modifier et exécuter l'arbre de syntaxe, mais également analyser l'arbre de syntaxe généré par Source dans le code source python. Par conséquent, ast laisse suffisamment de place pour la vérification du code source Python, l’analyse syntaxique, la modification du code et le débogage du code.

1. Introduction à AST

L'interpréteur CPython officiellement fourni par Python traite le code source de python comme suit :

Analyser le code source en un arbre d'analyse (Parser/pgen.c)

Transformer l'arbre d'analyse en un arbre de syntaxe abstraite (Python/ast.c)

Transformer AST en un graphique de flux de contrôle ( Python/compile.c)

Émettre du bytecode basé sur le Control Flow Graph (Python/compile.c)

C'est-à-dire le processus de traitement du code Python réel est le suivant :

Analyse du code source--> Arbre de syntaxe--> Arbre de syntaxe abstraite (AST)--> #Ci-dessus La procédure est appliquée après python2.5. Le code source Python est d'abord analysé dans un arbre syntaxique, puis converti en un arbre syntaxique abstrait. Dans l'arbre de syntaxe abstraite, nous pouvons voir la structure syntaxique de Python dans le fichier de code source.

La plupart du temps, la programmation ne nécessite pas l'utilisation d'arbres de syntaxe abstraits, mais dans des conditions et exigences spécifiques, AST a sa propre commodité particulière.

Ce qui suit est un exemple simple de syntaxe abstraite.

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

2. Créer AST

2.1 Fonction de compilation

Tout d'abord, comprenons brièvement la fonction de compilation.

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

    source - - Objet String ou AST (Abstract Syntax Trees). Généralement, l'intégralité du contenu du fichier py peut être transmise à file.read().
  • filename -- le nom du fichier de code, ou transmettez une valeur identifiable si le code n'est pas lu à partir d'un fichier.
  • mode -- Spécifiez le type de code compilé. Peut être spécifié comme exec, eval, single.
  • flags -- Portée variable, espace de noms local, s'il est fourni, peut être n'importe quel objet de mappage.
  • flags et dont_inherit sont des drapeaux utilisés pour contrôler lors de la compilation du code source.
  • func_def = \
    """
    def add(x, y):
        return x + y
    print add(3, 5)
    """
  • Utilisez Compile pour compiler et exécuter :
>>> cm = compile(func_def, &#39;<string>&#39;, &#39;exec&#39;)
>>> exec cm
>>> 8

Le func_def ci-dessus est compilé par compilation pour obtenir le bytecode, cm est l'objet code , #🎜🎜 #

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

compile(source, nom de fichier, mode, ast.PyCF_ONLY_AST) 209861d5cd2975725c730f519ed6ad71 ast.parse(source, nom de fichier='fd1b9d7e3c29612590ebb15a24338584', mode ='exec')

2.2 Générer ast

Utilisez le func_def ci-dessus pour générer ast.

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

Ce qui suit est l'ast correspondant à func_def Structure :

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

En plus de ast.dump, il existe de nombreuses bibliothèques tierces qui dumpent ast, telles que astunparse, codegen, unparse, etc. Ces bibliothèques tierces peuvent non seulement mieux afficher la structure AST, mais également exporter inversement l'AST vers le code source 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)
}

Ce qui précède fait partie de la grammaire abstraite extraite du site officiel Lors de la traversée réelle du nœud ast, ses propriétés sont accessibles en fonction du type du nœud.

3. Traverse AST

Python propose deux façons de parcourir l'ensemble de l'arborescence de syntaxe abstraite.

3.1 ast.NodeTransfer

Modifiez l'opération d'addition dans la fonction d'ajout de func_def en soustraction et ajoutez un journal d'appels pour l'implémentation de la fonction.

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

Résultats d'exécution :

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

L'utilisation de NodeVisitor modifie principalement la structure AST en modifiant les nœuds de l'arborescence syntaxique. Principalement pour remplacer les nœuds dans ast.

Puisque l'ajout défini dans func_def a été modifié en fonction de soustraction, nous serons alors plus approfondis et modifierons le nom de la fonction, les paramètres et les fonctions appelées dans ast, et ajouterons la fonction Le journal des appels est plus compliqué à écrire, essayez de le changer au-delà de la reconnaissance :-)

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

Résultat :

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

La différence entre les deux est clairement visible dans le code. Je n’entrerai pas dans les détails ici.

4.L'application AST

Le module AST est rarement utilisé dans la programmation réelle, mais il est très significatif en tant que méthode auxiliaire de vérification du code source, de vérification de la syntaxe, de débogage des erreurs, de champ spécial ; détection, etc.

L'ajout d'informations du journal d'appels à la fonction ci-dessus est un moyen de déboguer le code source python, mais en réalité, nous parcourons et modifions le code source en analysant l'intégralité du fichier python.

4.1 Détection des caractères chinois

Ce qui suit est la plage d'encodage Unicode des caractères chinois, japonais et coréens

Idéogrammes unifiés CJK # 🎜🎜##🎜 🎜#

Gamme : 4E00—9FFF

Nombre de caractères : 20992
Langues : chinois, japonais, coréen, viet nom #🎜 🎜## 🎜🎜#

Utilisez la plage Unicode

u4e00 - u9fff

pour identifier les caractères chinois (par exemple, u';' == u. 'uff1b').#🎜🎜 #

Ce qui suit est une classe CNCheckHelper qui détermine si une chaîne contient des caractères chinois :

  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

L'interface is_any_chinese a deux modes de jugement. tant qu'il contient des chaînes chinoises, il n'est pas strictement nécessaire d'inclure tous les caractères chinois.

下面我们利用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节点。

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer