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



Проблема №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-модули?


Комментарии (12)


  1. onegreyonewhite
    15.11.2019 12:50

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

    Это когда лютый джун присылает тебе исправление бага на 100 строк, хотя должен был написать цикл в 4 строки? Не надо термины так переводить — на Хабре к куче переводов постоянно об этом пушут в комментариях.


    А по самой статье — очень интересно. Вроде __slots__ уже давно использую, но никогда не применял strict-модули. Кто-нибудь замерял, влияет ли это на производительность в какую-нибудь сторону или только на безопасное программирование?


    1. eumorozov
      15.11.2019 13:44

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

      Это пропиетарная технология Instagram, я пока не видел, чтобы они сделали ее доступной для всех.


      1. Lsh
        15.11.2019 18:19

        Т.е. ни в каком другом интерпретаторе Python, кроме используемого внутри Instagram, strict использовать нельзя?


        1. kalininmr
          16.11.2019 20:53

          некоторые фичи из этого можно.
          я например slots использую(метаклассами)


  1. walkingpendulum
    15.11.2019 13:08
    +1

    спасибо за статью, очень интересно!


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

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


  1. Lsh
    15.11.2019 18:22

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


    1. onegreyonewhite
      16.11.2019 04:06

      Вторая ссылка в гугле на запрос python __slots__. Более чем понятно.


  1. 0xd34df00d
    15.11.2019 19:06

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


    1. Guzergus
      16.11.2019 08:01

      Что характерно, многие из коллег-питонистов пришли на своих проектах к подобию строгой типизации через аннотации, сторонние решения, etc. С одной стороны, это радует, но с другой несколько обидно, что только сейчас, внезапно, приходит понимание, что javascript/python/other dynamic lang оказывается не такой удобный для крупных проектов (и не только крупных, на самом деле).

      Тем забавнее вспоминать Роберта Мартина и его мысли про необходимость типизации:

      You see, when a Java programmer gets used to TDD, they start asking themselves a very important question: “Why am I wasting time satisfying the type constraints of Java when my unit tests are already checking everything?” Those programmers begin to realize that they could become much more productive by switching to a dyna
      mically typed language like Ruby or Python.

      You don’t need static type checking if you have 100% unit test coverage. And, as we have repeatedly seen, unit test coverage close to 100% can, and is, being achieved.

      2016 год, если что.


      1. 0xd34df00d
        16.11.2019 16:54

        Что-то я о Мартине лучше думал.


        и не только крупных, на самом деле

        Да. Пишу скрипты сложнее трехстрочников, для которых подходит шелл, на статически типизированных языках и отлично себя чувствую.


        1. Guzergus
          16.11.2019 19:42

          Что-то я о Мартине лучше думал.

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


  1. 0xd34df00d
    16.11.2019 16:53

    Del