Dizionario vs oggetto - quale è più efficiente e perché?


126

Cosa è più efficiente in Python in termini di utilizzo della memoria e consumo di CPU: dizionario o oggetto?

Sfondo: devo caricare enormi quantità di dati in Python. Ho creato un oggetto che è solo un contenitore di campi. La creazione di istanze 4M e l'inserimento in un dizionario ha richiesto circa 10 minuti e ~ 6 GB di memoria. Dopo che il dizionario è pronto, accedervi è un battito di ciglia.

Esempio: per verificare le prestazioni ho scritto due semplici programmi che fanno lo stesso: uno sta usando oggetti, l'altro dizionario:

Oggetto (tempo di esecuzione ~ 18 sec):

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

Dizionario (tempo di esecuzione ~ 12 sec):

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

Domanda: sto facendo qualcosa di sbagliato o il dizionario è solo più veloce dell'oggetto? Se il dizionario funziona davvero meglio, qualcuno può spiegare perché?


10
Dovresti davvero usare xrange invece di range quando generi sequenze così grandi. Naturalmente, dato che hai a che fare con secondi di tempo di esecuzione, non farà molta differenza, ma è comunque una buona abitudine.
Xiong Chiamiov,

2
a meno che non sia python3
Barney il

Risposte:


157

Hai provato a usare __slots__?

Dalla documentazione :

Per impostazione predefinita, le istanze di classi sia vecchie che nuove hanno un dizionario per la memorizzazione degli attributi. Questo spreca spazio per oggetti con pochissime variabili di istanza. Il consumo di spazio può diventare acuto durante la creazione di un gran numero di istanze.

L'impostazione predefinita può essere ignorata definendo __slots__in una definizione di classe di nuovo stile. La __slots__dichiarazione accetta una sequenza di variabili di istanza e riserva uno spazio sufficiente in ciascuna istanza per contenere un valore per ogni variabile. Lo spazio viene salvato perché __dict__non viene creato per ogni istanza.

Quindi questo fa risparmiare tempo e memoria?

Confrontando i tre approcci sul mio computer:

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 (supportato in 2.6):

import collections

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

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

Esegui benchmark (usando 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__)

Utilizzando CPython 2.6.2, incluso il test della tupla denominato:

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

Quindi sì (non proprio una sorpresa), l'utilizzo __slots__è un'ottimizzazione delle prestazioni. L'uso di una tupla con nome ha prestazioni simili a __slots__.


2
È fantastico, grazie! Ho provato lo stesso sulla mia macchina - l'oggetto con slot è l'approccio più efficiente (ho ~ 7sec).
tkokoszka,

6
Esistono anche tuple, docs.python.org/library/collections.html#collections.namedtuple , una factory di classe per oggetti con slot. È decisamente più ordinato e forse anche più ottimizzato.
Jochen Ritzel,

Ho testato anche le tuple nominate e ho aggiornato la risposta con i risultati.
codeape,

1
Ho eseguito il codice alcune volte e sono rimasto sorpreso dal fatto che i miei risultati differiscono: slot = 3sec obj = 11sec dict = 12sec namedtuple = 16sec. Sto usando CPython 2.6.6 su Win7 64 bit
Jonathan il

Per enfatizzare la battuta finale - namedtuple ha ottenuto i risultati peggiori anziché i migliori
Jonathan

15

L'accesso agli attributi in un oggetto utilizza l'accesso al dizionario dietro le quinte, quindi utilizzando l'accesso agli attributi si aggiunge un sovraccarico aggiuntivo. Inoltre, nel caso dell'oggetto, si verificano ulteriori sovraccarichi a causa, ad esempio, di allocazioni di memoria aggiuntive ed esecuzione di codice (ad esempio del __init__metodo).

Nel tuo codice, se oè Objun'istanza, o.attrè equivalente a o.__dict__['attr']una piccola quantità di sovraccarico aggiuntivo.


Hai provato questo? o.__dict__["attr"]è quello con overhead extra, prendendo un bytecode extra op; obj.attr è più veloce. (Naturalmente l'accesso agli attributi non sarà più lento dell'accesso alla sottoscrizione - è un percorso di codice critico e fortemente ottimizzato.)
Glenn Maynard,

2
Ovviamente se in realtà non o .__ dict __ [ "attr"] sarà più lento - Volevo solo dire che era equivalente a quella, non che è stata attuata proprio in quel modo. Immagino non sia chiaro dalle mie parole. Ho anche menzionato altri fattori come allocazioni di memoria, tempo di chiamata del costruttore ecc.
Vinay Sajip,

