Объект произвольного типа как ключ словаря

190

Что мне нужно сделать, чтобы использовать мои объекты настраиваемого типа в качестве ключей в словаре Python (где я не хочу, чтобы «идентификатор объекта» действовал как ключ), например

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

Я бы хотел использовать MyThing в качестве ключей, которые считаются одинаковыми, если имя и местоположение совпадают. В C # / Java я привык переопределять и предоставлять метод equals и hashcode, и обещаю не изменять ничего, от чего зависит хэш-код.

Что мне нужно сделать в Python для этого? Я должен даже?

(В простом случае, как здесь, возможно, было бы лучше просто поместить кортеж (имя, местоположение) в качестве ключа, но подумайте, я бы хотел, чтобы ключ был объектом)

Аноним
источник
Что плохого в использовании хеша?
Rafe Kettler
5
Вероятно, потому, что он хочет MyThing, чтобы два , если у них одинаковые nameи location, чтобы индексировать словарь, возвращали одно и то же значение, даже если они были созданы отдельно как два разных «объекта».
Санта
1
"возможно, было бы лучше просто поместить кортеж (имя, местоположение) в качестве ключа - но подумайте, я бы хотел, чтобы ключ был объектом)" Вы имеете в виду: НЕКОМПОНЕНТНЫЙ объект?
eyquem 04

Ответы:

231

Вам нужно добавить 2 метода , обратите внимание __hash__и __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)

Документация Python dict определяет эти требования к ключевым объектам, т.е. они должны быть хешируемыми .

6502
источник
18
hash(self.name)выглядит лучше, чем self.name.__hash__(), и если вы это сделаете, вы можете hash((x, y))избежать XORing самостоятельно.
Рош Оксюморон
5
В качестве дополнительного примечания, я только что обнаружил, что такой вызов x.__hash__()тоже неправильный , потому что он может давать неверные результаты: pastebin.com/C9fSH7eF
Rosh Oxymoron
@ Рош Оксюморон: спасибо за комментарий. При написании я использовал явное выражение andfor, __eq__но потом подумал: «Почему бы не использовать кортежи?» потому что я и так часто делаю это (думаю, это более читабельно). Однако по какой-то странной причине мои глаза не возвращались к вопросу __hash__.
6502
1
@ user877329: вы пытаетесь использовать какую-то структуру данных блендера в качестве ключей? По-видимому, из некоторых репозиториев определенные объекты требуют, чтобы вы сначала «заморозили» их, чтобы избежать изменчивости (изменение объекта на основе значений, который использовался в качестве ключа в словаре Python, не допускается)
6502
1
@ kawing-цзю pythonfiddle.com/eq-method-needs-ne-method <- это показывает «ошибка» в Python 2. Python 3 не имеет этой проблемы : по умолчанию __ne__()была «фиксированной» .
Bob Stein
35

Альтернативой в Python 2.6 или выше является использование collections.namedtuple()- это избавляет вас от написания каких-либо специальных методов:

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
Свен Марнах
источник
20

Вы переопределяете, __hash__если вам нужна особая хеш-семантика, и / __cmp__или __eq__чтобы сделать ваш класс пригодным для использования в качестве ключа. Объекты, которые сравнивают равные, должны иметь одинаковое хеш-значение.

Python ожидает __hash__вернуть целое число, возвращать Banana()не рекомендуется :)

Как вы заметили, определенные пользователем классы __hash__по умолчанию имеют этот вызов id(self).

Существует несколько дополнительных советов от документации :.

Классы, которые наследуют __hash__() метод от родительского класса, но изменяют значение __cmp__()или так __eq__() , что возвращаемое хеш-значение больше не подходит (например, путем переключения на концепцию равенства на основе значений вместо равенства на основе идентичности по умолчанию) могут явно помечать себя как быть нехэшируемым, установленным __hash__ = None в определении класса. Это означает, что экземпляры класса не только будут вызывать соответствующий TypeError, когда программа пытается получить их хеш-значение, но они также будут правильно идентифицированы как нехешируемые при проверке isinstance(obj, collections.Hashable) (в отличие от классов, которые определяют свои собственные, __hash__()чтобы явно вызывать TypeError).

Скурмедель
источник
2
Одного хеша недостаточно, дополнительно нужно либо переопределить, __eq__либо __cmp__.
Обен Сонне
@Oben Sonne: __cmp__предоставляется Python, если это определенный пользователем класс, но вы, вероятно, все равно захотите переопределить их, чтобы приспособиться к новой семантике.
Skurmedel
1
@Skurmedel: Да, но хотя вы можете вызывать cmpи использовать =классы пользователей, которые не переопределяют эти методы, один из них должен быть реализован, чтобы удовлетворить требование спрашивающего, чтобы экземпляры с аналогичным именем и расположением имели одинаковый ключ словаря.
Обен Сонне