Шаг 180.
Python: сборник рецептов.
Метапрограммирование. Парсинг и анализ исходного кода Python

    На этом шаге мы рассмотрим использование модуля 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.




Предыдущий шаг Содержание Следующий шаг