str sentencias numeros nombres letras declarar convertir caracter asignaciones python performance dictionary object

python - sentencias - Diccionario vs Objeto: ¿cuál es más eficiente y por qué?



sentencias en python (6)

¿Qué es más eficiente en Python en términos de uso de memoria y consumo de CPU? ¿Diccionario u objeto?

Antecedentes: tengo que cargar gran cantidad de datos en Python. Creé un objeto que es solo un contenedor de campo. Crear instancias de 4M y ponerlas en un diccionario tomó alrededor de 10 minutos y ~ 6GB de memoria. Después de que el diccionario esté listo, acceder a él es un abrir y cerrar de ojos.

Ejemplo: Para verificar el rendimiento, escribí dos programas simples que hacen lo mismo: uno usa objetos, otro diccionario:

Objeto (tiempo de ejecución ~ 18seg):

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

Diccionario (tiempo de ejecución ~ 12seg):

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

Pregunta: ¿Estoy haciendo algo mal o el diccionario es más rápido que el objeto? Si el diccionario funciona mejor, ¿alguien puede explicar por qué?


¿Has considerado usar una namedtuple ? ( enlace para Python 2.4 / 2.5 )

Es la nueva forma estándar de representar datos estructurados que le proporciona el rendimiento de una tupla y la comodidad de una clase.

Es solo un inconveniente en comparación con los diccionarios, es que (al igual que las tuplas) no le da la capacidad de cambiar los atributos después de la creación.


¿Has intentado usar __slots__ ?

De la documentation :

De forma predeterminada, las instancias de clases antiguas y nuevas tienen un diccionario para el almacenamiento de atributos. Esto desperdicia espacio para objetos que tienen muy pocas variables de instancia. El consumo de espacio puede agudizarse cuando se crean grandes cantidades de instancias.

El valor predeterminado puede ser anulado definiendo __slots__ en una definición de clase de estilo nuevo. La declaración __slots__ toma una secuencia de variables de instancia y reserva el espacio suficiente en cada instancia para mantener un valor para cada variable. El espacio se guarda porque __dict__ no se crea para cada instancia.

Entonces, ¿esto ahorra tiempo y memoria?

Comparando los tres enfoques en mi computadora:

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 (compatible con 2.6):

import collections Obj = collections.namedtuple(''Obj'', ''i l'') all = {} for i in range(1000000): all[i] = Obj(i, [])

Ejecutar 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__)

Usando CPython 2.6.2, incluida la prueba de tupla nombrada:

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

Entonces sí (no es realmente una sorpresa), usar __slots__ es una optimización del rendimiento. Usar una tupla nombrada tiene un rendimiento similar a __slots__ .


Aquí hay una copia de la respuesta de @hughdbrown para python 3.6.1, hice que el conteo 5 veces más grande y agregué un código para probar la huella de memoria del proceso de python al final de cada ejecución.

Antes de que los downvoters lo hagan, tenga en cuenta que este método de contar el tamaño de los objetos no es exacto.

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

Y estos son mis resultados

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

Mi conclusión es:

  1. Las ranuras tienen la mejor huella de memoria y son razonables en velocidad.
  2. Los dicts son los más rápidos, pero usan la mayor cantidad de memoria.

El acceso de atributo en un objeto utiliza el acceso de diccionario detrás de escena, por lo que al usar el acceso de atributo está agregando sobrecarga adicional. Además, en el caso del objeto, está incurriendo en una sobrecarga adicional debido, por ejemplo, a asignaciones adicionales de memoria y ejecución de código (por ejemplo, del método __init__ ).

En su código, si o es una instancia de Obj , o.attr es equivalente a o.__dict__[''attr''] con una pequeña cantidad de sobrecarga adicional.


No hay pregunta.
Tienes datos, sin otros atributos (sin métodos, nada). Por lo tanto, tiene un contenedor de datos (en este caso, un diccionario).

Por lo general, prefiero pensar en términos de modelado de datos . Si hay un gran problema de rendimiento, entonces puedo renunciar a algo en la abstracción, pero solo con muy buenas razones.
La programación se trata de gestionar la complejidad, y el mantenimiento de la abstracción correcta suele ser una de las formas más útiles para lograr dicho resultado.

Acerca de las razones por las que un objeto es más lento, creo que su medición no es correcta.
Está realizando asignaciones muy pequeñas dentro del bucle for, y por lo tanto lo que ve allí es el tiempo diferente necesario para crear una instancia de un dict (objeto intrínseco) y un objeto "personalizado". Aunque desde la perspectiva del lenguaje son los mismos, tienen una implementación bastante diferente.
Después de eso, el tiempo de asignación debe ser casi el mismo para ambos, ya que al final los miembros se mantienen dentro de un diccionario.


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

Resultados:

hbrown@hbrown-lpt:~$ python ~/Dropbox/src//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