Словарь против объекта - что более эффективно и почему?

Что более эффективно в Python с точки зрения использования памяти и потребления процессора - словарь или объект?

Фон: Мне нужно загрузить огромное количество данных в Python. Я создал объект, который является полевым контейнером. Создание экземпляров 4M и помещение их в словарь занимали около 10 минут и ~ 6 ГБ памяти. После того, как словарь готов, доступ к нему - мгновенно.

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

Объект (время выполнения ~ 18 сек.):

class Obj(object):
  def __init__(self, i):
    self.i = i
    self.l = []
all = {}
for i in range(1000000):
  all[i] = Obj(i)

Словарь (время выполнения ~ 12 сек):

all = {}
for i in range(1000000):
  o = {}
  o['i'] = i
  o['l'] = []
  all[i] = o

Вопрос: Я что-то делаю неправильно, или словарь - это быстрее, чем объект? Если действительно словарь лучше работает, может кто-нибудь объяснить, почему?

Ответ 1

Пробовали ли вы использовать __slots__?

Из документации http://docs.python.org/reference/datamodel.html#slots:

"По умолчанию экземпляры как старых, так и новых классов имеют словарь для хранения атрибутов. Это освобождает пространство для объектов, имеющих очень мало переменных экземпляра. Расход пространства может стать острой при создании большого количества экземпляров.

Значение по умолчанию можно переопределить, определяя __slots__ в определении класса нового стиля. Объявление __slots__ принимает последовательность переменных экземпляра и резервирует достаточно места в каждом экземпляре для хранения значения для каждой переменной. Пространство сохраняется, потому что __dict__ не создается для каждого экземпляра. "

Таким образом, это экономит время, а также память?

Сравнение трех подходов на моем компьютере:

test_slots.py:

class Obj(object):
  __slots__ = ('i', 'l')
  def __init__(self, i):
    self.i = i
    self.l = []
all = {}
for i in range(1000000):
  all[i] = Obj(i)

test_obj.py:

class Obj(object):
  def __init__(self, i):
    self.i = i
    self.l = []
all = {}
for i in range(1000000):
  all[i] = Obj(i)

test_dict.py:

all = {}
for i in range(1000000):
  o = {}
  o['i'] = i
  o['l'] = []
  all[i] = o

test_namedtuple.py(поддерживается в версии 2.6):

import collections

Obj = collections.namedtuple('Obj', 'i l')

all = {}
for i in range(1000000):
  all[i] = Obj(i, [])

Запуск теста (с использованием CPython 2.5):

$ lshw | grep product | head -n 1
          product: Intel(R) Pentium(R) M processor 1.60GHz
$ python --version
Python 2.5
$ time python test_obj.py && time python test_dict.py && time python test_slots.py 

real    0m27.398s (using 'normal' object)
real    0m16.747s (using __dict__)
real    0m11.777s (using __slots__)

Использование CPython 2.6.2, включая именованный тестер:

$ python --version
Python 2.6.2
$ time python test_obj.py && time python test_dict.py && time python test_slots.py && time python test_namedtuple.py 

real    0m27.197s (using 'normal' object)
real    0m17.657s (using __dict__)
real    0m12.249s (using __slots__)
real    0m12.262s (using namedtuple)

Итак, да (не удивительно), используя __slots__ - это оптимизация производительности. Использование именованного кортежа имеет сходную производительность с __slots__.

Ответ 2

Доступ к атрибуту в объекте использует доступ к словарю за кулисами - поэтому, используя доступ к атрибутам, вы добавляете дополнительные накладные расходы. Кроме того, в объектном случае вы получаете дополнительные накладные расходы из-за, например, дополнительные распределения памяти и выполнение кода (например, метода __init__).

В вашем коде, если o является экземпляром Obj, o.attr эквивалентен o.__dict__['attr'] с небольшим количеством дополнительных накладных расходов.

Ответ 3

Рассматривали ли вы использование namedtuple? (ссылка для python 2.4/2.5)

Это новый стандартный способ представления структурированных данных, который дает вам производительность кортежа и удобство класса.

Это только недостаток по сравнению со словарями - это то, что (например, кортежи) он не дает вам возможности изменять атрибуты после создания.

Ответ 4

from datetime import datetime

ITER_COUNT = 1000 * 1000

def timeit(method):
    def timed(*args, **kw):
        s = datetime.now()
        result = method(*args, **kw)
        e = datetime.now()

        print method.__name__, '(%r, %r)' % (args, kw), e - s
        return result
    return timed

class Obj(object):
    def __init__(self, i):
       self.i = i
       self.l = []

class SlotObj(object):
    __slots__ = ('i', 'l')
    def __init__(self, i):
       self.i = i
       self.l = []

@timeit
def profile_dict_of_dict():
    return dict((i, {'i': i, 'l': []}) for i in xrange(ITER_COUNT))

@timeit
def profile_list_of_dict():
    return [{'i': i, 'l': []} for i in xrange(ITER_COUNT)]

@timeit
def profile_dict_of_obj():
    return dict((i, Obj(i)) for i in xrange(ITER_COUNT))

@timeit
def profile_list_of_obj():
    return [Obj(i) for i in xrange(ITER_COUNT)]

@timeit
def profile_dict_of_slotobj():
    return dict((i, SlotObj(i)) for i in xrange(ITER_COUNT))

