Бесполезный Репл. Отчет Яндекса

REPL (цикл чтения-оценки-печати) бесполезен в Python, даже если это волшебный IPython. Сегодня я предложу одно из возможных решений этой проблемы.

В первую очередь отчет и мое расширение TheREPL будут полезны тем, кто заинтересован в более быстрой и эффективной разработке, а также тем, кто пишет системы с состоянием.

— Меня зовут Александр, я работаю программистом в Яндексе.

В моей команде мы пишем на Python; мы еще не перешли на Go. Но в свободное от работы время, как ни странно, я еще и программирую и делаю это на очень динамичном языке — Common Lisp. Возможно, он даже более динамичен, чем Python. Его особенность заключается в том, что сам процесс разработки устроен несколько иначе.

Он более интерактивный и итеративный, поскольку в Lisp REPL вы можете делать все: создавать новые и удалять старые модули, добавлять и удалять методы, классы, переопределять классы и т. д.

Бесполезный РЕПЛ.
</p><p>
 отчет яндекса

В Python это сложнее.

Есть IPython. Конечно, IPython в чем-то улучшает REPL, добавляет автодополнение и позволяет использовать разные расширения.

Но для итеративной разработки он не очень подходит. Там можно скачать код, немного его протестировать и всё.

А иногда от него хочется большей интерактивности, чтобы реально использовать этот REPL в разработке, переключаться между модулями, менять внутри них функции и классы.

У меня такое бывает — запускаешь, например, IPython REPL в продакшене и начинаешь там запускать какие-то команды, что-то исследовать, а потом обнаруживаешь, что в модуле ошибка, и ты хочешь ее побыстрее исправить.

Но сделать это не возможно, потому что нужно собрать новый Docker-образ, выкатить его на продакшн, снова зайти в этот REPL, снова добиться там нужного состояния и снова запустить ту штуку, где все упало.

А в идеале мне бы пришлось подправить функцию, сразу запустить ее и мгновенно получить результат. Что вы можете с этим поделать? Как перезагрузить код в IPython? Я попробовал использовать автозагрузку, и мне это не понравилось по нескольким причинам.

Во-первых, при перезагрузке модуля он теряет то состояние, которое было в глобальных переменных внутри этого модуля.

И может быть кэшированное значение с результатами некоторых функций.

Или я мог бы, например, загружать туда данные по сети, чтобы потом быстрее с ними работать.

То есть автоперезагрузка теряет свое состояние.

Поэтому в качестве эксперимента я сделал собственное простое расширение для IPython и назвал его TheREPL. Я пришел к вам с этим докладом как идея того, что можно сделать с помощью REPL на Python. И я очень надеюсь, что вам понравится эта идея, вы будете носить ее в своих головах и продолжите придумывать вещи, которые сделают Python еще более эффективным и удобным.

Что такое РЕПЛ? Это расширение, которое вы загружаете, после чего в IPython появляется понятие пространства имен, и вы можете переключиться на любой модуль Python, посмотреть, какие там переменные, функции и так далее.

И что еще важно, вы можете напрямую написать def, имя функции, переопределить функцию или класс, и оно изменится во всех модулях, в которые оно было импортировано.

Но при этом сам модуль не перезагружается, поэтому состояние сохраняется.

Кроме того, TheREPL позволяет избежать некоторых других артефактов, находящихся в автоперезагрузке и которые мы сейчас рассмотрим.



Бесполезный РЕПЛ.
</p><p>
 отчет яндекса

Итак, при автоперезагрузке обновление кода происходит только при сохранении файла.

Но при этом нужно что-то ввести в сам REPL, и только тогда автоперезагрузка подхватит эти изменения.

Это проблема №1. То есть, если у вас в отдельном потоке идет какой-то фоновый процесс (например, сервер запущен), вы не можете просто пойти и поправить код. Автоматическая перезагрузка не применит эти изменения, пока вы не введете что-либо в REPL IPython. В случае с моим расширением вы нажимаете ярлык прямо в редакторе, и функция, которая находится под курсором, сразу же применяется и начинает работать.

То есть с помощью TheREPL вы можете менять код более детально.

Кроме того, вы также можете написать def в IPython.

Бесполезный РЕПЛ.
</p><p>
 отчет яндекса

Переключение между модулями, как я уже говорил, автоперезагрузка не поддерживает никак.

Вам остаётся только найти файл в файловой системе, изменить его и надеяться, что автозагрузка там всё разберёт.

Бесполезный РЕПЛ.
</p><p>
 отчет яндекса

Дальше.

