Эссе о разработке игр, мышлении и книгах

Типы в Python не радуют

Сделал ещё один заход на контроль типов в Python. На этот раз со стороны собственной библиотеки для контроля изменений типов переменных в runtime.

Общие выводы ясны из названия поста, хотя полученная библиотека более-менее работает и я попытаюсь её со временем довести до ума. Если разработчики Python наведут порядок у себя в проекте.

Задумка

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

Краткое обоснование:

  1. Важная часть семантики программы на динамическом языке программирования конструируется во время выполнения программы.
  2. Поэтому закодировать её статически не получится — сложно и дорого.
  3. Поэтому статический анализ типов для динамических языков не пригоден — он игнорирует критические части логики и провоцирует разработчиков на создание костылей для обхода этого игнорирования.
  4. Поэтому анализировать типы имеет смысл только время выполнения программы.

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

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

Библиотеку для такой функциональности я и попытался реализовать, но столкнулся с суровой реальностью.

Annotations, ????, profit

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

Многие пытались, но за время существования аннотаций во втором Python так и не придумали. Делали и ORM, и описания типов (много разных), и конечные автоматы, и чёрт знает что ещё, но ничего толком не получилось.

По мере развития третьего Python, разработчики решили двинуть в типизацию: зарезервировали аннотации для типов и добавили отдельный модуль с логикой их описания — typing.

Но! И я не могу найти этому разумного объяснения, они оставили оригинальную философию: «мы сделаем, а вы придумайте как это использовать».

В итоге сейчас есть стандартный метод описания типов, но не существует стандартного метода проверки их соответствия описанию. Вы не можете простым способом проверить что тип соответствует описанию. Нет даже сторонней библиотеки, которой можно скормить два типа/аннотации и получить анализ их соотношения. За чёрт знает сколько лет она так и не появилась.

Каждая библиотека пишет собственные костыли для этого и эти костыли эпичны.

Mypy — квинтесенция кастылизма. Как не попаду к ним в issues, так вижу комментарии: «да, есть такая проблема, мы её обязательно решим, но потом, сейчас у нас нет ресурсов на это». Конечно у вас нет ресурсов! Вы статический анализ типов к динамическому языку пытаетесь прикрутить. Никогда ресурсов у вас на это не хватит.

В Typeguard дела обстоят лучше, всё-таки он в runtime работает, но и там нет соответствующего кода. Библиотека сразу проверяет соответствие значения переменной аннотации, а две аннотации, например, сравнить не может. Ну и тоже видна собственная реализация сравнения с кучей ограничений и недоработок.

Более того, внутренности модуля typing, насколько я понял, часто перелопачиваются, из-за чего сделать стабильную стороннюю библиотеку ещё сложнее.

Проблемы с интроспекцией выполнения

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

Я остановился на использовании trace функции, которую интерпретатор может вызывать, опционально:

  • при входе в функцию и выходе из неё;
  • при выполнении каждой строки кода;
  • при выполнении каждой команды виртуальной машины.

Сама функция имеет доступ к текущему фрейму выполнения, со всеми его переменными, и к стеку этих фреймов. Кажется, вот оно, всё есть. Ан нет, не всё.

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

Причём объект фрейма содержит информацию о файле с исходниками, номер строки и название функции, но не её объект.

Знаете как принято получать объект функции? Проходят по всем объектам в сборщике мусора и сравнивают байткод объекта с байткодом, для которого надо найти функцию. Как альтернатива, наверно, можно искать модуль по файлу и брать оттуда функцию по имени, но ведь есть ещё методы классов и динамически создаваемые функции, замыкания всякие… И, конечно, может быть две функции с одинаковым байткодом.

Хотя нет, я вру немного. Всё это можно сделать, имея-то callback на каждое выполнение команды ВМ. Достаточно повторить часть логики интерпретатора в своём коде, собирая ту информацию, которую не предоставляет Python. Интересно, почему это никто до сих пор не сделал :-D

Что получилось-то

Описанное выше, конечно, меня изрядно подбесило, но это не те проблемы, которые в состоянии меня остановить :-) Поэтому библиотека работает: медленно, неполноценно, но несоответствие типов именам переменным находит.

Я остановился примерно на следующей логике.

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

На отслеживание сохранения этого инварианта и направлена работа библиотеки.

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

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

К сожалению, при попытке интеграции в Сказку (ветка интеграции) обнаружилось, внезапно :-D, что программа обычно находится на пересечении нескольких доменов — это затрудняет анализ. Например, одни и те же имена могут использоваться в домене логики и в домене работы с базой. Что с этим делать пока непонятно, но буду думать.

Если хотите покопаться в типах, то приглашаю присоединиться к проекту. Сам я вряд ли буду уделять ему много внимания, но с радостью помогу вам.