Package: astrologic
Пакет Astrologic содержит ряд инструментов, позволяющих "в рантайме" оптимизировать работу ваших функций или даже конструировать новые функции. Все это достигается за счет парсинга и преобразованияASTисходной функции. Парсинг происходит полностью "под капотом" и невидим для пользователя.ОглавлениеДисклеймерКак это работаетУстановкаВключаем и отключаем блоки кодаОптимизируем хвостовую рекурсиюИнлайним функцииДисклеймерВсе, что вы найдете в данном пакете - это демонстрация концепций. Существует огромное количество весьма разумных причин, почему код из данной библиотеки, или из других, содержащих подобные хаки, не должен использоваться в "реальности". Как минимум, манипуляции с AST приводят к тому, что у вас начинает реально исполняться совершенно не тот код, который вы видите в файлах проекта. Средства интроспекции Python тоже совершенно не ожидают таких усовершенствований и могут давать ложную информацию.Кроме того, большинство предложенных здесь оптимизаций фактически бесполезны. Они оптимизируют вещи, которые, как правило, отъедают совсем немного машинного времени, однако это происходит за счет значительного увеличения времени инициализации объектов в модулях программы.Если уж вы по собственной глупости решите практически использовать данную библиотеку, пожалуйста, хорошо тестируйте свой код. Иногда вы можете столкнуться с совершенно неожиданным его поведением. Учитывайте, что предложенные здесь функции плохо протестированы (ввиду чисто демонстрационных целей их написания), и содержащиеся в них потенциальные баги могут удивить даже автора.Как это работаетИнтерпретация программы на Python происходит в несколькоэтапов. Если коротко: сначала файл считывается и разбивается на токены, затем из считанных токенов составляетсяабстрактное синтаксическое дерево, на основе дерева компилируетсябайт-код, ну и тот уже в итоге выполняется виртуальной машиной.Мы не внедряемся в этот процесс, а обходим его. К сожалению, после компиляции байт-кода, абстрактное синтаксическое дерево не сохраняется, поэтому, если мы хотим его как-то исправить, нам надо проделать весь путь заново: получить исходный код, построить на его основе AST, скорректировать AST как нам требуется и скомпилировать из него байт-код.Встроенныесредства интроспекциипозволяют нам извлечь из объекта функциитекст ее исходного кода. Этот текст мы скармливаем функцииast.parse()и получаем уже нужный нам объект дерева, с которым дальше будем производить все оптимизации. Пакетastтакже предоставляет готовые классы для обхода дерева и замены отдельных нод, чем мы и пользуемся. После всех манипуляций мы скармливаем уже видоизмененное дерево встроенной функцииcompile(), чтобы получить порцию байт-кода. Байт-код мы выполняем черезexec(), передав туда словарь "пространства имен".exec()выполняет код инициализации функции как бы в отдельном "виртуальном" модуле. После выполнения инициализации мы достаем функции по имени из переданного вexec()словаря. Инициализация некоторых функций дергает дополнительные объекты из глобального пространства имен, поэтому, если попытаться выполнить их инициализацию в "голом"
модуле, мы получим ожидаемое исключение. Чтобы этого избежать, мы предварительно вручную (при помощиimportlib) импортируем модуль, из которого извлечена обрабатываемая нами функция, и все его содержимое перекладываем в тот самый словарь, что мы передаемexec()в качестве "пространства имен".Исходя уже из описания процесса ясно, что у него не может не быть побочных эффектов. Вот некоторые из них:Когда мы добавляем новые ноды в AST, они лишены привязки к конкретным строкам исходного кода. Если там случится исключение, в полученном трейсбэке вы получите искаженную информацию.Все эти повторные парсинги-компиляции довольно ресурсоемки.Как выяснилось опытным путем, директиваglobalв измененных таким образом функциях работает не совсем ожидаемым образом. Она указывает на соответствующую переменную в том "виртуальном" модуле, что был для нее создан при выполнении инициализации черезexec(). Вы можете воздействовать из своей функции на глобальную переменную там, в то время как глобальная переменная с тем же именем в том модуле, где фактически расположен исходный код вашей функции, никак затронута не будет.Фактически выполняется не тот код, что написан в вашем коде. Это, собственно, самая большая проблема. Вся суть программирования заключается в том, что вы пишете код и машинаеговыполняет. А тут она выполняет что-то другое, и вы толком не знаете, что (если хотите таки посмотреть, что именно выполнится, передайте в любой из декораторов данной библиотеки аргументdebug_mode_on=True, тогда в консоли вы увидите текст функции, которая была сгенерирована после изменения AST).Это не будет работать с функциями из замыканий.Нельзя навесить несколько модифицирующих AST декораторов на одну функцию.Особенность конкретно данной реализации, но не технологии в принципе. Модифицирующий декоратор должен быть первым в списке декораторов. Кроме того, все декораторы и строка с объявлением функции должны быть однострочными.Это значительные минусы, и, как вы могли прочитать вдисклеймере, я считаю их достаточным основанием, чтобы никогда в практическом смысле не использовать модификации AST. Однако это не мешает нам в развлекательных целях создавать "магию".УстановкаУстановите Astrologic черезpip:$ pip install astrologicВключаем и отключаем блоки кодаПростейшим из декораторов библиотеки Astrologic является@switcher. Вот пример его использования:fromastrologicimportswitcher@switcher(a=False,b=True)deffunction():print('begin')ifa:print('block a')ifb:print('block b')print('end')function()# Что будет выведено? Проверьте сами!@switcherпроходится по унарным условиям (условиям, в которых только один операнд). Если название переменной, которая фигурирует в данном условии, присутствует в именованных аргументах самого декоратора, он делает одну из двух вещей. Если именованный аргумент равен True, он достает данный блок кода из условия заменяет им само условие. Если аргумент равен False, он удаляет условие вместе с блоком кода. То есть функция из примера выше превращается в:# Тот код, который будет исполнен на машине. Блок кода "if a:" полностью вырезан, а блок "if b:" вытащен из проверки, в то время как сама проверка тоже вырезана.deffunction():print('begin')print('block b')print('end')Использовать данный декоратор вы можете, к примеру, чтобы избежать каких-то проверок в вашем коде, основанных на константах. При инициализации функций в начале работы интерпретатора вы указываете, какие блоки кода вам нужны. По сути, if'ы в данном случае служат не по прямому назначению, а работают разметкой кода.Поскольку@switcherредактирует функции на уровне AST, в некоторых случаях применение декоратора может ускорить ваш код, полностью убрав ненужные операции из реально исполняемого машиной кода.У@switcherесть ряд ограничений и правил использования, которые необходимо учитывать:Декоратор@switcherдолжен быть первым в списке декораторов, все остальные можно накладывать только поверх. Если не соблюсти это правило, декораторы, лежащие под@switcher, могут просто исчезнуть, будто их не было.Никаких else или else if у блоков if, которыми вы управляете через декоратор! У прочих блоков if использовать else можно, поскольку они никаких не модифицируются.Оптимизируем хвостовую рекурсиюЕще один крутой трюк с AST - автоматическая оптимизацияхвостовой рекурсии. Для этого нам понадобится декоратор@no_recursion:fromastrologicimportno_recursion@no_recursiondefc(b):ifb==100000000:returnbreturnc(b+1)print(c(1))# Попробуйте выполнить это сами!Как вы, вероятно, знаете, максимальная глубина рекурсии в Python ограничена, и обычно составляет около 1000. Однако код из этого примера отработает и вернет корректный результат, поскольку декоратор@no_recursionавтоматически преобразовал рекурсивный код в итеративный. В отличие от другихрешенийданной задачи, здесь не потребовалась менять исходный код функции, кроме как навешиванием на нее декоратора.Надо сказать, против автоматической оптимизации хвостовой рекурсиивысказывалсядаже самГвидо Ван Россум. Его доводы довольно весомы, но они относятся, прежде всего, к внедрению данной возможности непосредственно в интерпретатор. Сделать такую оптимизацию бесшовной, с учетом интерпретируемой и гибкой природы Python, действительно невозможно. Однако это не мешает нам сделать это, пойдя на некоторые ограничения.Вот они:Поддерживается только обычная хвостовая рекурсия. Она выглядит вот так:deffunction():returnfunction()Функция вызывает сама себя в блоке return и никак иначе. Никакие другие кейсы, включая, скажем, перекрестную рекурсию (это когда функция А вызывает функцию Б, а та, в свою очередь - функцию А), не покрываются.За рекурсию принимается вызов объекта с тем же именем, какое название у функции. Вызов того же имени от другого объекта будет принят за рекурсию!deffunction():returnobj.function()# Декоратор примет это за рекурсию, хотя, очевидно, obj.function != function.Внутри функции нельзя использовать именаis_recursionиsuperfunction. Особенности реализации. Остальные имена можно.Стек-трейсы, как и предупреждал Гвидо, могут не совсем корректно отражать реальность.Инлайним функции (экспериментальная фича)Следующая оптимизация - инлайновые функции. В данном пакете это работает немного иначе, чем вы, вероятно, ожидаете, если знаете протакую возможностьв C++. Здесь меткаinlineвешается не на ту функцию, которую мы инлайним, а на ту,вкоторую.fromastrologicimportinlinedefa(c):d=cprint(d)@inline('a')defb():print('lol')c='kek'a(c)b()На этом примере мы инлайним функцию a() в функцию b(). Имена функций, которые мы инлайним, должны быть перечислены при вызове декоратора@inline.По итогу код из примера преобразовывается примерно вот в это:defb():print('lol')c='kek'generated_ddea0cfb886b422891e7643ff31efbfa=cd=generated_ddea0cfb886b422891e7643ff31efbfaprint(d)Содержимое функции a() "вклеивается" в функцию b(), с попутным разрешением возможного конфликта имен.Ограничения:У функций, которые мы инлайним, не должно быть именованных параметров. Мне просто лень это прикручивать, для демонстрации возможности хватит и текущей версии.