Автоперезагрузка теряет глобальные переменные, TheREPL сохраняет и позволяет вам продолжать исследовать, как работает ваше приложение, изменять его внутренний код и, таким образом, быстро его разрабатывать.



Бесполезный РЕПЛ.
</p><p>
 отчет яндекса

автоперезагрузка также имеет эту функцию.

Он очень ловко применяет изменения к перезагружаемому модулю.

В частности, он там проделывает очень интересный трюк.

Если функция в этом модуле была обновлена, то чтобы изменить ее везде, где она была импортирована, он использует сборщик мусора, чтобы найти ее и все эти экземпляры функции и изменить код внутри них.

Далее мы рассмотрим примеры того, как это происходит. Благодаря этому код функции меняется, даже если он включен в замыкание.

Знаете ли вы, что такое закрытие? Это очень полезная вещь.

Разработчики JavaScript используют это постоянно.

Вы, скорее всего, тоже, просто не обращали внимания.

Но поскольку автоперезагрузка делает то, что я описал выше, вы можете оказаться в ситуации, когда старый код использует новый код, который может работать по-другому.

Например, функция может возвращать не одно значение, а два, кортеж вместо строки и т. д. Старый код на этом этапе сломается.

REPL не делает таких умных вещей специально, чтобы сделать все более последовательным.

То есть он изменяет функцию или класс в модуле, в котором он определен.

Находит этот класс во всех остальных модулях и меняет его там же.

После этого все работает по-новому.



Бесполезный РЕПЛ.
</p><p>
 отчет яндекса

Как работает функция замены автоперезагрузки? У нас есть две функции, одна и две.

Каждая функция имеет набор атрибутов: документация, код, аргументы и т.д. Здесь на слайде пример замены атрибутов, в которых хранится байт-код. После того как автоперезагрузка меняет его, вызываемая функция начинает работать по-другому.

Но это синтетический пример, который я просто воспроизвел от руки, чтобы вы понимали, что происходит. Функция вызывается так, но код там на самом деле другой.

А если разобрать, то еще можно увидеть, что он возвращает двойку.

К чему это приводит?

Бесполезный РЕПЛ.
</p><p>
 отчет яндекса

Вот пример закрытия.

Во второй строке мы создаем замыкание, которое захватывает функцию foo. Само замыкание ожидает, что эта функция, которую мы передали, вернет строку, она кодирует ее в utf-8 и все работает.

Бесполезный РЕПЛ.
</p><p>
 отчет яндекса

Но предположим, что вы меняете модуль, в котором определен foo, и автоматическая загрузка улавливает это изменение.

И вы измените его так, чтобы он возвращал не строку, а число.

Тогда замыкание перестанет работать корректно, потому что функция в нём изменилась внутренне, но замыкание этого не ожидает, оно не изменилось.

И такие проблемы с автоперезагрузкой могут появиться в самых неожиданных местах.



Бесполезный РЕПЛ.
</p><p>
 отчет яндекса

Как автоматически обновляются классы? Очень просто.

Он обновляет все методы класса так же, как и функции, а также обновляет атрибут __class__ всех экземпляров, чтобы разрешение метода (определяющее, какой метод необходимо вызвать) начало работать по-новому.

В TheREPL все немного сложнее, потому что при обновлении _класса_ может оказаться, что у него есть какие-то потомки, дочерние классы, которые тоже необходимо обновить, потому что в списке базовых классов что-то изменилось.

Чтобы решить эту проблему, вы можете перестроить класс.

Но давайте сначала посмотрим, что происходит с автоперезагрузкой при перезагрузке модуля.



Бесполезный РЕПЛ.
</p><p>
 отчет яндекса

Вот хороший пример.

Есть два модуля - а и б.

Модуль a определяет родительский класс, модуль b определяет дочерний класс, и мы создаем экземпляр дочернего класса.

И в строке 10 вы можете видеть, что да, это экземпляр родительского класса Foo.

Бесполезный РЕПЛ.
</p><p>
 отчет яндекса

Тогда мы просто берем и меняем модуль a. Например, давайте добавим документацию к классу Foo. Затем автоперезагрузка фиксирует эти изменения.

Как вы думаете, что он в этом случае вернется из Бара?

Бесполезный РЕПЛ.
</p><p>
 отчет яндекса

А возвращает false, потому что автоперезагрузка изменила класс Foo, и теперь это совсем другой класс, не тот, от которого унаследован класс Bar.

Бесполезный РЕПЛ.
</p><p>
 отчет яндекса

И сюрприз! В двух модулях a и b класс Foo является другим классом, а Bar наследуется от одного из них.

