В Django для ускорения запросов, возвращающих большое количество данных, существуют методы QuerySet-а values() и values_list(). Первый вместо моделей возвращает словари, второй кортежи. Работать и с теми, и с другими не так удобно как с экземплярами моделей, дескать, платите ребята за скорость удобством. А я вот не хочу, и благодаря именнованым кортежам из стандартного модуля collections, не буду.

Для тех кто не сталкивался и для тех кого пока не убедил, приведу кусочек кода, иллюстрирующий текущее положение вещей:

qs = Item.objects.filter(...)
for item in qs:
    print item.title, item.amount
    total += item.amount * item.price

# используем словари
qs = Item.objects.filter(...).values('title', 'amount', 'price')
for item in qs:
    print item['title'], item['amount']
    total += item['amount'] * item['price']

# используем кортежи
qs = Item.objects.filter(...).values_list('title', 'amount', 'price')
for item in qs:
    print item[0], item[1]
    total += item[1] * item[2]

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

Что же такое именованные кортежи?

Именованные кортежи появились в питоне 2.6 и представляют собой кортежи, в которых доступ к элементам возможен по заданным атрибутам. Небольшой кусочек кода для демонстрации:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
>>> from collections import namedtuple
>>> Point = namedtuple('Point', 'x y'.split())
>>> p = Point(x=11, y=22)   # создание с помощью именованных
>>> p2 = Point(11, 22)      # ... или позиционных параметров
>>> p1 == p2
True
>>> p[0] + p[1]             # индексация
33
>>> x, y = p                # ... и распаковка как для обычных кортежей
>>> x, y
(11, 22)
>>> p.x + p.y               # доступ к элементам через атрибуты
33
>>> Point._make([11, 22])   # создание из итерируемого объекта
Point(x=11, y=22)

NamedTuplesQuerySet

Осталось создать потомок QuerySet, который будет выдавать именованные кортежи. Благодаря тому, что вся логика запроса с обозначенными полями реализована в ValuesQuerySet (возвращается из QuerySet.values()), нам остаётся только обработать результат перед выдачей:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from itertools import imap
from collections import namedtuple

from django.db.models.query import ValuesQuerySet

class NamedTuplesQuerySet(ValuesQuerySet):
    def iterator(self):
        # собираем список имён полей
        extra_names = self.query.extra_select.keys()
        field_names = self.field_names
        aggregate_names = self.query.aggregate_select.keys()
        names = extra_names + field_names + aggregate_names

        # создаём класс кортежа
        tuple_cls = namedtuple('%sTuple' % self.model.__name__, names)

        results_iter = self.query.get_compiler(self.db).results_iter()
        # заворачиваем каждую строку в наш именованный кортеж
        return imap(tuple_cls._make, results_iter)

И, для удобства использования припишем метод к QuerySet:

1
2
3
4
5
from django.db.models.query import QuerySet

def namedtuples(self, *fields):
    return self._clone(klass=NamedTuplesQuerySet, setup=True, _fields=fields)
QuerySet.namedtuples = namedtuples

После этого код, приведённый в начале статьи, для моделей, словарей и кортежей можно будет записать как:

qs = Item.objects.filter(...).namedtuples('title', 'amount', 'price')
for item in qs:
    print item.title, item.amount
    total += item.amount * item.price

Забавно, что получившийся класс выполняет функции обоих имеющихся ускорителей values() и values_list(). Причём делает это либо лучше, либо почти также и при этом порождает более красивый код. Может, когда-нибудь это приведёт к их замене.

P.S. Код класса вместе с патчем можно стянуть отсюда gist.github.com/876324

P.P.S. Порядок полей в кортеже может не соответствовать порядку аргументов nametuples(), забил на это ради скорости. Реализацию переупорядочивания полей, если кому надо, можно взять из ValuesListQuerySet.iterator().