@timeit
def profile_list_of_slotobj():
    return [SlotObj(i) for i in xrange(ITER_COUNT)]

if __name__ == '__main__':
    profile_dict_of_dict()
    profile_list_of_dict()
    profile_dict_of_obj()
    profile_list_of_obj()
    profile_dict_of_slotobj()
    profile_list_of_slotobj()

Результаты:

[email protected]:~$ python ~/Dropbox/src/StackOverflow/1336791.py 
profile_dict_of_dict ((), {}) 0:00:08.228094
profile_list_of_dict ((), {}) 0:00:06.040870
profile_dict_of_obj ((), {}) 0:00:11.481681
profile_list_of_obj ((), {}) 0:00:10.893125
profile_dict_of_slotobj ((), {}) 0:00:06.381897
profile_list_of_slotobj ((), {}) 0:00:05.860749

Ответ 5

Нет вопроса.
У вас есть данные, без каких-либо других атрибутов (без методов, ничего). Следовательно, у вас есть контейнер данных (в данном случае словарь).

Я обычно предпочитаю думать в терминах моделирования данных. Если есть какая-то огромная проблема с производительностью, я могу отказаться от чего-то в абстракции, но только с очень вескими причинами.
Программирование касается управления сложностью, а поддержание правильной абстракции очень часто является одним из наиболее полезных способов достижения такого результата.

О причинах, по которым объект работает медленнее, я думаю, что ваше измерение неверно.
Вы выполняете слишком мало назначений внутри цикла for, и, следовательно, вы видите, что существует другое время, необходимое для создания экземпляра dict (внутреннего объекта) и "пользовательского" объекта. Хотя с точки зрения языка они одинаковы, они имеют совершенно другую реализацию.
После этого время назначения должно быть почти одинаковым для обоих, так как в конце члены поддерживаются внутри словаря.

Ответ 6

Вот копия @hughdbrown ответа для python 3.6.1, я сделал счет 5x больше и добавил некоторый код для проверки объема памяти процесса python в конце каждого прогона.

До того, как у него будут невидители, будьте уверены, что этот метод подсчета размера объектов не является точным.

from datetime import datetime
import os
import psutil

process = psutil.Process(os.getpid())


ITER_COUNT = 1000 * 1000 * 5

RESULT=None

def makeL(i):
    # Use this line to negate the effect of the strings on the test 
    # return "Python is smart and will only create one string with this line"

    # Use this if you want to see the difference with 5 million unique strings
    return "This is a sample string %s" % i

def timeit(method):
    def timed(*args, **kw):
        global RESULT
        s = datetime.now()
        RESULT = method(*args, **kw)
        e = datetime.now()

        sizeMb = process.memory_info().rss / 1024 / 1024
        sizeMbStr = "{0:,}".format(round(sizeMb, 2))

        print('Time Taken = %s, \t%s, \tSize = %s' % (e - s, method.__name__, sizeMbStr))

    return timed

class Obj(object):
    def __init__(self, i):
       self.i = i
       self.l = makeL(i)

class SlotObj(object):
    __slots__ = ('i', 'l')
    def __init__(self, i):
       self.i = i
       self.l = makeL(i)

from collections import namedtuple
NT = namedtuple("NT", ["i", 'l'])

@timeit
def profile_dict_of_nt():
    return [NT(i=i, l=makeL(i)) for i in range(ITER_COUNT)]

@timeit
def profile_list_of_nt():
    return dict((i, NT(i=i, l=makeL(i))) for i in range(ITER_COUNT))

@timeit
def profile_dict_of_dict():
    return dict((i, {'i': i, 'l': makeL(i)}) for i in range(ITER_COUNT))

@timeit
def profile_list_of_dict():
    return [{'i': i, 'l': makeL(i)} for i in range(ITER_COUNT)]

@timeit
def profile_dict_of_obj():
    return dict((i, Obj(i)) for i in range(ITER_COUNT))

@timeit
def profile_list_of_obj():
    return [Obj(i) for i in range(ITER_COUNT)]

@timeit
def profile_dict_of_slot():
    return dict((i, SlotObj(i)) for i in range(ITER_COUNT))

@timeit
def profile_list_of_slot():
    return [SlotObj(i) for i in range(ITER_COUNT)]

profile_dict_of_nt()
profile_list_of_nt()
profile_dict_of_dict()
profile_list_of_dict()
profile_dict_of_obj()
profile_list_of_obj()
profile_dict_of_slot()
profile_list_of_slot()

И это мои результаты

Time Taken = 0:00:07.018720,    provile_dict_of_nt,     Size = 951.83
Time Taken = 0:00:07.716197,    provile_list_of_nt,     Size = 1,084.75
Time Taken = 0:00:03.237139,    profile_dict_of_dict,   Size = 1,926.29
Time Taken = 0:00:02.770469,    profile_list_of_dict,   Size = 1,778.58
Time Taken = 0:00:07.961045,    profile_dict_of_obj,    Size = 1,537.64
Time Taken = 0:00:05.899573,    profile_list_of_obj,    Size = 1,458.05
Time Taken = 0:00:06.567684,    profile_dict_of_slot,   Size = 1,035.65
Time Taken = 0:00:04.925101,    profile_list_of_slot,   Size = 887.49

Мое заключение:

  • Слоты имеют лучшую память и разумны на скорости.
  • dicts являются самыми быстрыми, но используют большую часть памяти.