Хабрахабр

[Перевод] Использование strict-модулей в крупномасштабных Python-проектах: опыт Instagram. Часть 2

Представляем вашему вниманию вторую часть перевода материала, посвящённого особенностям работы с модулями в Python-проектах Instagram. В первой части перевода был дан обзор ситуации и показаны две проблемы. Одна из них касается медленного запуска сервера, вторая — побочных эффектов небезопасных команд импорта. Сегодня этот разговор продолжится. Мы рассмотрим ещё одну неприятность и поговорим о подходах к решению всех затронутых проблем.

Проблема №3: мутабельное глобальное состояние

Взглянем на ещё одну категорию распространённых ошибок.

def myview(request): SomeClass.id = request.GET.get("id")

Тут мы находимся в функции-представлении и присоединяем атрибут к некоему классу, основываясь на данных, полученных из запроса. Вероятно, вы уже поняли суть проблемы. Дело в том, что классы — это глобальные синглтоны. А мы тут помещаем состояние, зависящее от запроса, в долгоживущий объект. В процессе веб-сервера, который выполняется достаточно долго, это может привести к загрязнению каждого будущего запроса, выполняемого в рамках этого процесса.

В частности — в тех случаях, когда программисты пытаются пользоваться обезьяньими патчами и при этом не применяют менеджер контекста — вроде mock.patch. То же самое легко может произойти и в тестах. Это — серьёзная причина ненадёжного поведения нашей системы тестирования. Это может привести уже не к загрязнению запросов, а к загрязнению всех тестов, которые будут выполняться в том же процессе. В результате мы отказались от единой системы тестирования и перешли к схеме изоляции тестов, которую можно описать как «один тест на процесс». Проблема это значительная, да и предотвратить подобное очень сложно.

Мутабельное глобальное состояние — это явление, характерное не только для Python. Собственно говоря, это — наша третья проблема. Речь идёт о классах, модулях, о списках или словарях, прикреплённых к модулям или классам, об объектах-синглтонах, созданных на уровне модуля. Найти такое можно где угодно. Для того чтобы не допустить загрязнения глобального состояния во время работы программы, нужны очень хорошие знания Python. Работа в такой среде требует дисциплинированности.

Знакомство со strict-модулями

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

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

Она заключается в использовании strict-модулей. Поиск решений наших проблем привёл нас к одной идее.

Они реализованы с использованием множества низкоуровневых механизмов расширяемости, которые уже есть в Python. Strict-модули — это Python-модули нового типа, в начале которых есть конструкция __strict__ = True. Особый загрузчик модулей разбирает код с использованием модуля ast, выполняет абстрактную интерпретацию загруженного кода для его анализа, применяет к AST различные трансформации, а затем компилирует AST обратно в байт-код Python, используя встроенную функцию compile.

Отсутствие побочных эффектов при импорте

Strict-модули накладывают некоторые ограничения на то, что может происходить на уровне модуля. Так, весь код уровня модуля (в том числе — декораторы и функции/инициализаторы, вызываемые на уровне модуля) должен быть чистым, то есть — кодом, лишённым побочных эффектов и не использующим механизмы ввода-вывода. Эти условия проверяются абстрактным интерпретатором с помощью средств статического анализа кода во время компиляции.

Код, выполняемый во время импорта модуля, больше не может привести к возникновению неожиданных проблем. Это означает, что использование strict-модулей не приводит к возникновению побочных эффектов при их импорте. Многие виды динамического кода, лишённого побочных эффектов, можно спокойно использовать на уровне модуля. Из-за того, что мы проверяем это на уровне абстрактной интерпретации, используя инструменты, понимающие большое подмножество Python, мы избавляем себя от необходимости в чрезмерном ограничении выразительности Python. Сюда входят и различных декораторы, и определение констант уровня модуля с помощью списков или генераторов словарей.

Вот правильно написанный strict-модуль: Давайте, чтобы было понятнее, рассмотрим пример.

"""Module docstring."""
__strict__ = True
from utils import log_to_network
MY_LIST = [1, 2, 3]
MY_DICT =
def log_calls(func): def _wrapped(*args, **kwargs): log_to_network(f"{func.__name__} called!") return func(*args, **kwargs) return _wrapped
@log_calls
def hello_world(): log_to_network("Hello World!")

