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

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

nwwvsgregexaj1ae6ds3gpfon-4.png

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


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

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


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

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

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

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


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

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

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

Strict-модули — это Python-модули нового типа, в начале которых есть конструкция __strict__ = True. Они реализованы с использованием множества низкоуровневых механизмов расширяемости, которые уже есть в Python. Особый загрузчик модулей разбирает код с использованием модуля 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 = {x: x+1 for x in MY_LIST}
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-модулем.

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

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

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


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

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

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-модули?

-o2etuqogwhmdnmysb9_vivc9v4.png


1ba550d25e8846ce8805de564da6aa63.png

© Habrahabr.ru