¿Debería usar una clase o un diccionario?

99

Tengo una clase que contiene solo campos y no métodos, como este:

class Request(object):

    def __init__(self, environ):
        self.environ = environ
        self.request_method = environ.get('REQUEST_METHOD', None)
        self.url_scheme = environ.get('wsgi.url_scheme', None)
        self.request_uri = wsgiref.util.request_uri(environ)
        self.path = environ.get('PATH_INFO', None)
        # ...

Esto podría traducirse fácilmente a un dict. La clase es más flexible para futuras adiciones y podría ser más rápida __slots__. Entonces, ¿sería beneficioso usar un dictado en su lugar? ¿Un dictado sería más rápido que una clase? ¿Y más rápido que una clase con tragamonedas?

demonio
fuente
2
Siempre uso diccionarios para guardar datos, y esto me parece un caso de uso. en algunos casos, derivar una clase de dictpuede tener sentido, a. gran ventaja: cuando depure, solo diga print(request)y verá fácilmente toda la información del estado. con el enfoque más clásico, tendrá que escribir sus __str__métodos personalizados , lo que apesta si tiene que hacerlo todo el tiempo.
flujo
Son intercambiables. Stackoverflow.com/questions/1305532/…
Oleksiy
Si la clase tiene total sentido y es clara para los demás, ¿por qué no? Además, si define muchas clases con interfaces comunes, por ejemplo, ¿por qué no? Pero Python no admite conceptos orientados a objetos poderosos como C ++.
MasterControlProgram
3
@Ralf ¿Qué OOP no admite Python?
qwr

Respuestas:

31

¿Por qué harías de esto un diccionario? Cual es la ventaja? ¿Qué sucede si luego desea agregar algún código? ¿A dónde iría tu __init__código?

Las clases son para agrupar datos relacionados (y generalmente código).

Los diccionarios sirven para almacenar relaciones clave-valor, donde generalmente las claves son todas del mismo tipo y todos los valores también son de un tipo. Ocasionalmente, pueden ser útiles para agrupar datos cuando los nombres de clave / atributo no se conocen por adelantado, pero a menudo esto es una señal de que algo anda mal con su diseño.

Mantenga esto como una clase.

adw
fuente
Crearía una especie de método de fábrica creando un dictado en lugar del __init__método de clase . Pero tienes razón: me separaría de las cosas que van juntas.
demonio
88
No podría estar más en desacuerdo contigo: diccionarios, conjuntos, listas y tuplas están ahí para agrupar datos relacionados. De ninguna manera se supone que los valores de un diccionario deban o deban ser del mismo tipo de datos, al contrario. en muchas listas y conjuntos, los valores tendrán el mismo tipo, pero eso se debe principalmente a que eso es lo que nos gusta enumerar juntos. de hecho, creo que el uso generalizado de clases para mantener datos es un abuso de oop; cuando piensa en problemas de serialización, puede ver fácilmente por qué.
flujo
4
Es programación orientada a OBJETOS y no a clases por una razón: tratamos con objetos. Un objeto se caracteriza por 2 (3) propiedades: 1. estado (miembros) 2. comportamiento (métodos) y 3. instancia se puede describir en varias palabras. Por lo tanto, las clases sirven para agrupar el estado y el comportamiento.
friendzis
14
He marcado esto porque siempre debe usar la estructura de datos más simple cuando sea posible. En este caso, un diccionario es suficiente para el propósito previsto. La pregunta where would your __init__ code go?es preocupante. Podría persuadir a un desarrollador con menos experiencia de que solo se deben usar clases, ya que un método init no se usa en un diccionario. Absurdo.
Lloyd Moore
1
@Ralf Una clase Foo es simplemente un tipo, como int y string. ¿Almacena valor en un tipo entero o variable foo de tipo entero? Diferencia semántica sutil, pero importante. En lenguajes como C, esta distinción no es demasiado relevante fuera de los círculos académicos. Aunque la mayoría de los lenguajes OO tienen soporte para variables de clase, lo que hace que la distinción entre clase y objeto / instancia sea enormemente relevante: ¿almacena datos en clase (compartidos en todas las instancias) o en un objeto específico? No te
muerdan
44