В этом модуле мы можем пользоваться обычными конструкциями Python, включая динамический код, такой, который используется при создании словаря, и такой, который описывает декоратор уровня модуля. При этом обращение к сетевым ресурсам в функциях _wrapped или hello_world — это совершенно нормально. Дело в том, что они не вызываются на уровне модуля.

Но если бы мы переместили вызов log_to_network во внешнюю функцию log_calls, или если бы попытались использовать декоратор, вызывающий побочные эффекты (вроде @route из предыдущего примера), или если бы воспользовались вызовом hello_world() на уровне модуля, то он перестал бы быть правильным strict-модулем.

Мы исходим из предположения о том, что всё, импортированное из модулей, не являющихся strict-модулями, небезопасно, за исключением некоторых функций из стандартной библиотеки, о которых известно то, что они безопасны. Как узнать о том, что функции log_to_network или route небезопасно вызывать на уровне модуля? Если модуль utils является strict-модулем, тогда мы можем положиться на анализ нашего модуля, сообщающий нам о том, безопасна ли функция log_to_network.

Это открывает и другие возможности в плане исследования способов ускорения команд импорта. В дополнение к повышению надёжности кода, импорты, лишённые побочных эффектов, устраняют серьёзную преграду к безопасной инкрементной загрузке кода. Это куда лучше, чем следование «жадному» алгоритму, при применении которого весь код модуля выполняется заблаговременно. Если код уровня модуля лишён побочных эффектов, это значит, что мы можем безопасно выполнять отдельные инструкции модуля в «ленивом» режиме, по запросу, при доступе к атрибутам модуля. Это позволит нам организовать быстрый импорт неизменившихся модулей, не требующий повторного выполнения байт-кода уровня модуля. И, учитывая то, что форма всех классов в strict-модуле полностью известна во время компиляции, в будущем мы можем даже попытаться организовать постоянное хранение метаданных модуля (классов, функций, констант), генерируемых при выполнении кода.

Иммутабельность и атрибут __slots__

Strict-модули и классы, объявленные в них, иммутабельны после их создания. Модули делаются иммутабельными с помощью внутренней трансформации тела модуля в функцию, в которой доступ ко всем глобальным переменным организован через переменные замыкания. Эти изменения серьёзно уменьшили возможности по случайному изменению глобального состояния, хотя с мутабельным глобальным состоянием всё ещё можно работать в том случае, если решено будет пользоваться им через мутабельные контейнеры уровня модуля.

Они автоматически записываются в атрибут __slots__ в ходе трансформации AST, выполняемой загрузчиком модуля. Члены классов, объявленных в strict-модулях, кроме того, должны объявляться в __init__. Вот подобный класс: В результате позже нельзя уже прикрепить дополнительные атрибуты к экземпляру класса.

class Person: def __init__(self, name, age): self.name = name self.age = age

В ходе трансформации AST, выполняемой при обработке strict-модулей, будут обнаружены операции присваивания значений атрибутам name и age, выполняемые в __init__, и к классу будет прикреплён атрибут вида __slots__ = ('name', 'age'). Это предотвратит добавление в экземпляр класса любых других атрибутов. (Если же используются аннотации типов, то мы учитываем и сведения о типах, имеющихся на уровне класса, такие, как name: str, и также добавляем их в список слотов).

Они помогают ускорить выполнение кода. Описанные ограничения не только делают код надёжнее. Это позволяет избавиться от поиска по словарю при работе с отдельными экземплярами классов, что ускоряет доступ к атрибутам. Автоматическая трансформация классов с добавлением в них атрибута __slots__ увеличивает эффективность использования памяти при работе с этими классами. Кроме того, мы можем продолжить оптимизацию этих паттернов во время выполнения Python-кода, что позволит нам ещё сильнее улучшить нашу систему.

Итоги

Strict-модули — это всё ещё технология экспериментальная. У нас есть рабочий прототип, мы находимся на ранних стадиях развёртывания этих возможностей в продакшне. Мы надеемся, что после того, как наберёмся достаточно опыта в использовании strict-модулей, сможем рассказать о них подробнее.

Уважаемые читатели! Как вы думаете, пригодятся ли в вашем Python-проекте те возможности, которые предлагают strict-модули?

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»