Objeto de tipo personalizado como clave de diccionario

185

¿Qué debo hacer para usar mis objetos de un tipo personalizado como claves en un diccionario de Python (donde no quiero que el "ID del objeto" actúe como la clave), por ejemplo?

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

Me gustaría usar MyThing como claves que se consideran iguales si el nombre y la ubicación son los mismos. Desde C # / Java estoy acostumbrado a tener que anular y proporcionar un método igual y de código hash, y prometo no mutar nada de lo que depende el código hash.

¿Qué debo hacer en Python para lograr esto? ¿Debo incluso?

(En un caso simple, como aquí, tal vez sería mejor simplemente colocar una tupla (nombre, ubicación) como clave, pero tenga en cuenta que me gustaría que la clave sea un objeto)

Seudónimo
fuente
¿Qué hay de malo en usar el hash?
Rafe Kettler
55
Probablemente porque quiere dos MyThing, si tienen el mismo namey location, para indexar el diccionario para que devuelva el mismo valor, incluso si se crearon por separado como dos "objetos" diferentes.
Santa
1
"tal vez sería mejor colocar una tupla (nombre, ubicación) como clave, pero considere que me gustaría que la clave sea un objeto)" ¿Quiere decir: un objeto NO COMPUESTO?
eyquem

Respuestas:

221

Necesita agregar 2 métodos , nota __hash__y __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)

La documentación de dict de Python define estos requisitos en los objetos clave, es decir, deben ser hashaable .

6502
fuente
17
hash(self.name)se ve mejor que self.name.__hash__(), y si lo haces y puedes hacerlo hash((x, y))para evitar XORing a ti mismo.
Rosh Oxymoron
55
Como nota adicional, acabo de descubrir que llamar x.__hash__()así también está mal , porque puede producir resultados incorrectos : pastebin.com/C9fSH7eF
Rosh Oxymoron
@Rosh Oxymoron: gracias por el comentario. Al escribir yo estaba usando explícita andpara __eq__pero luego pensé "¿por qué no usar tuplas?" porque a menudo hago eso de todos modos (creo que es más legible). __hash__Sin embargo, por alguna extraña razón, mis ojos no volvieron a preguntar .
6502
1
@ user877329: ¿está intentando utilizar alguna estructura de datos de Blender como claves? Aparentemente, en algunos repositorios, ciertos objetos requieren que los "congele" primero para evitar la mutabilidad (no está permitido mutar un objeto basado en valores que se haya utilizado como clave en un diccionario de Python)
6502
1
@ kawing-chiu pythonfiddle.com/eq-method-needs-ne-method <- esto muestra el "error" en Python 2. Python 3 no tiene este problema : el valor predeterminado __ne__()ha sido "arreglado" .
Bob Stein
34

Se debe utilizar una alternativa en Python 2.6 o superior collections.namedtuple(): le ahorra escribir cualquier método especial:

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
fuente
20

Se anula __hash__si desea una semántica hash especial, __cmp__o __eq__para que su clase sea utilizable como clave. Los objetos que comparan igual deben tener el mismo valor hash.

Python espera __hash__devolver un número entero, Banana()no se recomienda regresar :)

Las clases definidas por el usuario tienen __hash__por defecto que las llamadas id(self), como usted notó.

Hay algunos consejos adicionales de la documentación :

Las clases que heredan un __hash__() método de una clase padre pero cambian el significado de __cmp__()o __eq__() tal que el valor hash devuelto ya no es apropiado (por ejemplo, al cambiar a un concepto de igualdad basado en el valor en lugar de la igualdad basada en la identidad predeterminada) pueden marcarse explícitamente como ser inconfundible estableciendo __hash__ = None en la definición de clase. Hacerlo significa que no solo las instancias de la clase generarán un TypeError apropiado cuando un programa intente recuperar su valor hash, sino que también se identificarán correctamente como no compartibles cuando se verifiquen isinstance(obj, collections.Hashable) (a diferencia de las clases que definen las suyas __hash__()para aumentar explícitamente TypeError).

Skurmedel
fuente
2
El hash por sí solo no es suficiente, además, debe anular __eq__o __cmp__.
Oben Sonne
@Oben Sonne: __cmp__Python te lo da si es una clase definida por el usuario, pero probablemente quieras anularlos de todos modos para acomodar la nueva semántica.
Skurmedel
1
@Skurmedel: Sí, pero aunque puede llamar cmpy usar =en clases de usuario que no anulan estos métodos, uno de ellos debe implementarse para cumplir con el requisito del interrogador de que las instancias con nombre y ubicación similares tengan la misma clave de diccionario.
Oben Sonne