Utilice un diccionario a menos que necesite el mecanismo adicional de una clase. También puede utilizar un namedtupleenfoque híbrido:

>>> from collections import namedtuple
>>> request = namedtuple("Request", "environ request_method url_scheme")
>>> request
<class '__main__.Request'>
>>> request.environ = "foo"
>>> request.environ
'foo'

Las diferencias de rendimiento aquí serán mínimas, aunque me sorprendería que el diccionario no fuera más rápido.

Katriel
fuente
15
"Las diferencias de rendimiento aquí serán mínimas , aunque me sorprendería que el diccionario no fuera significativamente más rápido". Esto no se computa. :)
mipadi
1
@mipadi: esto es cierto. Ahora arreglado: p
Katriel
El dict es 1,5 veces más rápido que el namedtuple y dos veces más rápido que una clase sin espacios. Revisa mi publicación sobre esta respuesta.
alexpinho98
@ alexpinho98: Me he esforzado mucho por encontrar la 'publicación' a la que te refieres, ¡pero no puedo encontrarla! puede proporcionar la URL. ¡Gracias!
Dan Oblinger
@DanOblinger Supongo que se refiere a su respuesta a continuación.
Adam Lewis
37

Una clase en Python es un dictado debajo. Obtienes algo de sobrecarga con el comportamiento de la clase, pero no podrás notarlo sin un generador de perfiles. En este caso, creo que te beneficias de la clase porque:

  • Toda tu lógica vive en una sola función
  • Es fácil de actualizar y permanece encapsulado.
  • Si cambia algo más tarde, puede mantener fácilmente la interfaz igual
Bruce Armstrong
fuente
Toda tu lógica no vive en una sola función. Una clase es una tupla de estado compartido y generalmente uno o más métodos. Si cambia una clase, no se le da ninguna garantía sobre su interfaz.
Lloyd Moore
25

Creo que el uso de cada uno es demasiado subjetivo para que pueda entrar en eso, así que me limitaré a los números.

Comparé el tiempo que lleva crear y cambiar una variable en un dict, una clase new_style y una clase new_style con ranuras.

Aquí está el código que utilicé para probarlo (está un poco desordenado pero funciona).

import timeit

class Foo(object):

    def __init__(self):

        self.foo1 = 'test'
        self.foo2 = 'test'
        self.foo3 = 'test'

def create_dict():

    foo_dict = {}
    foo_dict['foo1'] = 'test'
    foo_dict['foo2'] = 'test'
    foo_dict['foo3'] = 'test'

    return foo_dict

class Bar(object):
    __slots__ = ['foo1', 'foo2', 'foo3']

    def __init__(self):

        self.foo1 = 'test'
        self.foo2 = 'test'
        self.foo3 = 'test'

tmit = timeit.timeit

print 'Creating...\n'
print 'Dict: ' + str(tmit('create_dict()', 'from __main__ import create_dict'))
print 'Class: ' + str(tmit('Foo()', 'from __main__ import Foo'))
print 'Class with slots: ' + str(tmit('Bar()', 'from __main__ import Bar'))

print '\nChanging a variable...\n'

print 'Dict: ' + str((tmit('create_dict()[\'foo3\'] = "Changed"', 'from __main__ import create_dict') - tmit('create_dict()', 'from __main__ import create_dict')))
print 'Class: ' + str((tmit('Foo().foo3 = "Changed"', 'from __main__ import Foo') - tmit('Foo()', 'from __main__ import Foo')))
print 'Class with slots: ' + str((tmit('Bar().foo3 = "Changed"', 'from __main__ import Bar') - tmit('Bar()', 'from __main__ import Bar')))

Y aquí está la salida ...

Creando ...

Dict: 0.817466186345
Class: 1.60829183597
Class_with_slots: 1.28776730003

Cambiar una variable ...

