Pokok Sintaks Abstrak ialah pokok sintaks abstrak. Ast ialah produk perantaraan daripada kod sumber Python kepada kod bait Dengan bantuan modul ast, struktur kod sumber boleh dianalisis dari perspektif pokok sintaks.
Selain itu, kami bukan sahaja boleh mengubah suai dan melaksanakan pepohon sintaks, tetapi juga membongkar pepohon sintaks yang dijana oleh Sumber ke dalam kod sumber python. Oleh itu, ast meninggalkan ruang yang cukup untuk pemeriksaan kod sumber Python, analisis sintaks, pengubahsuaian kod dan penyahpepijatan kod.
Penterjemah CPython yang disediakan secara rasmi oleh Python memproses kod sumber python seperti berikut:
Menghuraikan kod sumber ke dalam pepohon parse (Parser/ pgen.c)
Ubah pepohon parse kepada Pokok Sintaks Abstrak (Python/ast.c)
Ubah AST kepada Graf Aliran Kawalan (Python/compile.c)
Pancarkan bytecode berdasarkan Graf Aliran Kawalan (Python/compile.c)
Iaitu, proses pemprosesan kod python sebenar adalah seperti berikut:
Analisis kod sumber--> ; Pokok sintaks- -> Pokok sintaks abstrak (AST) --> Kod sumber Python mula-mula dihuraikan menjadi pokok sintaks, dan kemudian ditukar menjadi pokok sintaks abstrak. Dalam pokok sintaks abstrak kita boleh melihat struktur sintaks Python dalam fail kod sumber.
Kebanyakan masa pengaturcaraan mungkin tidak memerlukan penggunaan pokok sintaks abstrak, tetapi di bawah syarat dan keperluan tertentu, AST mempunyai kemudahan khasnya sendiri.
Berikut ialah contoh mudah sintaks abstrak.
Module(body=[ Print( dest=None, values=[BinOp( left=Num(n=1),op=Add(),right=Num(n=2))], nl=True, )])
2. Cipta AST
func_def = \ """ def add(x, y): return x + y print add(3, 5) """
>>> cm = compile(func_def, '<string>', 'exec') >>> exec cm >>> 8
Func_def di atas disusun oleh compile untuk mendapatkan bytecode, cm ialah objek kod,
Benar == isinstance(cm, types.CodeType).susun(sumber, nama fail, mod, ast.PyCF_ONLY_AST) <==> ast.parse(sumber, nama fail='
2.2 Hasilkan ast
r_node = ast.parse(func_def) print astunparse.dump(r_node) # print ast.dump(r_node)
Berikut ialah struktur ast yang sepadan dengan func_def:
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) ])
Selain ast .dump, terdapat banyak perpustakaan pihak ketiga untuk lambakan ast, seperti astunparse, codegen, unparse, dsb. Pustaka pihak ketiga ini bukan sahaja boleh memaparkan struktur AST dengan cara yang lebih baik, tetapi juga mengeksport terbalik AST ke kod sumber 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) }
Di atas adalah sebahagian daripada Tatabahasa Abstrak yang dipetik daripada tapak web rasmi Semasa pelayaran sebenar ast Node, sifatnya diakses mengikut jenis Node.
3. Traverse 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')
Hasil berjalan:
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
Memandangkan penambahan yang ditakrifkan dalam func_def telah ditukar kepada fungsi penolakan, maka kami akan lebih teliti dan menukar nama fungsi, parameter dan fungsi yang dipanggil dalam ast, dan log panggilan fungsi tambahan Penulisan adalah lebih rumit , dan saya cuba mengubahnya di luar pengiktirafan:-)
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')
Keputusan:
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
Perbezaan antara keduanya dapat dilihat dengan jelas dalam kod. Saya tidak akan pergi ke butiran di sini.
4. Aplikasi AST
Menambah maklumat log panggilan di atas pada fungsi adalah cara untuk menyahpepijat kod sumber python, tetapi sebenarnya kami merentasi dan mengubah suai kod sumber dengan menghuraikan keseluruhan fail python.
4.1 Pengesanan aksara Cina
Julat : 4E00— 9FFF
Untuk mengenal pasti aksara Cina, ambil perhatian bahawa julat ini tidak termasuk aksara Cina (cth. u';' == u'uff1b').Bilangan aksara: 20992
Bahasa: cina, jepun, korea, vietnam
Gunakan julat unicode u4e00 - u9fff
Berikut ialah kelas CNCheckHelper yang menentukan sama ada a rentetan mengandungi aksara Cina:
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
下面我们利用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节点。
Atas ialah kandungan terperinci Bagaimanakah pokok sintaks abstrak Python Ast digunakan?. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!