È ancora così con le recenti versioni di python3, 11 anni dopo?
matanster

9

Hai preso in considerazione l'uso di un namedtuple ? ( collegamento per Python 2.4 / 2.5 )

È il nuovo modo standard di rappresentare dati strutturati che ti offre le prestazioni di una tupla e la comodità di una classe.

L'unico aspetto negativo rispetto ai dizionari è che (come le tuple) non ti dà la possibilità di cambiare gli attributi dopo la creazione.


5

Ecco una copia della risposta di @hughdbrown per python 3.6.1, ho ampliato il conteggio di 5 volte e aggiunto del codice per testare l'impronta di memoria del processo python alla fine di ogni esecuzione.

Prima che i downvoter lo abbiano, tieni presente che questo metodo di conteggio delle dimensioni degli oggetti non è accurato.

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

E questi sono i miei risultati

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

La mia conclusione è:

  1. Gli slot hanno il miglior footprint di memoria e sono ragionevoli sulla velocità.
  2. i dadi sono i più veloci, ma usano più memoria.

Amico, dovresti trasformarlo in una domanda. L'ho eseguito anche sul mio computer, solo per essere sicuro (non avevo installato psutil, quindi ho eliminato quella parte). Ad ogni modo, questo è sconcertante per me e significa che alla domanda originale non viene data una risposta completa. Tutte le altre risposte sono come "namedtuple is great" e "use slot ", e apparentemente un nuovissimo oggetto dict ogni volta che è più veloce di loro? Immagino che i dadi siano davvero ben ottimizzati?
Multihunter,

1
Sembra essere il risultato della funzione makeL che restituisce una stringa. Se si restituisce un elenco vuoto, invece, i risultati corrispondono all'incirca a quelli di hughdbrown di python2. Tranne il fatto che le coppie con nome sono sempre più lente di SlotObj :(
Multihunter

Potrebbe esserci un piccolo problema: makeL potrebbe funzionare con velocità diverse in ogni round di "@timeit" poiché le stringhe sono memorizzate nella cache in Python - ma forse mi sbaglio.
Barney,

@BarnabasSzabolcs dovrebbe creare una nuova stringa ogni volta perché deve sostituire il valore "Questa è una stringa di esempio% s"% i
Jarrod Chesney,

Sì, è vero all'interno del ciclo, ma nel secondo test ricomincio da 0.
Barney,

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

risultati:

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

Non ci sono domande
Hai dati, senza altri attributi (nessun metodo, niente). Quindi hai un contenitore di dati (in questo caso, un dizionario).

Di solito preferisco pensare in termini di modellazione dei dati . Se c'è un grosso problema di prestazioni, allora posso rinunciare a qualcosa nell'astrazione, ma solo con ottime ragioni.
La programmazione si basa sulla gestione della complessità e il mantenimento dell'astrazione corretta è molto spesso uno dei modi più utili per ottenere tale risultato.

Per quanto riguarda i motivi per cui un oggetto è più lento, penso che la tua misurazione non sia corretta.
Stai eseguendo compiti troppo piccoli all'interno del ciclo for, e quindi quello che vedi è il diverso tempo necessario per istanziare un dict (oggetto intrinseco) e un oggetto "personalizzato". Sebbene dal punto di vista linguistico siano uguali, hanno un'implementazione abbastanza diversa.
Successivamente, il tempo di assegnazione dovrebbe essere quasi lo stesso per entrambi, poiché alla fine i membri vengono mantenuti all'interno di un dizionario.


0

Esiste ancora un altro modo per ridurre l'utilizzo della memoria se la struttura dei dati non deve contenere cicli di riferimento.

Confrontiamo due classi:

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

e

$ 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

È diventato possibile da allora structclass classi basate su base non supportano la raccolta dei rifiuti ciclica, che non è necessaria in questi casi.

C'è anche un vantaggio sulla __slots__classe sopra- basata: puoi aggiungere ulteriori attributi:

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

Ecco le mie prove del bellissimo script di @ Jarrod-Chesney. Per fare un confronto, lo eseguo anche su python2 con "range" sostituito da "xrange".

Per curiosità, ho anche aggiunto test simili con OrderedDict (ordict) per il confronto.

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

Quindi, su entrambe le versioni principali, le conclusioni di @ Jarrod-Chesney sembrano ancora buone.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.