Python: ¿Puedes hacer que este __eq__ sea fácil de entender?
Pregunta
Tengo otra pregunta para ti.
Tengo una clase de python con una lista 'metainfo'. Esta lista contiene nombres de variables que mi clase podría contener. Escribí un método __eq__
que devuelve True si tanto self
como other
tienen las mismas variables de metainfo
y esas las variables tienen el mismo valor.
Aquí está mi implementación:
def __eq__(self, other):
for attr in self.metainfo:
try:
ours = getattr(self, attr)
try:
theirs = getattr(other, attr)
if ours != theirs:
return False
except AttributeError:
return False
except AttributeError:
try:
theirs = getattr(other, attr)
return False
except AttributeError:
pass
return True
¿Alguien tiene alguna sugerencia sobre cómo puedo hacer que este código sea más fácil a la vista? Sé tan despiadado como quieras.
Solución
Utilice getattr
's Tercer argumento para establecer distintos valores por defecto:
def __eq__(self, other):
return all(getattr(self, a, Ellipsis) == getattr(other, a, Ellipsis)
for a in self.metainfo)
Como valor predeterminado, establezca algo que nunca será un valor real, como Ellipsis
† . Por lo tanto, los valores solo coincidirán si ambos objetos contienen el mismo valor para un determinado atributo o si ambos no tienen dicho atributo.
Editar : como Nadia señala, NotImplemented
puede ser una constante más apropiada (a menos que esté almacenando el resultado de ricas comparaciones ...).
Edición 2: De hecho, como lo señala Lac , simplemente use hasattr
da como resultado una solución más legible:
def __eq__(self, other):
return all(hasattr(self, a) == hasattr(other, a) and
getattr(self, a) == getattr(other, a) for a in self.metainfo)
& nbsp; † : para mayor claridad, puede escribir ...
en lugar de Ellipsis
, por lo tanto, getattr (self, a, ...)
etc. No, no lo hagas :)
Otros consejos
Agregaría una cadena de documentos que explique lo que compara, como hiciste en tu pregunta.
def __eq__(self, other):
"""Returns True if both instances have the same variables from metainfo
and they have the same values."""
for attr in self.metainfo:
if attr in self.__dict__:
if attr not in other.__dict__:
return False
if getattr(self, attr) != getattr(other, attr):
return False
continue
else:
if attr in other.__dict__:
return False
return True
Ya que está a punto de facilitar su comprensión, no es corto ni muy rápido:
class Test(object):
def __init__(self):
self.metainfo = ["foo", "bar"]
# adding a docstring helps a lot
# adding a doctest even more : you have an example and a unit test
# at the same time ! (so I know this snippet works :-))
def __eq__(self, other):
"""
This method check instances equality and returns True if both of
the instances have the same attributs with the same values.
However, the check is performed only on the attributs whose name
are listed in self.metainfo.
E.G :
>>> t1 = Test()
>>> t2 = Test()
>>> print t1 == t2
True
>>> t1.foo = True
>>> print t1 == t2
False
>>> t2.foo = True
>>> t2.bar = 1
>>> print t1 == t2
False
>>> t1.bar = 1
>>> print t1 == t2
True
>>> t1.new_value = "test"
>>> print t1 == t2
True
>>> t1.metainfo.append("new_value")
>>> print t1 == t2
False
"""
# Then, let's keep the code simple. After all, you are just
# comparing lists :
self_metainfo_val = [getattr(self, info, Ellipsis)
for info in self.metainfo]
other_metainfo_val = [getattr(other, info, Ellipsis)
for info in self.metainfo]
return self_metainfo_val == other_metainfo_val
Ir con " Plano es mejor que anidado " Quitaría las declaraciones de prueba anidadas. En su lugar, getattr debería devolver un centinela que solo sea igual a sí mismo. A diferencia de Stephan202, sin embargo, prefiero mantener el bucle for. También crearía un centinela por mí mismo y no reutilizar algún objeto de Python existente. Esto garantiza que no hay falsos positivos, incluso en las situaciones más exóticas.
def __eq__(self, other):
if set(metainfo) != set(other.metainfo):
# if the meta info differs, then assume the items differ.
# alternatively, define how differences should be handled
# (e.g. by using the intersection or the union of both metainfos)
# and use that to iterate over
return False
sentinel = object() # sentinel == sentinel <=> sentinel is sentinel
for attr in self.metainfo:
if getattr(self, attr, sentinel) != getattr(other, attr, sentinel):
return False
return True
Además, el método debe tener una cadena de documentos que explique su comportamiento eq ; Lo mismo ocurre con la clase que debe tener una cadena de documentos que explique el uso del atributo metainfo.
Finalmente, una prueba de unidad para este comportamiento de igualdad también debería estar presente. Algunos casos de prueba interesantes serían:
- Objetos que tienen el mismo contenido para todos los atributos de metainfo, pero contenido diferente para algunos otros atributos (= > son iguales)
- Si es necesario, verifique la conmutatividad de iguales, es decir, si a == b: b == a
- Objetos que no tienen ninguno de los atributos de metainfo establecidos
Yo dividiría la lógica en partes separadas que sean más fáciles de entender, cada una verificando una condición diferente (y cada una asumiendo que se verificó lo anterior). Más fácil solo para mostrar el código:
# First, check if we have the same list of variables.
my_vars = [var for var in self.metainf if hasattr(self, var)]
other_vars = [var for var in other.metainf if hasattr(other, var)]
if my_vars.sorted() != other_vars.sorted():
return False # Don't even have the same variables.
# Now, check each variable:
for var in my_vars:
if self.var != other.var:
return False # We found a variable with a different value.
# We're here, which means we haven't found any problems!
return True
Editar: no entendí la pregunta, aquí hay una versión actualizada. Sigo pensando que esta es una forma clara de escribir este tipo de lógica, pero es más fea de lo que pretendía y no es del todo eficiente, por lo que en este caso probablemente vaya con una solución diferente.
El try / excepts hace que tu código sea más difícil de leer. Usaría getattr con un valor predeterminado que se garantiza que no estará allí de otra manera. En el código de abajo acabo de hacer un objeto temporal. De esa manera, si el objeto no tiene un valor dado, ambos devolverán " NOT_PRESENT " y así contar como ser igual.
def __eq__(self, other):
NOT_PRESENT = object()
for attr in self.metainfo:
ours = getattr(self, attr, NOT_PRESENT)
theirs = getattr(other, attr, NOT_PRESENT)
if ours != theirs:
return False
return True
Aquí hay una variante que es bastante fácil de leer IMO, sin usar objetos centinela. Primero comparará si ambos tienen o no tiene el atributo, luego comparen los valores.
Se podría hacer en una línea usando all () y una expresión generadora como lo hizo Stephen, pero creo que esto es más legible.
def __eq__(self, other):
for a in self.metainfo:
if hasattr(self, a) != hasattr(other, a):
return False
if getattr(self, a, None) != getattr(other, a, None):
return False
return True
Me gusta la respuesta de Stephan202, pero creo que su código no hace que las condiciones de igualdad sean lo suficientemente claras. Aquí está mi opinión sobre él:
def __eq__(self, other):
wehave = [attr for attr in self.metainfo if hasattr(self, attr)]
theyhave = [attr for attr in self.metainfo if hasattr(other, attr)]
if wehave != theyhave:
return False
return all(getattr(self, attr) == getattr(other, attr) for attr in wehave)