Из-за таких косяков очень сложно предсказать, как будет работать ваш код после того, как автоперезагрузка что-то в нем исправит.

Бесполезный РЕПЛ.
</p><p>
 отчет яндекса

Примерно так он обновляет классы.

Прокомментирую картинку.

Первоначально класс Foo импортируется в модуль b и остается там.

При замене автоперезагрузки этот модуль а перезагружается, и там появляется новый класс, но в модуле б он не обновляется.



Бесполезный РЕПЛ.
</p><p>
 отчет яндекса

REPL делает все немного по-другому.

Он внедряет измененный класс в каждый модуль, куда он был импортирован.

Поэтому там все работает корректно.

При этом если в классе были объекты, они сохранятся.



Бесполезный РЕПЛ.
</p><p>
 отчет яндекса

И вот как TheREPL решает проблему с дочерними классами.

То есть, когда родительский класс изменился, он определяет список базовых классов через магический атрибут mro (порядок разрешения метода).

Этот атрибут содержит список классов в том порядке, в котором вы хотите искать в них методы или атрибуты.

И каждый раз, когда вы вызываете, например, метод get_name на своем объекте, Python сначала проверит его в классе Bar, затем в классе Foo, затем в классе объекта, если не найдет. Он работает в соответствии с порядком разрешения метода.

REPL использует эту функцию.

Берёт список базовых классов, меняет там класс, который вы только что поменяли, на новый.

Создает новый дочерний тип, это второй шаг.

С помощью функции типа вы действительно можете создавать классы.

Если вы никогда им не пользовались, попробуйте, это весело.

Вы просто говорите имя класса, говорите, какой у него базовый класс.

В простейшем случае, например, объект. И — словарь с методами и атрибутами класса.

Вот и все, у вас есть новый класс, экземпляр которого можно создать как обычно.

REPL использует эту функцию.

Он генерирует дочерний класс и меняет указатели на него во всех объектах старого класса Bar. У меня еще есть подготовленная демо-версия, давайте посмотрим, как она работает. Для начала давайте посмотрим на такую простую вещь.

Первая демонстрация Я сказал, что вы можете изменить код внутри модуля.

Допустим, у нас есть сервер.

Я сейчас запущу.

В какой-то момент мы обнаруживаем, что он по какой-то причине создает временные каталоги.

Или начал творить, но раньше не творил.

Затем мы можем подключиться к этому серверу и, догадавшись, что он, вероятно, создает эти каталоги с помощью функции mkdtemp из файлового модуля, мы можем перейти прямо в этот модуль Python. Посмотрите - в углу изменилось название текущего модуля.

Теперь там написано tempfile. И я вижу, какие там функции.

Мы видим их и, что немаловажно, можем дать им новое определение.

Я подготовил специальную обертку, позволяющую оформить любую функцию так, чтобы для всех ее вызовов был виден след, откуда она вызывается.

Теперь мы их импортируем и применим.

То есть я обертываю стандартную функцию Python, даже не имея доступа к исходному коду этого модуля.

Могу взять и завернуть.

И в следующем выводе мы увидим Traceback и узнаем, откуда он вызывается.

Таким же образом вы можете откатить эти изменения, чтобы она нам не спамила.

То есть мы видим, что именно сервер внутри воркера на восьмой строке вызывает mkdtemp и продолжает выдавать нам временные каталоги, захламляя файловую систему.

Это одно приложение.

Давайте рассмотрим еще один пример того, почему автоперезагрузка иногда работает не очень хорошо.

Я подготовил телеграм-бота: Второе демо Теперь активируем автоперезагрузку и посмотрим, как она нам поможет. Всё, теперь вы можете запустить бота и пообщаться с ним.

Чтобы вам было лучше видно, мы начнем с ним диалог.

Давайте познакомимся с ботом.

Так.

Здесь какая-то ошибка.

Я имел в виду совершенно другую ошибку, и я решил внести изменения в последнюю минуту.

Но не важно.

Сейчас мы это исправим, в этом нам поможет автоперезагрузка.

Переключаемся на бут. И это я временно прокомментирую, если это так.

Я сохраняю файл.

автоперезагрузка, по идее, должна была уловить эти изменения.

Давайте снова запустим бота.

Бот узнал меня.

Давайте поговорим с ним.

Еще одна ошибка.

Это уже так задумано.

Пошли исправлять.

Я оставлю бота, он будет работать в фоне, переключусь на редактор, и мы найдем в редакторе эту ошибку.

Здесь просто опечатка, и я забыл, что моя переменная называется user_name. Я сохранил файл.

автоперезагрузка должна была его подхватить, и мы сейчас это увидим.