Dict: 0.0735140918748
Class: 0.111714198313
Class_with_slots: 0.10618612142

Entonces, si solo está almacenando variables, necesita velocidad y no requerirá que haga muchos cálculos, le recomiendo usar un dictado (siempre puede hacer una función que parezca un método). Pero, si realmente necesita clases, recuerde: use siempre __ slots __ .

Nota:

Probé la 'Clase' con las clases new_style y old_style. Resulta que las clases old_style son más rápidas de crear pero más lentas de modificar (no mucho, pero sí significativas si estás creando muchas clases en un circuito cerrado (consejo: lo estás haciendo mal)).

Además, los tiempos para crear y cambiar variables pueden diferir en su computadora ya que la mía es vieja y lenta. Asegúrese de probarlo usted mismo para ver los resultados "reales".

Editar:

Más tarde probé la tupla con nombre: no puedo modificarla, pero para crear las 10000 muestras (o algo así) tomó 1,4 segundos, por lo que el diccionario es de hecho el más rápido.

Si cambio la función dict para incluir las claves y los valores y devolver el dict en lugar de la variable que contiene el dict cuando lo creo, me da 0,65 en lugar de 0,8 segundos.

class Foo(dict):
    pass

Crear es como una clase con ranuras y cambiar la variable es lo más lento (0.17 segundos), así que no use estas clases . ir por un dict (velocidad) o por la clase derivada del objeto ('syntax candy')

alexpinho98
fuente
Me gustaría ver los números de una subclase de dict(sin métodos anulados, ¿supongo?). ¿Funciona de la misma manera que una clase de estilo nuevo que se escribió desde cero?
Benjamin Hodgson
12

Estoy de acuerdo con @adw. Nunca representaría un "objeto" (en un sentido orientado a objetos) con un diccionario. Los diccionarios agregan pares nombre / valor. Las clases representan objetos. He visto código donde los objetos se representan con diccionarios y no está claro cuál es la forma real de la cosa. ¿Qué sucede cuando ciertos nombres / valores no están ahí? Lo que impide que el cliente introduzca algo en absoluto. O intente sacar algo. La forma de la cosa siempre debe estar claramente definida.

Al usar Python, es importante construir con disciplina ya que el lenguaje permite que el autor se pegue un tiro en el pie de muchas formas.

Jaydel
fuente
9
Las respuestas de SO se ordenan por votos y las respuestas con el mismo recuento de votos se ordenan al azar. Así que aclara a quién te refieres con "el último cartel".
Mike DeSimone
4
¿Y cómo sabe que algo que solo tiene campos y ninguna funcionalidad es un "objeto" en un sentido orientado a objetos?
Muhammad Alkarouri
1
Mi prueba de fuego es "¿la estructura de estos datos está fija?". En caso afirmativo, utilice un objeto; de lo contrario, un dict. Salir de esto solo creará confusión.
weberc2
Tienes que analizar el contexto del problema que estás resolviendo. Los objetos deberían ser relativamente obvios una vez que esté familiarizado con ese paisaje. Personalmente, creo que devolver un diccionario debería ser su último recurso, a menos que lo que devuelva DEBE ser un mapeo de pares de nombre / valor. He visto demasiado código descuidado en el que todo lo que pasa dentro y fuera de los métodos es solo un diccionario. Es perezoso. Me encantan los lenguajes de tipado dinámico, pero también me gusta que mi código sea claro y lógico. Poner todo en un diccionario puede ser una conveniencia que oculta el significado.
Jaydel
5

Recomendaría una clase, ya que se trata de todo tipo de información relacionada con una solicitud. Si uno usara un diccionario, esperaría que los datos almacenados fueran de naturaleza mucho más similar. Una pauta que suelo seguir es que si quiero recorrer todo el conjunto de pares clave-> valor y hacer algo, uso un diccionario. De lo contrario, los datos aparentemente tienen mucha más estructura que un mapeo básico de clave-> valor, lo que significa que una clase probablemente sería una mejor alternativa.

Por lo tanto, quédese con la clase.

