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

126

Что более эффективно в 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

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

tkokoszka
источник
10
Вам действительно следует использовать xrange вместо диапазона при создании таких больших последовательностей. Конечно, поскольку вы имеете дело с секундами времени выполнения, это не будет иметь большого значения, но все же это хорошая привычка.
Xiong Chiamiov
2
если только это не python3
Барни

Ответы:

157

Вы пробовали использовать __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__.

codeape
источник
2
Это здорово - спасибо! Я пробовал то же самое на своей машине - объект со слотами - самый эффективный подход (у меня было ~ 7 секунд).
tkokoszka
6
Есть также именованные кортежи, docs.python.org/library/collections.html#collections. namedtuple , фабрика классов для объектов со слотами. Он определенно более аккуратный и, возможно, даже более оптимизированный.
Йохен Ритцель,
Я также протестировал именованные кортежи и обновил ответ результатами.
codeape
1
Я запускал ваш код несколько раз и был удивлен, что мои результаты различаются - slots = 3sec obj = 11sec dict = 12sec namedtuple = 16sec. Я использую CPython 2.6.6 на Win7 64bit
Джонатан
Чтобы подчеркнуть кульминацию - namedtuple получил худшие результаты вместо лучших
Джонатан
15

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

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

Винай Саджип
источник
Вы это проверяли? o.__dict__["attr"]это тот, у которого дополнительные накладные расходы, принимая дополнительный байт-код op; obj.attr быстрее. (Конечно, доступ к атрибутам не будет медленнее, чем доступ по подписке - это критический, сильно оптимизированный путь кода.)
Гленн Мейнард,
2
Очевидно, что если вы действительно выполните o .__ dict __ ["attr"], он будет медленнее - я только хотел сказать, что это было эквивалентно этому, а не то, что это было реализовано именно таким образом. Думаю, это не ясно из моей формулировки. Я также упомянул другие факторы, такие как распределение памяти, время вызова конструктора и т. Д.
Винай Саджип,
Так ли обстоит дело с последними версиями python3, 11 лет спустя?
Матанстер
9

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

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

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

Джон Фухи
источник
6

Вот копия ответа @hughdbrown для python 3.6.1. Я увеличил счетчик в 5 раз и добавил код для проверки объема памяти, используемого процессом 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

Мой вывод:

  1. Слоты имеют лучший объем памяти и разумную скорость.
  2. dicts самые быстрые, но используют больше всего памяти.
