web-dev-qa-db-fra.com

Dictionnaire vs objet - qui est plus efficace et pourquoi?

Quoi de plus efficace en Python en termes d’utilisation de mémoire et de consommation d’UC - Dictionnaire ou Objet?

Background: Je dois charger une énorme quantité de données dans Python. J'ai créé un objet qui n'est qu'un conteneur de champ. Créer des instances 4M et les mettre dans un dictionnaire prenait environ 10 minutes et environ 6 Go de mémoire. Une fois que le dictionnaire est prêt, il est facile d'y accéder.

Exemple: Pour vérifier les performances, j’ai écrit deux programmes simples qui font la même chose - l’un utilise des objets, l’autre dictionnaire:

Objet (temps d'exécution ~ 18sec):

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

Dictionnaire (temps d'exécution ~ 12sec):

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

Question: Est-ce que je fais quelque chose de mal ou le dictionnaire est juste plus rapide que l'objet? Si le dictionnaire fonctionne mieux, quelqu'un peut-il expliquer pourquoi?

106
tkokoszka

Avez-vous essayé d'utiliser __slots__?

De la documentation :

Par défaut, les instances des classes de style ancien et nouveau ont un dictionnaire pour le stockage d'attributs. Cela gaspille de l'espace pour des objets ayant très peu de variables d'instance. La consommation d'espace peut devenir aiguë lors de la création d'un grand nombre d'instances.

La valeur par défaut peut être remplacée en définissant __slots__ dans une définition de classe d'un nouveau style. La déclaration __slots__ prend une séquence de variables d'instance et réserve juste assez d'espace dans chaque instance pour contenir une valeur pour chaque variable. L'espace est enregistré car __dict__ n'est pas créé pour chaque instance.

Alors, est-ce que cela fait gagner du temps et de la mémoire?

En comparant les trois approches sur mon ordinateur:

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 (pris en charge en 2.6):

import collections

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

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

Exécuter un benchmark (à l'aide de 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__)

Avec CPython 2.6.2, y compris le test de tuple nommé:

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

Donc oui (pas vraiment une surprise), utiliser __slots__ est une optimisation des performances. L'utilisation d'un tuple nommé a des performances similaires à __slots__.

137
codeape

L'accès aux attributs dans un objet utilise l'accès au dictionnaire en coulisse. Ainsi, en utilisant l'accès aux attributs, vous ajoutez une surcharge supplémentaire. De plus, dans le cas des objets, vous induisez des frais généraux supplémentaires, par exemple des allocations de mémoire supplémentaires et l'exécution de code (par exemple, de la méthode __init__).

Dans votre code, si o est une instance Obj, o.attr équivaut à o.__dict__['attr'] avec une petite quantité de temps système supplémentaire.

14
Vinay Sajip

Avez-vous envisagé d'utiliser un namedtuple ? ( lien pour python 2.4/2.5 )

C'est la nouvelle manière standard de représenter des données structurées qui vous donne les performances d'un tuple et la commodité d'une classe.

Le seul inconvénient, comparé aux dictionnaires, est que (comme les tuples), cela ne vous donne pas la possibilité de changer les attributs après la création.

8
John Fouhy
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 __== '__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()

Résultats:

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
hughdbrown

Voici une copie de @hughdbrown answer pour python 3.6.1. J'ai multiplié le nombre par cinq et ajouté du code pour tester l'empreinte mémoire du processus python à la fin de chaque exécution.

Avant que les électeurs n’arrivent à la fin, sachez que cette méthode de comptage de la taille des objets n’est pas précise.

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

Et ce sont mes résultats

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

Ma conclusion est:

  1. Les machines à sous ont la meilleure empreinte mémoire et leur vitesse est raisonnable.
  2. les dict sont les plus rapides, mais utilisent le plus de mémoire.
3
Jarrod Chesney

Il n'y a aucune question.
Vous avez des données, sans autres attributs (pas de méthodes, rien). Vous avez donc un conteneur de données (dans ce cas, un dictionnaire).

Je préfère généralement penser en termes de modélisation de données . S'il y a un problème de performance énorme, je peux alors renoncer à quelque chose dans l'abstraction, mais seulement avec de très bonnes raisons.
La programmation consiste uniquement à gérer la complexité et le maintien de la abstraction correcte est très souvent l’un des moyens les plus utiles d’atteindre ce résultat.

À propos des raisons un objet est plus lent, je pense que votre mesure est incorrecte.
Vous effectuez trop peu d’assignations dans la boucle for et vous voyez donc le temps nécessaire pour instancier un dict (objet intrinsèque) et un objet "personnalisé". Bien que, du point de vue linguistique, ils soient identiques, leur mise en œuvre est très différente.
Après cela, le temps d’attribution devrait être presque le même pour les deux, car à la fin, les membres sont conservés dans un dictionnaire.

2
rob

Il existe un autre moyen de réduire l'utilisation de la mémoire si la structure de données n'est pas censée contenir des cycles de référence.

Comparons deux classes:

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

et

$ 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

Cela est devenu possible car les classes basées sur structclass ne prennent pas en charge la récupération de place cyclique, ce qui n'est pas nécessaire dans de tels cas.

Il existe également un avantage par rapport à la classe basée sur __slots__: vous pouvez ajouter des attributs supplémentaires:

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