Słownik vs obiekt-co jest bardziej efektywne i dlaczego?

Co jest bardziej wydajne w Pythonie pod względem wykorzystania pamięci i zużycia procesora-Słownik czy obiekt?

Tło: Muszę załadować ogromną ilość danych do Pythona. Stworzyłem obiekt, który jest tylko kontenerem pola. Tworzenie instancji 4M i umieszczanie ich w słowniku zajęło około 10 minut i ~6GB pamięci. Gdy słownik jest gotowy, dostęp do niego jest mrugnięciem oka.

Przykład: Aby sprawdzić wydajność napisałem dwa proste programy, które robią to samo-jeden używa obiektów, drugi słownika:

Obiekt (czas wykonania ~18sek):

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

Słownik (czas wykonania ~12sek):

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

Pytanie: Czy robię coś nie tak, czy słownik jest po prostu szybszy niż obiekt? Jeśli rzeczywiście słownik działa lepiej, może ktoś wyjaśnić dlaczego?

Author: tkokoszka, 2009-08-26

8 answers

Próbowałeś użyć __slots__?

Z dokumentacji :

Domyślnie instancje klas Starego i nowego stylu mają słownik do przechowywania atrybutów. To marnuje miejsce na obiekty posiadające bardzo mało zmiennych instancji. Zużycie przestrzeni może stać się dotkliwe przy tworzeniu dużej liczby instancji.

Wartość domyślna może zostać nadpisana przez zdefiniowanie __slots__ w definicji klasy w Nowym stylu. Deklaracja __slots__ przyjmuje sekwencję instancji zmienne i rezerwy wystarczająca ilość miejsca w każdym wystąpieniu do przechowywania wartości dla każdej zmiennej. Przestrzeń jest zapisywana, ponieważ __dict__ nie jest tworzona dla każdej instancji.

Czy to oszczędza zarówno czas, jak i pamięć?

Porównanie trzech podejść na moim komputerze:

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 (obsługiwane w 2.6):

import collections

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

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

Uruchom benchmark (używając 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__)

Korzystanie z CPython 2.6.2, w tym nazwanego testu krotki:

$ 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)

Więc tak (nie jest to zaskoczeniem), użycie __slots__ jest optymalizacją wydajności. Użycie krotki nazwanej ma podobną wydajność do __slots__.

 163
Author: codeape,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-06-20 09:12:55

Dostęp do atrybutów w obiekcie wykorzystuje dostęp do słownika za kulisami - więc używając dostępu do atrybutów dodajesz dodatkowe koszty. Dodatkowo w przypadku obiektu, ponosisz dodatkowe koszty z powodu np. dodatkowych alokacji pamięci i wykonania kodu(np. metody __init__).

W Twoim kodzie, Jeśli o jest instancją Obj, o.attr jest równoważne o.__dict__['attr'] z niewielką ilością dodatkowych kosztów.

 17
Author: Vinay Sajip,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-08-26 19:03:57

Rozważałeś użycie o nazwie? (link do Pythona 2.4/2.5)

Jest to nowy standardowy sposób reprezentacji danych strukturalnych, który zapewnia wydajność krotki i wygodę klasy.

Jedynym minusem w porównaniu ze słownikami jest to, że (jak krotki) nie daje możliwości zmiany atrybutów Po utworzeniu.

 9
Author: John Fouhy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-02-08 23:08:12

Oto Kopia odpowiedzi @hughdbrown dla Pythona 3.6.1, zwiększyłem liczbę 5x i dodałem trochę kodu, aby przetestować ślad pamięci procesu Pythona pod koniec każdego uruchomienia.

Zanim downvoterzy się na nim, należy pamiętać, że ta metoda zliczania wielkości obiektów nie jest dokładna.

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()

A to są moje wyniki

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

Mój wniosek brzmi:

  1. sloty mają najlepszą pamięć i są rozsądne pod względem szybkości.
  2. dicts are the najszybszy, ale wykorzystaj najwięcej pamięci.
 7
Author: Jarrod Chesney,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-12-07 19:22:45
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()

Wyniki:

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
 4
Author: hughdbrown,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-05-03 14:58:48

Nie ma wątpliwości.
Masz dane, bez innych atrybutów(bez metod, nic). Stąd masz kontener danych(w tym przypadku słownik).

Zazwyczaj wolę myśleć w kategoriach modelowania danych . Jeśli jest jakiś ogromny problem z wydajnością, to mogę zrezygnować z czegoś w abstrakcji, ale tylko z bardzo dobrych powodów.
Programowanie polega na zarządzaniu złożonością, a utrzymywanie poprawnej abstrakcji {[2] } jest bardzo często jednym z najbardziej przydatny sposób, aby osiągnąć taki wynik.

O powody obiekt jest wolniejszy, myślę, że twój pomiar nie jest poprawny.
Wykonujesz zbyt mało zadań wewnątrz pętli for i dlatego widzisz inny czas potrzebny do utworzenia instancji dict (obiektu wewnętrznego) i obiektu "niestandardowego". Choć z punktu widzenia językowego są takie same, mają całkiem inną implementację.
Po tym czasie czas przydziału powinien być prawie to samo dla obu, tak jak na końcu członkowie są utrzymywane wewnątrz słownika.

 3
Author: rob,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-08-26 19:28:12

Istnieje jeszcze inny sposób na zmniejszenie zużycia pamięci, jeśli struktura danych nie powinna zawierać cykli referencyjnych.

Porównajmy dwie klasy:

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

I

$ 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

Stało się to możliwe, ponieważ klasy bazujące na structclass nie obsługują cyklicznego garbage collection, co nie jest potrzebne w takich przypadkach.

Jest też jedna przewaga nad klasą bazującą na __slots__: możesz dodać dodatkowe atrybuty:

>>> 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
Author: intellimath,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-12-26 09:32:07

Oto moje testy bardzo ładnego skryptu @ Jarrod-Chesney. Dla porównania, ja również uruchomić go z python2 z "zakres" zastąpiony przez "xrange".

Z ciekawości dodałem również podobne testy z OrderedDict (ordict) dla porównania.

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

Więc, w obu głównych wersjach, wnioski @Jarrod-Chesney nadal wyglądają dobrze.

 0
Author: Florent V,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-04-18 13:37:02