Преобразование 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, добавить комментарий или отправить на ревью.

Преобразование кода Python - comments

En
преобразовать код python (python)