На этом шаге мы рассмотрим использование модуля ast.
Вы хотите писать программы, которые парсят и анализируют исходный код Python.
Многие программисты знают, что Python может выполнять или вычислять исходный код, предоставленный в форме строки. Например:
>>> x = 42 >>> eval('2 + 3*4 + x') 56 >>> exec('for i in range(10): print(i)') 0 1 2 3 4 5 6 7 8 9 >>>
Однако можно использовать модуль ast, чтобы скомпилировать исходный код Python в абстрактное синтаксическое дерево (AST), которое может быть проанализировано. Например:
>>> import ast >>> ex = ast.parse('2 + 3*4 + x', mode='eval') >>> ex <_ast.Expression object at 0x000002AB966D7520> >>> ast.dump(ex) "Expression(body=BinOp(left=BinOp(left=Constant(value=2, kind=None), op=Add(), right=BinOp(left=Constant(value=3, kind=None), op=Mult(), right=Constant(value=4, kind=None))), op=Add(), right=Name(id='x', ctx=Load())))" >>> top = ast.parse('for i in range(10): print(i)', mode='exec') >>> top <_ast.Module object at 0x000002AB96728CA0> >>> ast.dump(top) "Module(body=[For(target=Name(id='i', ctx=Store()), iter=Call(func=Name(id='range', ctx=Load()), args=[Constant(value=10, kind=None)], keywords=[]), body=[Expr(value=Call(func=Name(id='print', ctx=Load()), args=[Name(id='i', ctx=Load())], keywords=[]))], orelse=[], type_comment=None)], type_ignores=[])" >>>
Анализ исходного дерева требует от вас некоторых усилий. Он состоит из коллекции AST-узлов. Самый простой способ работы с этим узлами - определить класс-посетитель, который реализует различные методы visitNodeName, где NodeName совпадает с интересующим узлом. Вот пример такого класса, который записывает информацию о том, какие имена были загружены, сохранены и удалены.
import ast class CodeAnalyzer(ast.NodeVisitor): def __init__(self): self.loaded = set() self.stored = set() self.deleted = set() def visit_Name(self, node): if isinstance(node.ctx, ast.Load): self.loaded.add(node.id) elif isinstance(node.ctx, ast.Store): self.stored.add(node.id) elif isinstance(node.ctx, ast.Del): self.deleted.add(node.id) # Пример использования if __name__ == '__main__': # Какой-то код code = ''' for i in range(10): print(i) del i ''' # Парсим в AST top = ast.parse(code, mode='exec') # Скармливаем AST анализатору использования имен c = CodeAnalyzer() c.visit(top) print('Loaded:', c.loaded) print('Stored:', c.stored) print('Deleted:', c.deleted)
Если вы запустите эту программу, то получите такой вывод:
Loaded: {'i', range', 'print'}
Stored: {'i'}
Deleted: {'i'}
Наконец, AST могут быть скомпилированы и исполнены с использованием функции compile(). Например:
>>> exec(compile(top,'<stdin>', 'exec')) 0 1 2 3 4 5 6 7 8 9 >>>
Тот факт, что вы можете анализировать исходный код и извлекать из него информацию, может стать основой для написания различных инструментов для анализа, оптимизации и верификации кода. Например, вместо передачи фрагментов кода в функцию exec() вы можете превратить его в AST и изучить, чтобы понять, что он делает. Вы можете также написать инструменты, которые просматривают весь исходный код модуля и производят некий статический анализ.
Стоит отметить, что можно переписать AST, чтобы представить новый код, если вы на самом деле понимаете, что делаете. Вот пример декоратора, который "опускает" имена с глобальным доступом в тело функции путем репарсинга исходного кода тела функции, переписывания AST и воссоздания объекта кода функции:
import ast import inspect # Узел-посетитель, который "понижает" глобально доступные # имена в тело функции, делая их локальными переменными. class NameLower(ast.NodeVisitor): def __init__(self, lowered_names): self.lowered_names = lowered_names def visit_FunctionDef(self, node): # Компилируем некие присвоения для понижения регистра констант code = '__globals = globals()\n' code += '\n'.join("{0} = __globals['{0}']".format(name) for name in self.lowered_names) code_ast = ast.parse(code, mode='exec') # Инъецируем новые инструкции в тело функции node.body[:0] = code_ast.body # Сохраняем объект функции self.func = node # Декоратор, который превращает глобальные имена в локальные def lower_names(*namelist): def lower(func): srclines = inspect.getsource(func).splitlines() # Пропускаем линии исходного кода перед декоратором @lower_names for n, line in enumerate(srclines): if '@lower_names' in line: break src = '\n'.join(srclines[n+1:]) # Хак, чтобы разобраться с отступами кода if src.startswith((' ', '\t')): src = 'if 1:\n' + src top = ast.parse(src, mode='exec') # Трансформируем AST cl = NameLower(namelist) cl.visit(top) # Выполняем модифицированное AST temp = {} exec(compile(top, '', 'exec'), temp, temp) # Достаем модифицированный объект кода func.__code__ = temp[func.__name__].__code__ return func return lower
Чтобы использовать этот код, вы могли бы написать что-то такое:
# Пример INCR = 1 def countdown1(n): while n > 0: n -= INCR @lower_names('INCR') def countdown2(n): while n > 0: n -= INCR if __name__ == '__main__': import time print('Running a performance check') start = time.time() countdown1(100000000) end = time.time() print('countdown1:', end-start) start = time.time() countdown2(100000000) end = time.time() print('countdown2:', end-start)
Декоратор переписывает исходный код функции countdown2(), чтобы она выглядела так:
def countdown2(n): __globals = globals() INCR = __globals['INCR'] while n > 0: n -= INCR
В тесте производительности выяснилось, что это заставило функцию работать в среднем на 10-20% быстрее:
Running a performance check countdown1: 9.535615682601929 countdown2: 8.019522666931152
Должны ли вы применить этот декоратор ко всем своим функциям? Скорее всего, нет. Однако это хороший пример весьма продвинутых вещей, которые доступны через манипуляции AST, исходным кодом и прочие подобные приемы.
Работа с AST - это высокоуровневый подход, который мог бы быть немного прямолинейнее. См. следующий шаг, в котором приведены сведения о байт-коде.
На следующем шаге мы рассмотрим дизассемблирование байт-кода Python.