Estigma
fuente
2
Estoy totalmente en desacuerdo. No hay razón para restringir el uso de diccionarios a cosas para iterar. Son para mantener un mapeo.
Katriel
1
Lea un poco más de cerca, por favor. Dije que puede querer hacer un bucle , no lo haré . Para mí, usar un diccionario implica que existe una gran similitud funcional entre las claves y los valores. Por ejemplo, nombres con edades. Usar una clase significaría que las distintas 'claves' tienen valores con significados muy diferentes. Por ejemplo, tome una clase de PErson. Aquí, 'nombre' sería una cadena y 'amigos' puede ser una lista, un diccionario o algún otro objeto adecuado. No recorrerías todos estos atributos como parte del uso normal de esta clase.
Stigma
1
Creo que la distinción entre clases y diccionarios no se distingue en Python por el hecho de que los primeros se implementan utilizando los segundos (no obstante, las 'ranuras'). Sé que esto me confundió un poco cuando estaba aprendiendo el idioma por primera vez (junto con el hecho de que las clases eran objetos y, por lo tanto, instancias de algunas metaclases misteriosas).
martineau
4

Si todo lo que desea lograr es sintaxis similar a dulces en obj.bla = 5lugar de obj['bla'] = 5, especialmente si tiene que repetirlo mucho, tal vez desee usar alguna clase de contenedor simple como en la sugerencia de martineaus. Sin embargo, el código es bastante inflado e innecesariamente lento. Puedes hacerlo simple así:

class AttrDict(dict):
    """ Syntax candy """
    __getattr__ = dict.__getitem__
    __setattr__ = dict.__setitem__
    __delattr__ = dict.__delitem__

Otra razón para cambiar a namedtupleso una clase con__slots__ podría ser el uso de memoria. Los dictados requieren mucha más memoria que los tipos de listas, por lo que este podría ser un punto en el que pensar.

De todos modos, en su caso específico, no parece haber ninguna motivación para alejarse de su implementación actual. No parece mantener millones de estos objetos, por lo que no se requieren tipos derivados de listas. Y en realidad contiene algo de lógica funcional dentro del __init__, por lo que tampoco deberías quedarte con AttrDict.

Miguel
fuente
types.SimpleNamespace(disponible desde Python 3.3) es una alternativa al AttrDict personalizado.
Cristian Ciupitu
4

Puede ser posible tener su pastel y comérselo también. En otras palabras, puede crear algo que proporcione la funcionalidad de una clase y una instancia de diccionario. Vea la receta Dɪᴄᴛɪᴏɴᴀʀʏ ᴡɪᴛʜ ᴀᴛᴛʀɪʙᴜᴛᴇ-sᴛʏʟᴇ ᴀᴄᴄᴇss de ActiveState y comentarios sobre las formas de hacerlo.

Si decides usar una clase normal en lugar de una subclase, he encontrado que la receta de Tʜᴇ sɪᴍᴘʟᴇ ʙᴜᴛ ʜᴀɴᴅʏ "ᴄᴏʟʟᴇᴄᴛᴏʀ ᴏғ ᴀ ʙᴜɴᴄʜ ᴏғ ɴᴀᴍᴇᴅ sᴛᴜғғ" ᴄʟᴀss (de Alex Martelli) es muy flexible y útil para el tipo de cosas que parece que lo estás haciendo (es decir, crea un agregador de información relativamente simple). Dado que es una clase, puede ampliar fácilmente su funcionalidad añadiendo métodos.

Por último, debe tenerse en cuenta que los nombres de los miembros de la clase deben ser identificadores legales de Python, pero las claves de diccionario no lo hacen, por lo que un diccionario proporcionaría una mayor libertad en ese sentido porque las claves pueden ser cualquier cosa que se pueda hash (incluso algo que no sea una cadena).

Actualizar

Una clase object(que no tiene una __dict__) subclase nombrada SimpleNamespace(que sí tiene una) se agregó al typesmódulo Python 3.3, y es otra alternativa.

martineau
fuente