Словник проти об’єкта - який більш ефективний і чому?


126

Що в Python ефективніше в плані використання пам'яті та споживання процесора - словник чи об’єкт?

Передумови: я повинен завантажити величезну кількість даних у Python. Я створив об’єкт, який є просто контейнером для поля. Створення 4-екземплярів і введення їх у словник займало близько 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

Питання: Чи я щось роблю неправильно, або словник просто швидший за об'єкт? Якщо справді словник працює краще, може хтось пояснить, чому?


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__.


2
Це чудово - дякую! Я спробував те ж саме на своїй машині - об'єкт зі слотами є найбільш ефективним підходом (у мене ~ 7 сек).
tkokoszka

6
Також є названі кортежі, docs.python.org/library/collections.html#collections.namedtuple , фабрика класів для об'єктів зі слотами. Це безумовно акуратніше і, можливо, ще більше оптимізовано.
Jochen Ritzel

Я також перевірив названі кортежі та оновив відповідь результатами.
кодеп

1
Я кілька разів запускав ваш код і був здивований, що мої результати відрізняються - слоти = 3sec obj = 11sec dict = 12sec namedtuple = 16sec. Я використовую CPython 2.6.6 на Win7 64bit
Jonathan

Щоб підкреслити пуншлін - nametuple отримав найгірші результати замість найкращих
Джонатан

15

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

У вашому коді, якщо oце Objекземпляр, o.attrеквівалентно o.__dict__['attr']невеликій кількості зайвих накладних витрат.


Ви це тестували? o.__dict__["attr"]є той, хто має додаткові накладні витрати, приймаючи додатковий байт-код; obj.attr швидше. (Звичайно, доступ до атрибутів не буде повільнішим, ніж доступ до підписки - це критичний, сильно оптимізований шлях коду.)
Гленн Мейнард,

2
Очевидно, якщо ви насправді робите o .__ dict __ ["attr"], це буде повільніше - я хотів сказати лише те, що це було рівнозначно тому, а не те, що було реалізовано саме таким чином. Напевно, це не зрозуміло з моїх формулювань. Я також згадав про інші фактори, такі як розподіл пам'яті, час виклику конструктора тощо
Vinay Sajip,

Чи все ж таки це стосується останніх версій python3 через 11 років?
matanster

9

Чи обдумали ви використовувати nametuple ? ( посилання на python 2.4 / 2.5 )

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

Єдиний мінус порівняно зі словниками полягає в тому, що він (як кортежі) не дає можливості змінювати атрибути після створення.


5

Ось копія відповіді @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. дикти найшвидші, але використовують найбільше пам’яті.

Людино, ти повинен перетворити це на питання. Я запустив його і на власному комп’ютері, просто щоб переконатися (у мене не було встановлено psutil, тому я взяв цю частину). У будь-якому випадку це для мене бентежить, і означає, що на початкове запитання не знайдено відповіді. Всі інші відповіді на кшталт "nametuple is great" та "use slots ", і, мабуть, абсолютно новий об'єкт dict кожного разу швидше, ніж вони? Я думаю, що дикти дійсно добре оптимізовані?
Multihunter

1
Схоже, це результат функції makeL, що повертає рядок. Якщо ви повернете порожній список, натомість результати приблизно збігаються з hughdbrown's з python2. Окрім названих пар, завжди повільніше, ніж SlotObj :(
Multihunter

Може виникнути невелика проблема: makeL міг би працювати з різною швидкістю в кожному раунді '@timeit', оскільки рядки кешуються в python - але, можливо, я помиляюся.
Барні

@BarnabasSzabolcs повинен створювати нову рядок щоразу, оскільки вона має замінити значення "Це зразок рядка% s"% i
Jarrod

Так, це вірно в циклі, але у другому тесті я починаю з 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

3

Немає питання.
У вас є дані, без інших атрибутів (без методів, нічого). Отже, у вас є контейнер даних (в даному випадку - словник).

Зазвичай я вважаю за краще мислити з точки зору моделювання даних . Якщо є якесь величезне питання щодо продуктивності, то я можу щось відмовитись від абстракції, але лише з дуже поважних причин.
Програмування стосується управління складністю та підтримки правильної абстракції дуже часто є одним із найкорисніших способів досягнення такого результату.

Щодо причин, коли об'єкт повільніший, я вважаю, що ваше вимірювання не є правильним.
Ви виконуєте занадто мало завдань всередині циклу for для циклу, і тому, що ви бачите там, є різний час, необхідний для створення екземпляра (внутрішнього об'єкта) та "спеціального" об'єкта. Хоча з мовної точки зору вони однакові, вони мають зовсім іншу реалізацію.
Після цього час призначення повинен бути майже однаковим для обох, оскільки в кінці членів підтримується всередині словника.


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

Також є одна перевага над __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

0

Ось мої тестові запуски дуже приємного сценарію @ Jarrod-Chesney. Для порівняння, я також запускаю його проти python2 з "діапазоном", заміненим на "xrange".

З цікавості я також додав подібні тести з OrriedDict (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 все ще виглядають добре.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.