
В любой компании рано или поздно появляется легаси-код. Иногда это устаревшие скрипты, иногда большие модули, которые «пишет только один человек», а чаще — просто код, который давно никто не трогал, но все боятся менять. И пусть кажется, что если система работает, то и трогать ее не нужно, на практике все иначе: любая остановка развития или усложнение поддержки со временем превращается в дополнительные издержки и головную боль для бизнеса.
Рефакторинг помогает «оздоровить» такой код, сделать его чище и удобнее для развития. Но стандартный ручной рефакторинг — задача дорогая и трудоемкая. В последнее время на волне развития искусственного интеллекта появились инструменты, которые обещают упростить этот процесс. Мы решили провести эксперимент и проверить, способны ли нейросети действительно помочь с рефакторингом легаси-кода на Python, и если да, то в каких случаях.

Selectel Tech Day — 8 октября
Разберем реальный опыт IT-команд, технический бэкстейдж и ML без спецэффектов. 15 стендов и интерактивных зон, доклады, мастер-классы, вечерняя программа и нетворкинг. Участие бесплатное: нужна только регистрация.
Используйте навигацию, если не хотите читать текст целиком:
Откуда берется Python-легаси и чем он опасен
Python за последние годы стал языком «по умолчанию». Его используют для прототипирования, автоматизации и разработки сложных сервисов. Среди причин популярности — низкий порог входа и огромное количество библиотек, благодаря которым запуск новых проектов идет быстро. Однако по мере развития продукта, смены требований и изменений в команде отдельные участки кода могут со временем устаревать или терять поддержку. Такие фрагменты принято называть легаси-кодом: они могут мешать внедрению новых функций, усложнять тестирование и сопровождение.
Даже в крупных успешных Python-проектах иногда встречаются подобные примеры. Рассмотрим несколько пунктов, которые помогут их распознать.
Длинные функции и модули. Сегодня добавили «еще одну проверку», завтра — кусок логики, и вот уже в одной функции 100+ строк. Такие «монолиты» сложно тестировать и переиспользовать, их боятся трогать даже опытные коллеги.
Ручной парсинг данных и конфигов. Казалось бы, проще прочитать пару строк из файла и разбить их через
.split('=')
, чем подключать отдельную библиотеку для работы с настройками. Но это быстро приводит к ошибкам, дублированию кода и трудностям при масштабировании.Повторяющиеся фрагменты. Вспомнили полезную функцию — скопировали в другой файл, чуть изменили. Теперь однотипные баги разбросаны по всему проекту.
Слабая обработка ошибок. Исключения ловятся «на глаз», сообщения об ошибках неинформативны, логи заполняются неструктурированными данными.
«Магические» числа и строки в коде. «Порог ошибки — 80, давление — 30, напряжение — 5» — такие значения часто зашиваются прямо в функции. Потом никто не помнит, почему они выбраны, а изменить их без страха сложно.
Для инфраструктурных и серверных проектов эти проблемы еще острее: автоматизация деплоя, мониторинг и интеграция с внешними сервисами требуют, чтобы код был не только рабочим, но и поддерживаемым, читаемым и гибким к изменениям. Именно поэтому рефакторинг — не каприз разработчиков, а важная инвестиция в устойчивость и скорость проекта.
Наш подопытный. Код анализа данных с космического телескопа «Джеймс Уэбб»
Чтобы проверить возможности автоматического рефакторинга на практике, мы решили взять не абстрактный пример из учебника, а рабочее решение с открытым исходным кодом. Для эксперимента выбрали репозиторий spacetelescope/jwql — проект, разработанный для автоматизации мониторинга, сбора и анализа данных с телескопа «Джеймс Уэбб».
Почему этот проект?
Это живой Python‑код, используемый в инфраструктуре реального научного инструмента.
Проект существует давно, активно развивается и содержит в себе как современные решения, так и классические «боли» крупных Python‑баз: большие функции, ручной парсинг, нестандартизированную обработку ошибок.
Лицензия BSD‑3 позволяет свободно использовать код для анализа и демонстрации.
Что мы ищем?
В этом репозитории мы специально отобрали фрагменты, которые чаще всего встречаются в легаси‑коде инфраструктурных сервисов.
Функции с длинной логикой без деления на подзадачи.
Обработка конфигов «на коленке» через открытие и разбор файла.
Ручное форматирование строк для отчетов.
Однотипные проверки и условия, разбросанные по проекту.
Явные «магические» значения прямо в коде.
Автоматический рефакторинг с помощью AI: алгоритм
Автоматический рефакторинг с помощью нейросетей пока нельзя назвать универсальным решением, но тема активно обсуждается и вызывает интерес. Мы решили на практике проверить, каким может быть процесс улучшения кода с использованием искусственного интеллекта и что из этого получится, на примере ChatGPT.
1. Выбор фрагмента кода. Из всей кодовой базы выбираем функцию или участок, который кажется «узким местом»: слишком длинный, повторяющийся, с ручными операциями или дублирующимися данными. Это мы уже сделали.
2. Подготовка промта. Для получения качественного результата важно корректно сформулировать запрос к AI. Мы используем универсальную инструкцию, которую легко адаптировать под любой Python-код:
Ты опытный Python-разработчик и эксперт по рефакторингу. Твоя задача — улучшить предоставленный код так, чтобы он стал чище, современнее и удобнее для поддержки. Придерживайся лучших практик Python, ориентируйся на стандарты PEP8. Постарайся:
— Упростить и структурировать код.
— Избавиться от дублирования и «магических» чисел.
— Сделать код более читаемым и понятным для других разработчиков.
— Улучшить обработку ошибок, добавить необходимые проверки.
— По возможности применить современные паттерны и стандартные библиотеки Python.
— Обязательно сохраняй интерфейс функций: не изменяй список параметров, возвращаемые значения и поведение при ошибочных/граничных ситуациях, если иное не согласовано отдельно.
— Избегай изменений, затрагивающих бизнес-логику, если она не содержит явных ошибок.
— При необходимости добавь краткие комментарии, поясняющие внесенные улучшения.
В ответе:
Сначала приведи полный переписанный вариант кода.
Далее кратко и по пунктам опиши, что именно и зачем было улучшено.
3. Отправка кода и промта в ChatGPT. Вставляем выбранный фрагмент вместе с промтом в окно чата. Можно использовать как веб-интерфейс, так и интеграции с API.
4. Получение и разбор ответа. AI предложит переписанный код и кратко объяснит внесенные улучшения. Важно внимательно изучить ответ — особенно логику обработки ошибок и работу с данными.
5. Проверка и тестирование. Обязательно прогоняем новые функции через тесты (или хотя бы руками проверяем в dev-среде), чтобы убедиться: бизнес-логика не изменилась, а ошибки и предупреждения действительно стали обрабатываться лучше.
6. Интеграция в проект. Если результат устроил — добавляем переписанный код в проект, фиксируем изменения через pull request, описываем плюсы рефакторинга для команды.
Примеры легаси-кода и его преображение
В качестве примеров для эксперимента выбрали функции log_info
из файла logging_functions.py
и amplifier_info
из файла instrument_properties.py
. Они показались яркими представителями легаси-кода по ряду причин:
выполняют слишком много разных задач сразу;
сильно зависят от окружения и внешних утилит;
содержат скрытые побочные эффекты, сложны для тестирования;
имеют много «магических» чисел.
Посмотрим, удалось ли с помощью рефакторинга и инструментов AI сделать этот участок кода более чистым и удобным для поддержки.
Работа с путями и зависимостями
До рефакторинга: используется os.path
, нет обработки ошибок, имена файлов жестко сшиты.
toml_file = os.path.join(os.path.dirname(get_config()['setup_file']), 'pyproject.toml')
with open(toml_file, "rb") as f:
data = tomllib.load(f)
required_modules = data['project']['dependencies']
После рефакторинга: используется pathlib, вся логика вынесена в отдельную функцию, добавлена обработка ошибок.
setup_file = Path(get_config()["setup_file"])
pyproject = setup_file.parent / "pyproject.toml"
required_modules = _read_required_modules(pyproject)
Вывод: код стал компактнее и удобнее для поддержки, однако после рефакторинга предполагается, что setup_file в конфиге всегда указывает на файл, а не на директорию. В легаси-проектах это не всегда так, поэтому такой подход может потребовать дополнительной проверки конфигурации или доработки логики для поддержки разных вариантов.
Очистка и обработка списка зависимостей
До рефакторинга: длинная цепочка манипуляций со строкой, неочевидно, какие случаи она покрывает.
module_list = [item.strip().replace("'", "").replace(",", "")
.split("=")[0].split(">")[0].split("<")[0] for item in required_modules]
После рефакторинга: вся логика вынесена, появились комментарии, стало понятно, что и зачем делается.
def _read_required_modules(pyproject_path: Path) -> Iterable[str]:
"""Extract top-level package names from ``project.dependencies`` in pyproject.toml."""
try:
with pyproject_path.open("rb") as f:
data = tomllib.load(f)
deps = data.get("project", {}).get("dependencies", []) or []
except Exception as exc: # log later in decorator
logging.exception("Failed to read dependencies from %s", pyproject_path)
deps = []
# Normalize entries like "package>=1.0,<2.0" -> "package"
cleaned = []
for item in deps:
name = (
item.strip()
.split(";", 1)[0]
.split("[", 1)[0]
.split(">=", 1)[0]
.split("==", 1)[0]
.split("<=", 1)[0]
.split("<", 1)[0]
.split(">", 1)[0]
.split("~=", 1)[0]
.strip()
)
if name:
cleaned.append(name)
return cleaned
Вывод: теперь модуль обработки зависимостей можно использовать и тестировать отдельно, он стал прозрачнее и надежнее.
Логирование информации о модулях
До рефакторинга: логика размазана, используются строковые конкатенации, ошибки сливаются в одну кучу.
for module in module_list:
try:
mod = importlib.import_module(module)
logging.info(module + ' Version: ' + importlib.metadata.version(module))
logging.info(module + ' Path: ' + mod.__path__[0])
except (ImportError, AttributeError) as err:
logging.warning(err)
После рефакторинга: выделен отдельный чистый блок для логирования, улучшено форматирование логов, расширена обработка ошибок.
def _log_module_info(modules: Iterable[str]) -> None:
for module in modules:
try:
imported = importlib.import_module(module)
version = importlib.metadata.version(module)
path = getattr(imported, "__path__", [getattr(imported, "__file__", "N/A")])[0]
logging.info("%s Version: %s", module, version)
logging.info("%s Path: %s", module, path)
except Exception as err:
logging.warning("Could not import/log %s: %s", module, err)
Вывод: код стал более структурированным, логика вынесена в отдельную функцию, а обработка ошибок стала прозрачнее. Однако использование сложных цепочек с getattr
и работа с путями в «магическом» стиле сохраняет риск появления неочевидных багов и затрудняет поддержку.
Запуск внешних команд (conda env export)
До рефакторинга: бизнес-логика и работа с окружением спрятаны внутри основного тела функции.
try:
environment = subprocess.check_output('conda env export', universal_newlines=True, shell=True) # nosec
logging.info('Environment:')
for line in environment.split('\n'):
logging.info(line)
except Exception as err:
logging.exception(err)
После рефакторинга: работа с окружением вынесена в отдельную функцию, обработка ошибок стала информативнее.
def _log_environment() -> None:
try:
env_txt = subprocess.check_output(
"conda env export", universal_newlines=True, shell=True,
)
logging.info("Environment:")
for line in env_txt.splitlines():
logging.info(line)
except Exception as err:
logging.exception("Failed to export conda environment: %s", err)
Вывод: сбои при экспорте окружения не мешают основной логике, поведение теперь легче тестировать отдельно.
Логирование времени выполнения
До рефакторинга: много однотипных операций в теле основной функции, повторение логики для разных метрик.
t1_cpu = time.perf_counter()
t1_time = time.time()
func(*args, **kwargs)
t2_cpu = time.perf_counter()
t2_time = time.time()
hours_cpu, remainder_cpu = divmod(t2_cpu - t1_cpu, 60 * 60)
minutes_cpu, seconds_cpu = divmod(remainder_cpu, 60)
hours_time, remainder_time = divmod(t2_time - t1_time, 60 * 60)
minutes_time, seconds_time = divmod(remainder_time, 60)
logging.info('Elapsed Real Time: {}:{}:{}'.format(int(hours_time), int(minutes_time), int(seconds_time)))
logging.info('Elapsed CPU Time: {}:{}:{}'.format(int(hours_cpu), int(minutes_cpu), int(seconds_cpu)))
После рефакторинга: логика форматирования вынесена в отдельную функцию, стало короче и яснее.
def _format_duration(seconds: float) -> str:
hours, remainder = divmod(int(seconds), 3600)
minutes, secs = divmod(remainder, 60)
return f"{hours:02d}:{minutes:02d}:{secs:02d}"
# Внутри log_info:
logger.info("Elapsed Real Time: %s", _format_duration(t_wall_end - t_wall_start))
logger.info("Elapsed CPU Time: %s", _format_duration(t_cpu_end - t_cpu_start))
t_cpu_start = time.perf_counter()
t_wall_start = time.time()
func(*args, **kwargs)
t_cpu_end = time.perf_counter()
t_wall_end = time.time()
Вывод: появилась новая абстракция, которая делает основной код компактнее и проще для поддержки. Но подобное изменение меняет внутреннюю структуру функции, поэтому при рефакторинге важно убедиться, что внешний контракт и ожидаемое поведение остаются неизменными.
Работа с «магическими» числами
До рефакторинга: 2048 — фиксированный размер кадра для всех инструментов, чистое «магическое» число.
def amplifier_info(filename, omit_reference_pixels=True):
…
if instrument.lower() == 'miri' or ((x_dim == 2048) and (y_dim == 2048)) or \
subarray_name in FOUR_AMP_SUBARRAYS:
num_amps = 4
amp_bounds = deepcopy(AMPLIFIER_BOUNDARIES[instrument])
else:
if subarray_name not in NIRCAM_SUBARRAYS_ONE_OR_FOUR_AMPS:
num_amps = 1
amp_bounds = {'1': [(0, x_dim, 1), (0, y_dim, 1)]}
После рефакторинга: «магическое» число вынесено в отдельную константу FULLFRAME_SIZE
.
_FULL_FRAME_SIZE = 2048
def amplifier_info(filename: str, omit_reference_pixels: bool = True):
…
if (
instrument == "miri"
or (x_dim == _FULL_FRAME_SIZE and y_dim == _FULL_FRAME_SIZE)
or subarray_name in FOUR_AMP_SUBARRAYS
):
# Full-frame (or known 4-amp subarrays) → always 4 amps
num_amps = 4
amp_bounds = deepcopy(AMPLIFIER_BOUNDARIES[instrument])
Вывод: нейросеть правильно идентифицировала «магическое» число и вынесла его в отдельную константу, что благоприятно отразится на дальнейшей поддержке кода.
В результате рефакторинга функции действительно стали чище и понятнее: обязанности разделены, вспомогательные задачи вынесены в отдельные блоки, логирование и обработка ошибок стали более прозрачными. Однако даже при самом аккуратном рефакторинге важно тщательно проверять результат: запускать тесты, анализировать граничные случаи и следить за тем, чтобы не поменялось поведение, от которого зависит остальной код.
Полный результат автоматического рефакторинга этих функций →
Итоги эксперимента
Вернемся к основным признакам легаси-кода, которые мы назвали вначале, и посмотрим, как AI-рефакторинг справился с каждым из них.
Длинные функции и модули — справился. ChatGPT корректно разбивает монолиты, выносит вспомогательные функции, код становится проще для поддержки.
Ручной парсинг данных и конфигов — не удалось. В большинстве случаев AI оставляет ручную обработку конфигов, если в оригинале не использованы сторонние библиотеки (например, configparser). Полностью автоматизировать или стандартизировать этот кусок без контекста AI не смог.
Повторяющиеся фрагменты — справился. ChatGPT умеет определять повторяющиеся куски кода и предлагает выносить их в отдельные функции или методы.
Слабая обработка ошибок — справился. После рефакторинга обработка ошибок становится более структурированной и информативной, добавляются try/except, логи становятся понятнее.
«Магические» числа и строки — справился. AI умеет определять зашитые в код «непонятные» значения и выносить их в отдельные переменные и константы.
AI‑инструменты уже умеют брать на себя рутинную часть рефакторинга: дробить длинные функции, устранять дублирование, вычищать «магические» числа. Это экономит часы (а то и дни) разработчиков и ускоряет вывод новых фич. Но важно помнить: нейросеть — это лишь усилитель, а не замена профессионала. Финальное слово всегда остается за командой, которая знает бизнес‑логику и несет ответственность за код.
Как начать безопасно
Экспериментируйте точечно. Выберите пару некритичных модулей, прогоните их через наш промт, посмотрите diff, запустите тесты.
Автоматизируйте окружение. Для контейнерной разработки подключите Managed Kubernetes или Container Registry, чтобы быстро разворачивать тестовые стенды и катить обновленные сервисы.
Делитесь знаниями. Поддерживайте внутреннюю «академию»: короткие гайды, как правильно формировать промты, на какие метрики смотреть до/после, какие паттерны чаще всего предлагает AI.
⚠️ Не рекомендуем полностью писать или рефакторить код через нейросети, если у вас нет опыта разработки. ChatGPT и аналоги — это помощники, способные разгрузить рутину, но не более.
positroid
Так может не в ИИ проблема? Реквестирую ту же проверку на базе IDE с доступом к коду (Cursor / Windsurf / etc), чтобы получить недостающий контекст.
А в целом по сабжу - это достаточно рутинные задачи, которые, тем не менее требуют хорошего покрытия тестами. Впрочем, это покрытие может обеспечить тот же ИИ перед началом рефакторинга.