Визуализация кода на Python
Визуализация кода Python: обзор инструментов и подходов
Визуализация кода помогает разработчикам быстрее понимать структуру проектов, выявлять узкие места и анализировать потоки выполнения. Ниже рассмотрены несколько методов: от простого построения графа вызовов до визуализации синтаксических деревьев и профилирования.
Главный инструмент: code2flow
Как построить граф потока управления для всего проекта?
Утилита code2flow анализирует исходный код Python и создаёт визуальный граф вызовов функций. Установка проста:
pip install code2flowвизуализация кода python (визуализация кода python)
Затем для файла или папки выполняется команда:
code2flow my_script.py -o output.png
Инструмент автоматически находит все функции и строит граф их взаимных вызовов. Формат вывода может быть PNG, SVG, DOT или JSON.
Возможные проблемы:
При большом количестве функций граф становится перегруженным. Решение - использовать флаг --filter для выбора нужных модулей. Если код использует динамическую типизацию или декораторы, некоторые вызовы могут быть пропущены.
Вариант 1: ручное построение графа с Graphviz
Как нарисовать произвольные диаграммы зависимостей без готового анализатора?
Библиотека graphviz позволяет создавать графы «с нуля». Пример для визуализации структуры классов:
from graphviz import Digraph
dot = Digraph('project')
dot.node('A', 'ClassA')
dot.node('B', 'ClassB')
dot.edge('A', 'B', label='depends on')
dot.render('classes.gv', view=True)
Такой подход удобен для небольших проектов, но требует ручного описания всех связей.
Типичная ошибка:
Игнорирование вложенных структур (например, методов внутри классов). Для корректного отображения лучше сначала получить AST.
Вариант 2: визуализация AST (синтаксическое дерево)
Как визуально исследовать структуру кода до его выполнения?
Модуль ast вместе с graphviz позволяет нарисовать дерево разбора:
import ast
from graphviz import Digraph
code = """
def hello():
print('Hi')
"""
tree = ast.parse(code)
dot = Digraph()
def visit(node, parent=None):
name = type(node).__name__
dot.node(str(id(node)), name)
if parent:
dot.edge(str(id(parent)), str(id(node)))
for child in ast.iter_child_nodes(node):
visit(child, node)
visit(tree)
dot.render('ast.gv', view=True)
Это даёт полное представление о синтаксической структуре. Подходит для обучения, отладки парсеров.
Сложность:
Для больших файлов дерево становится огромным. Лучше фильтровать узлы (например, только FunctionDef).
Вариант 3: граф вызовов с pycallgraph
Как визуализировать реальные вызовы функций во время исполнения?
Хотя библиотека pycallgraph сейчас не обновляется, она всё ещё работает для простых случаев. Установка:
pip install pycallgraph
Пример декорирования точки входа:
from pycallgraph import PyCallGraph
from pycallgraph.output import GraphvizOutput
with PyCallGraph(output=GraphvizOutput()):
# код, который нужно отследить
my_function()
По окончании создаётся PNG-файл с графом. Однако для современных проектов (асинхронность, декораторы) лучше перейти на code2flow.
Известные ошибки:
pycallgraph не отслеживает вызовы в сторонних библиотеках, если они не помечены особым образом. Возможны зацикливания.
Вариант 4: анализ зависимостей модулей с pyan3
Как увидеть, какие модули импортируют другие?
Инструмент pyan3 строит граф импортов. Установка:
pip install pyan3
Команда:
pyan3 my_project/*.py --dot -o imports.dot
# затем преобразовать в PNG:
dot -Tpng -o imports.png imports.dot
Это помогает реорганизовать проект, избавиться от циклических зависимостей.
Ограничение:
pyan3 анализирует только статические импорты (не динамические через importlib).
Вариант 5: визуализация профилирования с snakeviz
Как визуально изучить результаты профилирования?
Вместо текстовых отчётов cProfile можно открыть интерактивный график в browser через snakeviz:
python -m cProfile -o profile.prof my_script.py
snakeviz profile.prof
В браузере появится круговой граф, где размер секторов соответствует времени выполнения функций. Удобно для поиска узких мест.
Нюансы:
Для асинхронного кода профилирование требует отдельной настройки. snakeviz не показывает вызовы, если функция вызвана миллионы раз – граф становится трудночитаемым.
Расширенные примеры визуализации кода
Приведённые ниже примеры демонстрируют более глубокое использование описанных инструментов. Для каждого примера показан исходный код и результат визуализации.
Пример 1: code2flow для многофайлового проекта
Пусть есть два файла: utils.py и main.py. В utils.py определена функция helper, в main.py – функция process, которая вызывает helper.
# utils.py
def helper(data):
return data * 2
# main.py
from utils import helper
def process():
result = helper(10)
print(result)
if __name__ == '__main__':
process()
Запуск code2flow:
code2flow main.py -o project_flow.png
Результат (граф в PNG) показывает две функции: process (вызывает helper) и helper (вызывается из process). Добавление опции --hide-unreachable скрывает функции, не имеющие входных точек.
[изображение: граф с двумя узлами process->helper]
Пример 2: AST визуализация с фильтрацией узлов
Визуализируем только функции и классы из файла, игнорируя вызовы и выражения:
import ast, graphviz
code = open('example.py').read()
tree = ast.parse(code)
dot = Digraph(format='png')
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.ClassDef)):
name = node.name
dot.node(name, name)
# поиск вызовов внутри тела
for child in ast.walk(node):
if isinstance(child, ast.Call):
if isinstance(child.func, ast.Name):
called = child.func.id
dot.edge(name, called)
dot.render('ast_filtered.png', view=True)
Результат – граф вызовов, построенный на основе синтаксического анализа, без запуска кода.
[изображение: граф с узлами main и helper, стрелка от main к helper]
Пример 3: pyan3 с группировкой по пакетам
Структура проекта:
project/
__init__.py
core.py (импортирует utils.helpers)
utils/
__init__.py
helpers.py
parsing.py
Запуск pyan3 с группировкой в кластеры:
pyan3 project/*.py project/utils/*.py --dot --no-defines -o imports.dot
dot -Tpng imports.dot -o imports.png
Опция --no-defines убирает функции, оставляя только модули. Результат – граф, где видно, что core.py зависит от utils/helpers.py.
[изображение: модули с иерархией импортов]
Пример 4: snakeviz с фильтрацией по времени
Профилируем скрипт, который много раз вызывает io-операции:
import time, random
def io_task():
time.sleep(random.uniform(0.01, 0.1))
def heavy():
for _ in range(100):
io_task()
if __name__ == '__main__':
import cProfile
cProfile.run('heavy()', sort='time')
Сохраняем профиль и открываем в snakeviz – видно, что io_task занимает 95% времени. Можно кликнуть на функцию, чтобы увидеть её вызовы.
[в браузере: круговая диаграмма с большим сектором io_task]
Пример 5: динамический граф вызовов с trace и graphviz
Без дополнительных библиотек можно использовать модуль trace для сбора вызовов и построить граф:
import trace
from graphviz import Digraph
# создаём объект trace
tracer = trace.Trace(count=True, trace=True, ignoredirs=[sys.prefix, sys.exec_prefix])
tracer.runfunc(my_function)
# получаем статистику вызовов
results = tracer.results()
# преобразуем в граф (упрощённо)
dot = Digraph()
for call, count in results.counts.items():
caller, callee = call
dot.node(caller, str(caller))
dot.node(callee, str(callee))
dot.edge(caller, callee, label=str(count))
dot.render('callgraph.png')
Этот способ даёт более детальный граф, чем code2flow, но требует больше кода.
[изображение: граф с числами вызовов на рёбрах]