Неочевидная оптимизация по скорости при решении конкретной задачи на Python

в 10:01, , рубрики: python, sql, метки: ,

Начнём

Имеется SQL база данных. Задача описывается тремя фразами:

  • выгрузка данных
  • валидация данных
  • генерация отчёта

Задача детальнее

  1. Скрипт должен выполняться очень часто.
  2. Выгрузка данных заключается в вычитке из базы результат простейшего запроса SELECT * FROM table. В таблице/вьюшке строк обычно более 100000, колонок ~100.
  3. Валидация представляет собой проверку набора условий вида rowObject.Column1 == Value (<, >, !=) и более сложных проверок. Смысл в том что проверка требует обращения к колонке по имени.
  4. Генерация отчёта по результату проверок.

Обратим внимание на пункт 1

Остальное не так интересно.
(В качестве примера использую базу данных sqlite)

Использовать какую либо ORM для такой задачи по меньшей мере странно. Делаем в лоб (для упрощения в память выгружаем весь результат)

import sqlite3
conn = sqlite3.connect(filePath)
result = tuple(row for row in conn.cursor().execute("SELECT * FROM test"))

После выполнения result содержит кортеж кортежей. Нам же нужен объект с аттрибутами.
Усложняем:

ColsCount = 100
class RowWrapper(object):
    def __init__(self, values):
        self.Id = values[0]
        for x in xrange(ColsCount):
            setattr(self, "Col{0}".format(x), values[x + 1])
result = tuple(RowWrapper(row) for row in conn.cursor().execute(self.query))

Мы готовы перейти к пункту 2. Или нет? А давайте замеряем скорость обоих примеров(полный тестовый код здесь).
100000 строк, 101 колонка
У меня получилось в секундах:
Sample 1: 4.64823588605
Sample 2: 17.1091031498
На создание инстансов класса тратится > 10сек
С++ программисту внутри меня захотелось с этим что-нибудь сделать.

Решение нашлось такое

Используем namedtuple из модуля collections. Не буду описывать здесь подробно принцип его работы. Приведу лишь небольшой пример демонстрирующий нужную нам функциональность.

import collections
columns = ('name', 'age', 'story')
values = ('john', '99', '...blahblah...')
SuperMan = collections.namedtuple('SuperMan', columns)
firstSuperMan = SuperMan._make(values)
print(firstSuperMan.name)
print(firstSuperMan.age)
print(firstSuperMan.story)

А теперь пример в контексте задачи:

import collections
columns = tuple(itertools.chain(('Id',), tuple("Col{0}".format(x) for x in xrange(ColsCount))))
TupleClass = collections.namedtuple("TupleClass", Columns)
result = tuple(TupleClass._make(row) for row in conn.cursor().execute(self.query))

Замеряем скорость:
Sample 1: 4.30456730876
Sample 2: 15.3314512807
Sample 3: 4.67008026138

Совсем другое дело. Полный пример кода с созданием базы и замерами скорости смотрим здесь

Для примеров в статье использовалось

Автор: tbd

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js