2025, Nov 06 03:01

SyntaxError в annotate: как корректно считать Count с Q и ~Q

Разбираем SyntaxError: positional argument follows keyword argument при annotate в Django ORM. Как корректно использовать фильтрованный Count с Q и ~Q без N+1.

Подсчёт связанных строк с условием в Django ORM кажется простым, но безобидный рефакторинг способен обернуться синтаксической ошибкой задолго до того, как до базы данных дойдёт сам запрос. Ниже — практичный разбор распространённой ловушки при использовании annotate с фильтрованным Count и способа её исправить, не сомневаясь в корректности логики Q.

Постановка задачи

Цель — агрегировать количество связанных объектов EventReport для каждого Store, исключив записи со статусом «C». Рефакторинг заменяет покомпонентные запросы на annotate с фильтрованным Count, чтобы устранить проблему N+1.

Однако вызов приводит к ошибке SyntaxError: positional argument follows keyword argument. Это быстро отвлекает от сути агрегации и вызывает сомнения, допустимо ли вообще выражение Q(...) & ~Q(...) внутри Count(..., filter=...).

Неудачный пример

Задача — в одном агрегате задать фильтр по диапазону дат и исключить статус «C»:

monitored_stores_qs.annotate(
    reports_total=Count(
        'eventreport',
        filter=Q(
            eventreport__event_at__gte=window_start,
            eventreport__event_at__lte=yesterday_cutoff
        ) & ~Q(eventreport__status='C')
    ),
    Count('another_metric'),
)

Есть и вариант, где фильтр формируется тем же способом:

filter=Q(
    eventreport__event_at__gte=range_start,
    eventreport__event_at__lte=range_end
) & ~Q(eventreport__status='C')

Ожидается, что будут учтены только связанные строки EventReport в заданном окне дат и без статуса «C».

Что на самом деле не так

Это не ошибка Django — её выбрасывает сам Python. Корень проблемы — порядок аргументов при вызове функции. В Python позиционные аргументы должны идти раньше именованных. Минимальный пример: f(x, y=z) — корректно, а f(y=z, x) — нет. В нашем коде annotate сначала передаёт именованный аргумент, а затем «голый» позиционный, что и вызывает синтаксическую ошибку.

Само выражение Q(...) & ~Q(...) подходит для построения фильтра; парсер до логики Django просто не добирается — интерпретатор останавливается на некорректной сигнатуре вызова.

Исправление и корректный пример

Сохраняйте фильтрованный Count как есть и проследите, чтобы позиционные аргументы шли перед именованными в одном вызове. Достаточно переставить аргументы annotate:

monitored_stores_qs.annotate(
    Count('another_metric'),
    reports_total=Count(
        'eventreport',
        filter=Q(
            eventreport__event_at__gte=window_start,
            eventreport__event_at__lte=yesterday_cutoff
        ) & ~Q(eventreport__status='C')
    ),
)

Так мы сохраняем задумку: один проход по связанным EventReport, подсчёт по Store, в нужном интервале дат и без статуса «C». Ключевое изменение — только порядок аргументов, а не логика агрегации или выражения Q.

Почему это важно

При оптимизации ORM-кода ради устранения N+1 легко списать сбой на базу данных или выражения Django. На деле правило синтаксиса Python способно сорвать работу ещё до построения запроса. Внимательно читайте полное сообщение об ошибке и проверяйте сигнатуру вызова — это экономит время и уберегает от неверного диагноза: конструкция Q и ~Q здесь ни при чём.

Вывод

Если при сборке annotate появляется SyntaxError: positional argument follows keyword argument, сначала проверьте, не идут ли позиционные аргументы после именованных в одном вызове. Для фильтруемых агрегатов конструкция Count('eventreport', filter=Q(...) & ~Q(...)) точно выражает условие status != 'C' вместе с другими критериями. Логику можно оставить без изменений — просто поправьте порядок аргументов, и агрегация заработает как задумано.

Статья основана на вопросе с StackOverflow от Raul Chiarella и ответе willeM_ Van Onsem.