Джаррод Чесни
источник
Человек, ты должен превратить это в вопрос. Я также запустил его на своем компьютере, просто чтобы убедиться (у меня не было установлено psutil, поэтому я удалил эту часть). В любом случае, это меня сбивает с толку и означает, что на исходный вопрос нет полного ответа. Все остальные ответы похожи на «namedtuple is great» и «use slots », и, по-видимому, новый объект dict каждый раз быстрее их? Думаю, диктовки действительно хорошо оптимизированы?
Multihunter
1
Похоже, это результат того, что функция makeL возвращает строку. Если вместо этого вы вернете пустой список, результаты примерно совпадают с результатами hughdbrown из python2. За исключением того, что namedtuples всегда медленнее, чем SlotObj :(
Multihunter
Может возникнуть небольшая проблема: makeL может работать с разной скоростью в каждом раунде '@timeit', поскольку строки кешируются в python - но, возможно, я ошибаюсь.
Barney
@BarnabasSzabolcs должен каждый раз создавать новую строку, потому что она должна подставлять в значение «Это образец строки% s»% i
Джаррод Чесни
Да, внутри цикла это так, но во втором тесте я снова начинаю с 0.
Барни
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()

Полученные результаты:

hbrown@hbrown-lpt:~$ 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
hughdbrown
источник
3

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

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

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

грабить
источник
0

Есть еще один способ уменьшить использование памяти, если структура данных не должна содержать ссылочные циклы.

Сравним два класса:

class DataItem:
    __slots__ = ('name', 'age', 'address')
    def __init__(self, name, age, address):
        self.name = name
        self.age = age
        self.address = address

и

$ pip install recordclass

>>> from recordclass import structclass
>>> DataItem2 = structclass('DataItem', 'name age address')
>>> inst = DataItem('Mike', 10, 'Cherry Street 15')
>>> inst2 = DataItem2('Mike', 10, 'Cherry Street 15')
>>> print(inst2)
>>> print(sys.getsizeof(inst), sys.getsizeof(inst2))
DataItem(name='Mike', age=10, address='Cherry Street 15')
64 40

Это стало возможным, поскольку structclassклассы -base не поддерживают циклическую сборку мусора, которая в таких случаях не нужна.

Есть также одно преимущество над __slots__классом -based: вы можете добавлять дополнительные атрибуты:

>>> DataItem3 = structclass('DataItem', 'name age address', usedict=True)
>>> inst3 = DataItem3('Mike', 10, 'Cherry Street 15')
>>> inst3.hobby = ['drawing', 'singing']
>>> print(inst3)
>>> print(sizeof(inst3), 'has dict:',  bool(inst3.__dict__))
DataItem(name='Mike', age=10, address='Cherry Street 15', **{'hobby': ['drawing', 'singing']})
48 has dict: True
intellimath
источник
0

Вот мои тестовые прогоны очень красивого сценария @ Jarrod-Chesney. Для сравнения я также запустил его против python2, заменив «range» на «xrange».

Из любопытства я также добавил похожие тесты с OrderedDict (ordict) для сравнения.

Python 3.6.9:

Time Taken = 0:00:04.971369,    profile_dict_of_nt,     Size = 944.27
Time Taken = 0:00:05.743104,    profile_list_of_nt,     Size = 1,066.93
Time Taken = 0:00:02.524507,    profile_dict_of_dict,   Size = 1,920.35
Time Taken = 0:00:02.123801,    profile_list_of_dict,   Size = 1,760.9
Time Taken = 0:00:05.374294,    profile_dict_of_obj,    Size = 1,532.12
Time Taken = 0:00:04.517245,    profile_list_of_obj,    Size = 1,441.04
Time Taken = 0:00:04.590298,    profile_dict_of_slot,   Size = 1,030.09
Time Taken = 0:00:04.197425,    profile_list_of_slot,   Size = 870.67

Time Taken = 0:00:08.833653,    profile_ordict_of_ordict, Size = 3,045.52
Time Taken = 0:00:11.539006,    profile_list_of_ordict, Size = 2,722.34
Time Taken = 0:00:06.428105,    profile_ordict_of_obj,  Size = 1,799.29
Time Taken = 0:00:05.559248,    profile_ordict_of_slot, Size = 1,257.75

Python 2.7.15+:

Time Taken = 0:00:05.193900,    profile_dict_of_nt,     Size = 906.0
Time Taken = 0:00:05.860978,    profile_list_of_nt,     Size = 1,177.0
Time Taken = 0:00:02.370905,    profile_dict_of_dict,   Size = 2,228.0
Time Taken = 0:00:02.100117,    profile_list_of_dict,   Size = 2,036.0
Time Taken = 0:00:08.353666,    profile_dict_of_obj,    Size = 2,493.0
Time Taken = 0:00:07.441747,    profile_list_of_obj,    Size = 2,337.0
Time Taken = 0:00:06.118018,    profile_dict_of_slot,   Size = 1,117.0
Time Taken = 0:00:04.654888,    profile_list_of_slot,   Size = 964.0

Time Taken = 0:00:59.576874,    profile_ordict_of_ordict, Size = 7,427.0
Time Taken = 0:10:25.679784,    profile_list_of_ordict, Size = 11,305.0
Time Taken = 0:05:47.289230,    profile_ordict_of_obj,  Size = 11,477.0
Time Taken = 0:00:51.485756,    profile_ordict_of_slot, Size = 11,193.0

Итак, по обеим основным версиям выводы @ Jarrod-Chesney по-прежнему выглядят хорошо.

Флоран V
источник