Словарь против объекта - что эффективнее и почему? - PullRequest
111 голосов
/ 26 августа 2009

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

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

Ответы [ 7 ]

139 голосов
/ 26 августа 2009

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

14 голосов
/ 26 августа 2009

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

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

8 голосов
/ 27 августа 2009

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

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

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

3 голосов
/ 05 июля 2017

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

Перед тем, как это сделают downvoters, имейте в виду, что этот метод подсчета размеров объектов не является точным.

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. диктовки самые быстрые, но используют больше всего памяти.
3 голосов
/ 26 августа 2009
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
2 голосов
/ 26 августа 2009

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

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

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

0 голосов
/ 25 декабря 2018

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

Давайте сравним два класса:

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 не поддерживают циклический сбор мусора, который в таких случаях не нужен.

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

>>> 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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...