Что такое Абстрактное синтаксическое дерево (abstract syntax tree)?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Абстрактное синтаксическое дерево (AST)
Abstract Syntax Tree (AST) — это древовидная структура данных, которая представляет синтаксическую структуру исходного кода. Каждый узел дерева соответствует конструкции исходного языка (функция, оператор, выражение и т.д.), а ребра показывают отношения между ними.
Как работает парсинг кода
Исходный код: a = 5 + 3
|
v
Токенизация: [NAME(a), ASSIGN(=), NUMBER(5), PLUS(+), NUMBER(3)]
|
v
Парсинг: AST
|
v
Модуль
|
Assign
|
+-- targets: [Name('a')]
+-- value: BinOp
|
+-- left: Constant(5)
+-- op: Add()
+-- right: Constant(3)
AST в Python
Python имеет встроенный модуль ast для работы с синтаксическими деревьями:
import ast
code = '''
a = 5 + 3
b = a * 2
'''
# Парсим код в AST
tree = ast.parse(code)
# Выводим структуру
print(ast.dump(tree, indent=2))
Вывод:
Module(
body=[
Assign(
targets=[Name(id='a')],
value=BinOp(
left=Constant(value=5),
op=Add(),
right=Constant(value=3))),
Assign(
targets=[Name(id='b')],
value=BinOp(
left=Name(id='a'),
op=Mult(),
right=Constant(value=2)))
])
Основные типы узлов AST
import ast
# Выражения (Expression)
code = "x + 5"
node = ast.parse(code)
print(type(node.body[0].value)) # <class '_ast.BinOp'>
# Функции (FunctionDef)
def foo(x):
return x * 2
tree = ast.parse('def foo(x):\n return x * 2')
func = tree.body[0]
print(type(func)) # <class '_ast.FunctionDef'>
print(func.name) # 'foo'
print(func.args.args) # Аргументы функции
# Циклы
for_loop = ast.parse('for i in range(10): print(i)')
print(type(for_loop.body[0])) # <class '_ast.For'>
# If-else
if_stmt = ast.parse('if x > 5:\n print("больше")')
print(type(if_stmt.body[0])) # <class '_ast.If'>
Обход AST (NodeVisitor)
NodeVisitor позволяет обойти дерево и выполнить действия для разных типов узлов:
import ast
class FunctionFinder(ast.NodeVisitor):
"""Находит все функции в коде"""
def __init__(self):
self.functions = []
def visit_FunctionDef(self, node):
"""Вызывается для каждой функции"""
self.functions.append({
'name': node.name,
'args': [arg.arg for arg in node.args.args],
'line': node.lineno
})
self.generic_visit(node) # Обходим дочерние узлы
def visit_AsyncFunctionDef(self, node):
"""Асинхронные функции"""
self.functions.append({
'name': node.name,
'type': 'async',
'args': [arg.arg for arg in node.args.args]
})
self.generic_visit(node)
code = '''
def hello(name):
print(f"Hello, {name}")
async def fetch_data(url):
pass
'''
tree = ast.parse(code)
finder = FunctionFinder()
finder.visit(tree)
for func in finder.functions:
print(func)
# {'name': 'hello', 'args': ['name'], 'line': 1}
# {'name': 'fetch_data', 'type': 'async', 'args': ['url']}
Практический пример: Поиск импортов
import ast
class ImportFinder(ast.NodeVisitor):
"""Находит все импорты в коде"""
def __init__(self):
self.imports = []
def visit_Import(self, node):
"""import x, y, z"""
for alias in node.names:
self.imports.append({
'type': 'import',
'module': alias.name,
'alias': alias.asname
})
self.generic_visit(node)
def visit_ImportFrom(self, node):
"""from x import y, z"""
for alias in node.names:
self.imports.append({
'type': 'from',
'module': node.module,
'name': alias.name,
'alias': alias.asname
})
self.generic_visit(node)
code = '''
import os
import sys as system
from django.db import models
from collections import defaultdict as dd
'''
tree = ast.parse(code)
finder = ImportFinder()
finder.visit(tree)
for imp in finder.imports:
print(imp)
Практический пример: Поиск переменных и их использования
import ast
class VariableAnalyzer(ast.NodeVisitor):
"""Анализирует использование переменных"""
def __init__(self):
self.assignments = set() # Переменные которые присваиваются
self.usages = set() # Переменные которые используются
def visit_Assign(self, node):
"""x = 5"""
for target in node.targets:
if isinstance(target, ast.Name):
self.assignments.add(target.id)
self.generic_visit(node)
def visit_Name(self, node):
"""Использование переменной"""
if isinstance(node.ctx, ast.Load):
self.usages.add(node.id)
self.generic_visit(node)
code = '''
x = 5
y = x + 10
z = y * 2
print(x, y, z)
'''
tree = ast.parse(code)
analyzer = VariableAnalyzer()
analyzer.visit(tree)
print(f'Присвоены: {analyzer.assignments}')
print(f'Используются: {analyzer.usages}')
print(f'Неиспользованные: {analyzer.assignments - analyzer.usages}')
Практический пример: Поиск потенциальных проблем
import ast
class ProblemFinder(ast.NodeVisitor):
"""Находит потенциальные проблемы в коде"""
def __init__(self):
self.problems = []
def visit_Except(self, node):
"""Ловят все исключения без типа"""
if node.type is None:
self.problems.append(f'Line {node.lineno}: Bare except clause')
self.generic_visit(node)
def visit_Compare(self, node):
"""x == None вместо x is None"""
for op, comparator in zip(node.ops, node.comparators):
if isinstance(op, ast.Eq):
if isinstance(comparator, ast.Constant) and comparator.value is None:
self.problems.append(f'Line {node.lineno}: Use "is None" not "== None"')
self.generic_visit(node)
code = '''
try:
x = 1 / 0
except: # Проблема!
pass
if x == None: # Проблема!
pass
'''
tree = ast.parse(code)
finder = ProblemFinder()
finder.visit(tree)
for problem in finder.problems:
print(problem)
Изменение AST (NodeTransformer)
NodeTransformer позволяет изменять дерево:
import ast
class VariableRenamer(ast.NodeTransformer):
"""Переименовывает переменные"""
def __init__(self, old_name, new_name):
self.old_name = old_name
self.new_name = new_name
def visit_Name(self, node):
if node.id == self.old_name:
node.id = self.new_name
return node
code = '''
x = 5
y = x + 10
print(x, y)
'''
tree = ast.parse(code)
renamer = VariableRenamer('x', 'variable')
tree = renamer.visit(tree)
# Преобразуем обратно в код
compiled = compile(tree, filename='<ast>', mode='exec')
# Или используем astor (pip install astor)
import astor
print(astor.to_source(tree))
Использование AST в реальном мире
1. Линтеры (pylint, flake8):
# Проверяют AST на проблемы стиля и логики
2. Форматеры (black):
# Парсят код в AST, затем форматируют обратно
3. Рефакторинг инструменты:
import ast
# Автоматическое переименование переменных
# Удаление неиспользуемых импортов
# Оптимизация кода
4. Анализ кода (инструменты типа mypy):
# Проверка типов на основе AST
Граммарика Python в AST
import ast
# Все возможные типы узлов
print(dir(ast))
# Основные:
# Module, FunctionDef, AsyncFunctionDef, ClassDef
# Return, Delete, Assign, AugAssign, For, AsyncFor, While
# If, With, AsyncWith, Try, Assert, Import, ImportFrom
# Global, Nonlocal, Expr, Pass, Break, Continue
Ограничения AST
import ast
# AST теряет часть информации:
code = "x = 5 # комментарий"
tree = ast.parse(code)
# Пробелы потеряны
# Комментарии потеряны
# Форматирование потеряно
# Для сохранения всей информации используйте libcst
Итого
AST используется для:
- Анализа кода (линтеры, type checkers)
- Трансформации кода (форматеры, рефакторинг)
- Выполнение пользовательского кода безопасно
- Создание инструментов разработки
- Оптимизация кода
Ключевые компоненты:
- ast.parse() — парсит код в дерево
- ast.NodeVisitor — обходит дерево
- ast.NodeTransformer — изменяет дерево
- ast.dump() — выводит структуру
Альтернативы:
- libcst — сохраняет точное форматирование
- parso — парсер Python с полной информацией
- ast-grep — grep для AST структур
AST это фундаментальный инструмент для работы с кодом на уровне структуры, а не текста.