Objeto do tipo personalizado como chave do dicionário

185

O que devo fazer para usar meus objetos de um tipo personalizado como chaves em um dicionário Python (onde não quero que o "ID do objeto" atue como chave), por exemplo

class MyThing:
    def __init__(self,name,location,length):
            self.name = name
            self.location = location
            self.length = length

Eu gostaria de usar o MyThing como chaves que são consideradas iguais se o nome e o local forem iguais. No C # / Java, estou acostumado a substituir e fornecer um método equals e hashcode e prometer não alterar nada do qual o hashcode dependa.

O que devo fazer em Python para fazer isso? Eu deveria mesmo?

(Em um caso simples, como aqui, talvez seja melhor colocar uma tupla (nome, localização) como chave - mas considere que eu gostaria que a chave fosse um objeto)

Anonimato
fonte
O que há de errado em usar o hash?
Rafe Kettler 04/02
5
Provavelmente porque ele quer dois MyThing, se eles têm o mesmo namee location, para indexar o dicionário para retornar o mesmo valor, mesmo se eles foram criados separadamente como dois "objetos" diferentes.
Santa
1
"talvez seja melhor colocar uma tupla (nome, localização) como chave - mas considere que eu quero que a chave seja um objeto". Você quer dizer: um objeto NÃO COMPOSTO?
Eyquem

Respostas:

221

Você precisa adicionar 2 métodos , observação __hash__e __eq__:

class MyThing:
    def __init__(self,name,location,length):
        self.name = name
        self.location = location
        self.length = length

    def __hash__(self):
        return hash((self.name, self.location))

    def __eq__(self, other):
        return (self.name, self.location) == (other.name, other.location)

    def __ne__(self, other):
        # Not strictly necessary, but to avoid having both x==y and x!=y
        # True at the same time
        return not(self == other)

A documentação do dict do Python define esses requisitos nos objetos principais, ou seja, eles devem ser laváveis .

6502
fonte
17
hash(self.name)parece melhor do que self.name.__hash__(), e se você o fizer, poderá hash((x, y))evitar o XORing.
Rosh Oxymoron
5
Como uma nota adicional, eu só descobri que chamar x.__hash__()assim também é errado , porque ele pode produzir incorretas resultados: pastebin.com/C9fSH7eF
Rosh oxímoro
@Rosh Oxymoron: obrigado pelo comentário. Ao escrever, eu estava usando explícito andpara, __eq__mas depois pensei "por que não usar tuplas?" porque geralmente faço isso de qualquer maneira (acho que é mais legível). Por alguma estranha razão, meus olhos não voltaram a questionar __hash__.
6502
1
@ user877329: você está tentando usar alguma estrutura de dados do blender como chaves? Aparentemente, a partir de alguns repos certos objetos requerem que você "congelar"-los primeiro a mutabilidade evitar (mutação de um objeto baseado em valor que tem sido utilizado como uma chave em um dicionário python não é permitido)
6502
1
@ kawing-chiu pythonfiddle.com/eq-method-needs-ne-method <- mostra o "bug" no Python 2. O Python 3 não tem esse problema : o padrão __ne__()foi "corrigido" .
Bob Stein
34

Uma alternativa no Python 2.6 ou superior é usar collections.namedtuple()- você economiza escrevendo quaisquer métodos especiais:

from collections import namedtuple
MyThingBase = namedtuple("MyThingBase", ["name", "location"])
class MyThing(MyThingBase):
    def __new__(cls, name, location, length):
        obj = MyThingBase.__new__(cls, name, location)
        obj.length = length
        return obj

a = MyThing("a", "here", 10)
b = MyThing("a", "here", 20)
c = MyThing("c", "there", 10)
a == b
# True
hash(a) == hash(b)
# True
a == c
# False
Sven Marnach
fonte
20

Você substitui __hash__se deseja semântica hash especial e / __cmp__ou __eq__para tornar sua classe utilizável como chave. Objetos que comparam igual precisam ter o mesmo valor de hash.

Python espera __hash__retornar um número inteiro, Banana()não é recomendado retornar :)

As classes definidas __hash__pelo usuário têm como padrão as chamadas id(self), como você observou.

Existem algumas dicas extras na documentação .

Classes que herdam um __hash__() método de uma classe pai, mas alteram o significado de __cmp__()ou para __eq__() que o valor de hash retornado não seja mais apropriado (por exemplo, alternando para um conceito de igualdade baseado em valor em vez da igualdade baseada em identidade padrão) pode explicitamente se sinalizar como sendo lavável, definindo __hash__ = None na definição de classe. Fazer isso significa que não apenas as instâncias da classe gerarão um TypeError apropriado quando um programa tentar recuperar seu valor de hash, mas também serão identificadas corretamente como unhashable ao verificar isinstance(obj, collections.Hashable) (ao contrário das classes que se definem __hash__()para gerar explicitamente TypeError).

Skurmedel
fonte
2
O hash por si só não é suficiente; além disso, você precisa substituir __eq__ou __cmp__.
Oben Sonne
@ Oben Sonne: __cmp__é dado a você pelo Python se for uma classe definida pelo usuário, mas você provavelmente deseja substituí-los de qualquer maneira para acomodar novas semânticas.
Skurmedel
1
@ Skurmedel: Sim, mas embora você possa chamar cmpe usar =classes de usuários que não substituam esses métodos, um deles deve ser implementado para atender ao requisito do questionador de que instâncias com nome e local semelhantes tenham a mesma chave de dicionário.
Oben Sonne