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

Начнём


Имеется 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

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

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


Метки:
Поделиться публикацией
Похожие публикации
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 25
  • +4
    А можно вопрос, зачем здесь ООП? Вы показали, что встроенный namedtuple работает лучше самодельного класса. А зачем тут вообще нужны классы?
    • 0
      Класс в общем то не нужен. Нужна возможность обратиться к колонке по имени.
      value = getattr(rowObject, «ColumnName»)
      логичным видится замапить результат query на class

      В реальной задаче правила проверки лежали в базе в виде (упрощённо)
      таблица | имя колонки | операция | значение
      • 0
        Нет, я конечно понимаю, что с классами удобнее и нагляднее, но если уж говорить про оптимизацию, то лучше наверное здесь просто использовать словарь. И читаемо будет, и быстро. Хотя надо проверить)
        • 0
          быстрее как раз не получилось
          def test4(self):
              return tuple(dict(zip(Columns, row)) for row in self.cursor.execute(self.query))
          

          Результат:
          Sample 1: 4.27413250617
          Sample 2: 15.0353127761
          Sample 3: 4.66455941441
          Sample 4: 6.84044835724
          • 0
            А вроде по цифрам — так получилось. :)
            • 0
              Внимательнее! 4) быстрее чем 2) но медленнее чем 3)
      • +2
        А зачем тут вообще нужен питон если проверки простейшие и напрашивается реализация непосредственно средствами sql?
        • 0
          Это было требование к задаче. Само собой решение на stored procedure предлагалось… Пришлось крутиться.
      • –1
        Python программисту оптимизация должна показаться слишком очевидной.
        • 0
          Фраза в заголовке " для не Python программиста" показалась лишней информацией. Кто разбирается в теме просмотрит и быстро пролистнёт дальше.
          • 0
            Следуя этой логике все статьи стоит начинать со слова «неочевидный», ты ничего не теряешь, просто будет больше тех, кто зайдет и пролистнет дальше.
        • 0
          Со __slots__ не пробовали замерить?

          • 0
            Named tuple реализован с помощью них, смысла в замерах нет.
            • 0
              Судя по выводу исходника сгенеренного класса, не совсем — кода там нагенерировано существенно больше чем просто

              class Test(object):
                  __slots__ = ['field1','field2']

              и работа с полями ведется через property, что тоже может повлиять на время создания инстансов
              • 0
                Вобщем, потестил слегка — размер созданных классов получается одинаковый, размер объектов слегка разный.
                pastebin.com/dfdaxfMe
                • 0
                  Интересно, почему размер объектов разный, и тем более интересно почему у класса меньше

                  Хотя, следуя логике — в named tuple кроме этих двух полей есть ещё и что-то, относящееся к самому named tuple, так что всё ОК.
          • 0
            На создание инстансов класса тратится > 10сек

            setattr(self, "Col{0}".format(x), values[x + 1])

            То есть всё, что предлагается — хранить имена столбцов в описании класса (как делает namedtuple), а не в экземпляре. Ну ок, и что здесь неочевидного?
            • 0
              Для НЕ python программиста очень даже неочевидно. Наверное зря я не указал это в тексте.
            • 0
              Кстати, внезапно всё на том же хабре по тэгу namedtuple: Именованные кортежи из выборок.
              • 0
                А по запросу python namedtuple не выдаёт эту статью.
              • 0
                Почему у вас времена Sample 1/2 в 1-м «подходе» и 2-м существенно отличаются?
                • 0
                  писал в два подхода, возможно в фоне что то работало(тот же антивирус например)
                • +1
                  А кстати, интересно, насколько в этом случае будут производительны ORM-ки. Полагаю что результат будет на уровне Sample 2 и даже выше, поскольку этот тот же маппинг результатов на атрибуты объекта, но всё же интересно узнать насколько хуже.
                  • 0
                    на эту тему есть уже например тут
                    ради интереса сделал тест используя django
                    from django.db import models
                    
                    def buildDjangoObject():
                        class DjangoObject(models.Model):
                            class Meta:
                                app_label = ''
                        DjangoObject.Id = models.IntegerField()
                        for x in xrange(ColsCount):
                            setattr(DjangoObject, "Col{0}".format(x), models.TextField())
                        return DjangoObject
                    
                    DjangoObject = buildDjangoObject()
                    
                    и
                    def test5(self):
                        return tuple(row for row in DjangoObject.objects.raw(self.query))
                    

                    результаты:
                    Sample 1(tuple): 5.4720143202
                    Sample 2(RowWrapper): 19.0111270981
                    Sample 3(TupleClass): 5.80622400633
                    Sample 4(dict): 8.65782098053
                    Sample 5(django.model + raw()): 13.5116426081

                    обновлённый пример целиком тут

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.