¿Qué hace que una clase definida por el usuario sea inutilizable?


Los documentos dicen que una clase es hashable siempre y cuando defina el método __hash__ y el método __eq__. Sin embargo:

class X(list):
  # read-only interface of `tuple` and `list` should be the same, so reuse tuple.__hash__
  __hash__ = tuple.__hash__

x1 = X()
s = {x1} # TypeError: unhashable type: 'X'

¿Qué hace que X sea inhashable?

Tenga en cuenta que debo tener listas idénticas (en términos de igualdad regular) para ser hash con el mismo valor; de lo contrario, violaré este requisito en las funciones hash:

La única propiedad requerida es que los objetos que comparan equal tienen el mismo valor hash

Los documentos advierten que un objeto hashable no debería ser modificado durante su vida útil, y por supuesto no modifico instancias de X después de la creación. Por supuesto, el intérprete no comprobará eso de todos modos.

Author: max, 2012-04-21

4 answers

Simplemente establecer el método __hash__ al de la clase tuple no es suficiente. En realidad no le has dicho cómo hacer hash de manera diferente. las tuplas son hashable porque son inmutables. Si realmente quieres hacer que tu ejemplo específico funcione, podría ser así:

class X2(list):
    def __hash__(self):
        return hash(tuple(self))

En este caso, en realidad está definiendo cómo hash su subclase de lista personalizada. Solo tienes que definir exactamente cómo puede generar un hash. Puedes hacer hash en lo que quieras, en lugar de usar el hash de la tupla método:

def __hash__(self):
    return hash("foobar"*len(self))
 16
Author: jdi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-04-20 23:14:48

De los documentos Python3:

Si una clase no define un método __eq__() no debe definir un __hash__() operación; si se define __eq__ (), pero no __hash__(), sus instancias no podrán ser utilizados como elementos de hashable colecciones. Si una clase define objetos mutables e implementa un __eq__() método, no debe implementar __hash__(), ya que la ejecución de hashable colecciones requiere que un hash de la clave el valor es inmutable (si el valor hash del objeto cambios, será en el cubo de hachís equivocado).

Ref: objeto.__ hash _ _ (self)

Código de ejemplo:

class Hashable:
    pass

class Unhashable:
    def __eq__(self, other):
        return (self == other)

class HashableAgain:
    def __eq__(self, other):
        return (self == other)

    def __hash__(self):
        return id(self)

def main():
    # OK
    print(hash(Hashable()))
    # Throws: TypeError("unhashable type: 'X'",)
    print(hash(Unhashable()))  
    # OK
    print(hash(HashableAgain()))
 7
Author: kevinarpe,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-04-03 14:29:58

Lo que podría y debería hacer, basado en su otra pregunta, es: no subclases nada, solo encapsula una tupla. Está perfectamente bien hacerlo en el init.

class X(object):
    def __init__(self, *args):
        self.tpl = args
    def __hash__(self):
        return hash(self.tpl)
    def __eq__(self, other):
        return self.tpl == other
    def __repr__(self):
        return repr(self.tpl)

x1 = X()
s = {x1}

Que produce:

>>> s
set([()])
>>> x1
()
 5
Author: ch3ka,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-04-20 23:33:33

Si no modifica las instancias de X después de la creación, ¿por qué no está subclasificando la tupla?

Pero señalaré que esto en realidad no arroja un error, al menos en Python 2.6.

>>> class X(list):
...     __hash__ = tuple.__hash__
...     __eq__ = tuple.__eq__
... 
>>> x = X()
>>> s = set((x,))
>>> s
set([[]])

Dudo en decir "funciona" porque esto no hace lo que crees que hace.

>>> a = X()
>>> b = X((5,))
>>> hash(a)
4299954584
>>> hash(b)
4299954672
>>> id(a)
4299954584
>>> id(b)
4299954672

Solo está usando el id del objeto como un hash. Cuando realmente llamas __hash__ todavía obtienes un error; del mismo modo para __eq__.

>>> a.__hash__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: descriptor '__hash__' for 'tuple' objects doesn't apply to 'X' object
>>> X().__eq__(X())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: descriptor '__eq__' for 'tuple' objects doesn't apply to 'X' object

Deduzco que los internos de python, para algunos razón, están detectando que X tiene un __hash__ y un método __eq__, pero no los están llamando.

La moraleja de todo esto es: simplemente escribe una función hash real. Dado que este es un objeto de secuencia, convertirlo en una tupla y hash es el enfoque más obvio.

def __hash__(self):
    return hash(tuple(self))
 3
Author: senderle,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-04-20 23:24:40