Python: Você pode fazer essa __eq__ fácil de entender?
Pergunta
Eu tenho uma outra pergunta para você.
Eu tenho uma classe python com uma lista 'metainfo'. Esta lista contém nomes de variáveis ??que minha classe força contêm. Eu escrevi um método __eq__
que retorna True se a ambos self
e other
têm as mesmas variáveis ??de metainfo
e essas variáveis ??têm o mesmo valor.
Aqui está minha implementação:
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
Alguém tem alguma sugestão de como eu posso fazer este código mais fácil no olho? Seja tão implacável quanto quiser.
Solução
Use getattr
's terceiro argumento para definir valores padrão distintas :
def __eq__(self, other):
return all(getattr(self, a, Ellipsis) == getattr(other, a, Ellipsis)
for a in self.metainfo)
Como o valor padrão, defina algo que nunca será um valor real, como Ellipsis
† . Assim, os valores irão corresponder somente se ambos os objetos contêm o mesmo valor para um determinado atributo ou , se ambos não disseram atributo.
Editar : como Nadia aponta, NotImplemented
pode ser uma constante mais apropriado (a menos que você está armazenando o resultado de comparações ricos ... ).
Editar 2: Na verdade, como Lac aponta, apenas usando hasattr
resulta em uma solução mais legível:
def __eq__(self, other):
return all(hasattr(self, a) == hasattr(other, a) and
getattr(self, a) == getattr(other, a) for a in self.metainfo)
† : para a obscuridade extra que você poderia escrever ...
vez de Ellipsis
, assim getattr(self, a, ...)
etc. Não, não faça isso:)
Outras dicas
Eu gostaria de acrescentar uma docstring que explica o que compara, como você fez na sua pergunta.
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
Desde que está prestes a tornar mais fácil de entender, não a curto ou muito 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
Indo com "Flat é melhor que nested" eu iria remover as instruções try aninhadas. Em vez disso, getattr deve retornar uma sentinela que só é igual a si mesmo. Ao contrário Stephan202, no entanto, eu prefiro manter o loop for. Eu também iria criar uma sentinela por mim, e não re-utilizar algum objeto Python existente. Isso garante que não há falsos positivos, mesmo nas situações mais exóticos.
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
Além disso, o método deve ter um doc-string explicá-lo de eq comportamento; mesmo vale para a classe que deve ter uma docstring explicando o uso do atributo metainfo.
Finalmente, um teste de unidade para essa igualdade-comportamento deve estar presente também. Alguns casos de teste interessante seria:
- Os objetos que têm o mesmo conteúdo para todos os metainfo-atributos, mas conteúdo diferente para alguns outros atributos (=> eles são iguais)
- Se necessário, a verificação de comutatividade de iguais, ou seja, se a == b: b == um
- Os objetos que não têm qualquer um dos metainfo-conjunto de atributos
eu iria quebrar a lógica em pedaços separados que são mais fáceis de compreender, cada um verificando uma condição diferente (e cada um assumindo o anterior foi marcada). Mais fácil apenas para mostrar o 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: eu não entendi a pergunta, aqui está uma versão atualizada. Eu ainda acho que essa é uma forma clara de escrever este tipo de lógica, mas é mais feio do que eu pretendia e não em todos eficiente, portanto, neste caso, eu provavelmente vá com uma solução diferente.
Os try / excepts tornar seu código mais difícil de ler. Eu usaria getattr com um valor padrão que é garantido que não de outra forma estar lá. No código abaixo eu só fazer um objeto temporário. Dessa forma, se o objeto não tem um determinado valor que vai tanto retorno "NOT_PRESENT" e, portanto, contam como sendo iguais.
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
Aqui está uma variante que é muito fácil de ler IMO, sem o uso de objetos sentinela. Ele primeiro comparar se ambos tem ou has not o atributo, e depois comparar os valores.
Pode ser feito em uma linha usando tudo () e um gerador de expressão como Stephen fiz, mas eu sinto que este é mais legível.
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
Eu gosto de resposta de Stephan202, mas acho que seu código não tornar as condições de igualdade bastante clara. Aqui está a minha opinião sobre ele:
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)