Про Django ORM и SimpleLazyObject

Недавно я захотел создать собственный middleware, чтобы дополнить объект request, добавив в него дополнительный атрибут. Но я хотел, чтобы этот атрибут вычислялся лениво. Если у вас есть опыт разработки с Django, вы, вероятно, знаете, что он предоставляет ленивые функции, такие как reverse_lazy. Изучая внутреннюю реализацию функции, я обнаружил, что Django предоставляет модуль django.utils.functional, который содержит интересные функции и классы.

Мне понравился SimpleLazyObject, и я заметил, что он используется в middleware под названием django.contrib.auth.middleware.AuthenticationMiddleware. Всё казалось просто пока я стал использовать "ленивый" атрибут в ORM-запросе Django 😁

Мой атрибут возвращает список значений, который должен использоваться для фильтрации в запросе, но основная проблема в том, как Django обрабатывает эти значения внутри ORM. Класс Query имеет метод resolve_lookup_value, заглянув внутрь него я увидел такое условие:

elif isinstance(value, (list, tuple)):
    # The items of the iterable may be expressions and therefore need
    # to be resolved independently.
    values = (
        self.resolve_lookup_value(sub_value, can_reuse, allow_joins, summarize)
        for sub_value in value
    )
    type_ = type(value)
    if hasattr(type_, "_make"):  # namedtuple
        return type_(*values)
    return type_(values)

Этот код означает, что ваш экземпляр SimpleLazyObject будет преобразован в экземпляр SimpleLazyObject, содержащий генератор, возвращающий список значений от исходного объекта. И это не работает как я изначально ожидал. Моё решение было довольно простым: поскольку у меня был уникальный набор элементов, я заменил список значений на множество (имейте в виду, что кортеж не подойдёт из-за условия elif, которое проверяет и список, и кортеж).

Если у вас более сложный случай, я бы посоветовал расширить класс LazyObject (SimpleLazyObject является его подклассом) и реализовать свой собственный метод _setup.