抽象構文ツリーは抽象構文ツリーです。 Ast は Python のソース コードからバイトコードへの中間生成物であり、ast モジュールを利用することで、ソース コードの構造を構文ツリーの観点から解析できます。
さらに、構文ツリーを変更して実行するだけでなく、Source によって生成された構文ツリーを解析して Python ソース コードにすることもできます。したがって、ast には、Python ソース コードのチェック、構文分析、コードの変更、およびコードのデバッグのための十分な余地が残されています。
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, )])
まず、コンパイル関数について簡単に理解しましょう。
compile(source, filename, mode[, flags[, dont_inherit]])
func_def = \ """ def add(x, y): return x + y print add(3, 5) """
>>> cm = compile(func_def, '<string>', 'exec') >>> exec cm >>> 8
True == isinstance(cm, types.CodeType)。compile(source, filename, mode, ast.PyCF_ONLY_AST) <==> ast.parse(source, filename='
2.2 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 を除いて、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 を走査する
3.1 ast.NodeTransfer
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, '<string>', '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
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 == '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, '<string>', 'exec') # 新加入的node func_log_stmt 缺少lineno和col_offset属性 exec compile(source, '<string>', 'exec') exec compile(ast.parse(source), '<string>', '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
2 つの違いはコードで明確に見ることができます。ここでは詳細には触れません。
4.AST アプリケーション
上記の関数への呼び出しログ情報の追加は、Python ソース コードをデバッグする方法ですが、実際には、Python ファイル全体を解析することでソース コードを走査し、変更します。
4.1 漢字の検出
範囲 : 4E00— 9FFF
\u4e00 - \u9fff文字数 : 20992
言語 : 中国語、日本語、韓国語、ベトナム語
Unicode 範囲を使用します
中国語の文字を識別するには、この範囲に中国語の文字が含まれていないことに注意してください (例: 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 には 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'\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
一个函数中定义的函数或者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节点。
以上がPython Ast 抽象構文ツリーはどのように使用すればよいですか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。