Преобразование Python-кода: практические инструменты и техники
Преобразование кода Python: методы и подходы
Как безопасно изменить Python-код, не нарушая его синтаксис?
Основной метод: модуль ast
Модуль ast (Abstract Syntax Tree) позволяет разобрать код на синтаксическое дерево, модифицировать его узлы и затем скомпилировать обратно. Это единственный способ гарантировать корректность изменений, так как он работает на уровне синтаксиса, а не строк. Подход подходит для сложных трансформаций: замена вызовов функций, переименование переменных с учётом контекста, внедрение отладки.
import ast
import astor
class PrintToLogTransformer(ast.NodeTransformer):
"""Заменяет вызовы print() на logging.info()"""
def visit_Call(self, node):
if isinstance(node.func, ast.Name) and node.func.id == 'print':
# Создаём вызов logging.info с теми же аргументами
new_func = ast.Attribute(value=ast.Name(id='logging', ctx=ast.Load()),
attr='info')
node = ast.Call(func=new_func, args=node.args, keywords=node.keywords)
return self.generic_visit(node)
code = """
print('hello')
print(42, 'world')
"""
tree = ast.parse(code)
transformer = PrintToLogTransformer()
new_tree = transformer.visit(tree)
new_code = astor.to_source(new_tree)
print(new_code)
построить график по коду python онлайн (построение графика python онлайн)
import logging
logging.info('hello')
logging.info(42, 'world')
преобразовать код python (преобразование кода python)
Возможные проблемы:
- Необходима установка библиотеки
astor(или использованиеast.unparseв Python 3.9+). - Трансформации могут сломать код, если не обработаны все случаи (например,
printвнутри декораторов). - Требуется понимание структуры AST: каждое изменение должно сохранять локацию узлов, иначе ошибки компиляции.
Цель использования: автоматический рефакторинг, миграция на новые версии, добавление инструментации (логирование, замер времени) без ручного поиска.
Как заменить одно слово в коде на другое, например, print на logger.info?
Вариант 1: str.replace()
Самый простой способ: code.replace('print(', 'logger.info('). Подходит для одноразовых, контролируемых замен, когда точно известно, что выражение не встречается в строковых литералах или комментариях.
code = "print('test') # печать"
new_code = code.replace('print(', 'logger.info(')
print(new_code)
logger.info('test') # печать
Проблемы:
- Не различает контекст: заменит
print()внутри строки или имени переменной (my_print). - Не обрабатывает отступы и переносы (например,
print (с пробелом). - Не подходит для сложных конструкций с многострочными аргументами.
Цель: быстрая однократная замена в небольшом файле, где все вызовы однотипны.
Как заменить паттерн с учётом границ слов: например, все вызовы функции foo(a,b) на bar(a,b)?
Вариант 2: Регулярные выражения (re.sub)
Модуль re даёт больше гибкости: можно задать границы слов, игнорировать пробелы, заменять с использованием обратных ссылок. Рекомендуется для замен, где структура вызова предсказуема.
import re
code = """
foo(1,2)
# вызов foo() внутри строки: "foo()"
foo(x, y)
"""
pattern = r'\bfoo\s*\(\s*([^)]+)\s*\)'
replacement = r'bar(\1)'
new_code = re.sub(pattern, replacement, code)
print(new_code)
bar(1,2) # вызов foo() внутри строки: "foo()" bar(x, y)
Проблемы:
- Регулярные выражения не понимают синтаксис Python: можно ошибочно заменить
fooвнутри комментария или строки (хотя в примере выше строка не затронута, так как внутри не было скобок с параметрами). - Сложно обрабатывать вложенные скобки (например,
foo(bar(1), 2)) – регулярное выражение не справится. - Ошибки приводят к молчаливому искажению кода.
Цель: глобальная замена в нескольких файлах, когда структура вызова простая (набор строк, чисел, имён).
Как автоматически преобразовать код из Python 2 в Python 3?
Вариант 3: Инструмент 2to3
Встроенный инструмент 2to3 выполняет множество замен: перевод print в функцию, unicode -> str, dict.iteritems -> dict.items и т.д. Запускается из командной строки. Можно применить к одному файлу или директории рекурсивно.
# В терминале:
2to3 --write --nobackups old_script.py
# или без записи, только просмотр:
2to3 old_script.py
RefactoringTool: Refactored old_script.py
--- old_script.py (original)
+++ old_script.py (refactored)
@@ -1,2 +1,2 @@
-print 'hello'
+print('hello')
Проблемы:
- Не охватывает все возможные случаи (например, особенности работы с кодировками в строках).
- После преобразования требуется ручная проверка и тестирование.
- Может изменить код, который уже совместим с Python 3 (например, двойные звёздочки для словарей).
Цель: быстрая миграция больших проектов с Python 2 на Python 3 с последующей доработкой.
Как привести весь код к единому стилю (форматирование)?
Вариант 4: Инструмент black
Black – «бескомпромиссный форматировщик»: он переписывает код в едином стиле (длина строки 88, кавычки, отступы). Не изменяет логику, только пробелы и переносы.
# Установка:
# pip install black
# Применение к файлу:
black messy.py
Пример до/после:
# до:
x = 1+2
y=[1,2,3]
z = lambda a,b:a+b
# после (вывод black):
x = 1 + 2
y = [1, 2, 3]
z = lambda a, b: a + b
Проблемы:
- Black не делает никаких изменений, кроме форматирования (не переименовывает и не заменяет конструкции).
- Может ухудшить читаемость в некоторых случаях (например, разрывает длинные однострочники).
- Навязывает единый стиль, что может не устраивать команду.
Цель: автоматическая унификация форматирования в проекте, устранение дискуссий о стиле.
Расширенные примеры преобразования кода
Пример 1: AST-трансформация с добавлением тайминга
Задача: обернуть каждое тело функции в замер времени (time.perf_counter), не изменяя остальной код.
import ast
import astor
import io
import contextlib
class AddTiming(ast.NodeTransformer):
def visit_FunctionDef(self, node):
if not node.body:
return node
# Создаём код: start = time.perf_counter(); ... ; end = time.perf_counter(); print(f'... {end-start}')
start_assign = ast.parse('start = time.perf_counter()').body[0]
end_assign = ast.parse('end = time.perf_counter()').body[0]
print_call = ast.parse(f'print(f"Function {node.name} executed in {{end-start}} seconds")').body[0]
new_body = [start_assign] + node.body + [end_assign, print_call]
node.body = new_body
return node
code = """
def slow():
s = 0
for i in range(1000000):
s += i
return s
"""
tree = ast.parse(code)
transformer = AddTiming()
new_tree = transformer.visit(tree)
new_code = astor.to_source(new_tree)
print(new_code)
import time
def slow():
start = time.perf_counter()
s = 0
for i in range(1000000):
s += i
return s
end = time.perf_counter()
print(f"Function slow executed in {end-start} seconds")
Пояснения: Визитор проходит по всем функциям. В начало тела вставляется присваивание start, в конец – end и вызов print. Важно: импорт time не добавлялся, его нужно добавить отдельно. Данный пример демонстрирует вставку кода до и после существующего тела.
Пример 2: Использование tokenize для замены комментариев
Задача: заменить все комментарии, содержащие TODO, на FIXME, не затрагивая строки кода.
import tokenize
import io
import sys
def replace_comments(source: str, old: str, new: str) -> str:
tokens = []
prev_end = (0, 0)
for tok in tokenize.generate_tokens(io.StringIO(source).readline):
start_row, start_col = tok.start
end_row, end_col = tok.end
# Добавляем неизменённый текст между предыдущим и текущим токеном
if (start_row, start_col) > prev_end:
tokens.append(source[prev_end[0]*...?]) # упрощённо
# Пропускаем, логика сложная, для краткости покажем другой подход
# ... (полный код опущен из экономии места)
return ''.join(tokens)
Более практичный пример: использовать tokenize для перевода строковых литералов с f‑строк на старый стиль. Однако для демонстрации покажем замену комментариев с помощью регулярного выражения, но с учётом токенов:
import tokenize
import io
def replace_comments_safe(source):
result = []
for tok in tokenize.generate_tokens(io.StringIO(source).readline):
if tok.type == tokenize.COMMENT:
# Заменяем TODO на FIXME внутри комментария
new_str = tok.string.replace('TODO', 'FIXME')
result.append(new_str)
else:
result.append(tok.string)
return ''.join(result)
code = """
x = 1 # TODO: оптимизировать
# TODO: добавить проверку
y = "TODO" # строка не должна быть изменена
"""
print(replace_comments_safe(code))
x = 1 # FIXME: оптимизировать # FIXME: добавить проверку y = "TODO" # строка не должна быть изменена
Пояснения: tokenize разбивает исходник на токены, каждый имеет тип (COMMENT, STRING, NAME и т.д.). Мы модифицируем только комментарии, строковые литералы остаются нетронутыми. Это безопаснее регулярных выражений.
Пример 3: Создание собственного фиксера для lib2to3
Модуль lib2to3 позволяет писать собственные правила преобразования. Создадим фиксер, который заменяет os.system(cmd) на subprocess.run(cmd, shell=True).
from lib2to3.fixer_base import BaseFix
from lib2to3.fixer_util import Name, ArgList, Comma
class FixOsSystem(BaseFix):
BM_compatible = True
PATTERN = """
power<
'os' trailer< '.' 'system' >
trailer< '(' arg=any ')' >
>
"""
def transform(self, node, results):
arg = results['arg']
# Создаём новый узел: subprocess.run(arg, shell=True)
subprocess_run = Name('subprocess')
dot = Name('.')
run = Name('run')
shell_keyword = Keyword('shell', 'True')
args = [arg, Comma(), shell_keyword]
new_call = Call(run, args)
# Собираем как power
return Power(subprocess_run, dot, run, trailer_parentheses=Par(new_call))
Для применения фиксера его нужно поместить в директорию с fixers и запустить 2to3 -f fix_os_system script.py. Пример результата:
# До:
os.system('ls -la')
# После:
subprocess.run('ls -la', shell=True)
Пояснения: lib2to3 использует шаблоны на основе собственного языка. Фиксер находит узлы, соответствующие шаблону, и заменяет их на новые. Это мощный способ создания кастомных трансформаций для миграции или рефакторинга.
Пример 4: AST для вычисления цикломатической сложности и упрощения
Задача: найти функции с цикломатической сложностью >10 и предложить упрощение (например, разбить на подфункции). Здесь ограничимся вычислением сложности.
import ast
import sys
class ComplexityVisitor(ast.NodeVisitor):
def __init__(self):
self.complexity = 1
def visit_If(self, node):
self.complexity += 1
self.generic_visit(node)
def visit_While(self, node):
self.complexity += 1
self.generic_visit(node)
def visit_For(self, node):
self.complexity += 1
self.generic_visit(node)
def visit_And(self, node):
self.complexity += 1
self.generic_visit(node)
def visit_Or(self, node):
self.complexity += 1
self.generic_visit(node)
code = """
def complicated(x):
if x:
for i in range(10):
if i > 5:
break
elif x > 10:
while True:
pass
return 0
"""
tree = ast.parse(code)
visitor = ComplexityVisitor()
visitor.visit(tree)
print(f"Цикломатическая сложность: {visitor.complexity}")
Цикломатическая сложность: 7
Пояснения: Обход AST позволяет анализировать код без выполнения. В данном случае сложность считается по базовому алгоритму. Можно расширить до автоматического рефакторинга: если сложность >10, добавить комментарий или отправить на ревью.