Но автоперезагрузка, как я уже говорил, ничего не знает о том, что файл изменился, пока вы в него что-нибудь не введете.

При таком долгом процессе.

Его нужно прерывать и начинать заново.

Готовый.

Возвращаемся к нашему боту и пишем ему.

Видите ли, бот забыл, что меня зовут Саша.

Почему? autoreload пересоздал его заново, поскольку полностью перезагружает весь модуль.

И мне нужно снова написать боту, восстановить его состояние.

И если вы отлаживаете какую-то ошибку, возникающую при определенном состоянии, то вы не можете потерять это состояние, потому что в противном случае вы снова потратите много времени на достижение этого состояния.

В таких случаях на помощь приходит REPL. Посмотрим, как будет обновляться бот при использовании TheREPL. Для чистоты эксперимента я перезапущу IPython, и мы повторим все сначала.

И теперь я загружаю TheREPL. Он сразу же начинает прослушивать определенный порт, чтобы отправлять внутрь него код. Кстати, это можно сделать, даже если у вас где-то на сервере работает IPython, а редактор работает локально, что тоже может вас выручить в некоторых случаях.

Импортируем бота, запускаем, пишем заново.

Тут все понятно — мы перезапустили Python, поэтому он не помнит, кто я.

Давайте проверим, нет ли внутри ошибки.

Да, есть ошибка.

Что ж, давайте сделаем это.

Переключаюсь обратно в редактор и исправляю ошибку.

Нам даже не нужно сохранять файл, я нажимаю Ctrl-C, Ctrl-C, это ярлык, с помощью которого Emacs берет прямо под курсором текущее описание функции, которая у меня сейчас есть, и отправляет его процессу Python к которому он подключен.

Всё, теперь можно пройтись и проверить, как наш бот реагирует на мои сообщения.

Теперь он вспоминает, что я Саша, и честно отвечает, что ничего не умеет. Попробуем добавить новый функционал напрямую туда.

Для этого вернемся в редактор.

Например, давайте добавим команду help. А пока пусть ответит, что о помощи ничего не знает. Опять жму Ctrl-C, Ctrl-C, код применится.

Пойдем к лодке.

Посмотрим, поймет ли он эту команду.

Да, команда была применена.

Кстати, у него тоже есть такая штука, сейчас посмотрим, как изменится класс.

У него есть команда состояния, специальная команда отладки для просмотра состояния бота.

Тут какой-то Олег подключился.

Интересный.

Когда бот выполняет эту команду, он вызывает ответ, чтобы увидеть представление бота.

Мы можем пойти и исправить, например, этот ответ на что-то другое.

Например, убедитесь, что туда просто вписываются имена.

Вы можете сделать это.

Давайте вернемся к нашему мессенджеру и снова выполним состояние.

Вот и все.

Теперь ответ работает по-новому, но объект всё тот же, его состояние сохранилось, так как он помнит обо всех нас — об Олеге, Саше, Кеке и «DROP TABLE Users, Alex»! Таким образом, вы можете писать и отлаживать код на лету, не переключаясь на этот цикл, когда вам нужно собрать пакет и где-то его выкатить.

Вы можете быстро что-то протестировать, изменить все, что вам нужно, и только потом все эти изменения правильно упаковать и развернуть.

Естественно, в реальном продакшене этого делать не стоит, потому что при таком подходе могут возникнуть проблемы.

Вы можете забыть, что код, который вы только что запустили на сервере, необходимо позже сохранить и правильно развернуть.

Такой подход требует дисциплины.

Но в процессе разработки и отладки для некоторого тестирования это просто отличная вещь.

Вам обязательно нужно сделать плагин для PyCharm. Если найдется волонтер, который сможет помочь мне с Kotlin и плагином для PyCharm, буду рад поговорить.

Напиши мне по почте или в телеграмме .

* * * Соединять для развития TheREPL. Можно придумать еще много трюков.

Например, вы можете придумать способ обновлять экземпляры классов при их обновлении, добавлять туда новые атрибуты или обновлять их состояние каким-либо другим способом.

Таким же образом мы обновим базы данных.

Сейчас это не так.

Можно придумать код горячей перезагрузки для продакшена, чтобы при поступлении новых изменений не пришлось перезагружать сервера.

Вы можете придумать гораздо больше.

Это всего лишь идея, и я хочу, чтобы вы забрали ее отсюда.

Вам предстоит все настроить под себя и сделать удобным.

Это все, что у меня есть.

Теги: #python #github #Тестирование веб-сервисов #REPL #ipython

Вместе с данным постом часто просматривают: