Об'єкт користувацького типу як ключ словника


185

Що я повинен зробити, щоб використовувати свої об'єкти користувальницького типу як ключі в словнику Python (де я не хочу, щоб "ідентифікатор об'єкта" виконував роль ключа), наприклад

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

Я б хотів використовувати MyThing як ключі, які вважаються однаковими, якщо ім’я та місце розташування однакові. З C # / Java я звик переосмислювати та надавати метод рівних та хеш-кодів, і обіцяю не мутувати нічого, від чого залежить хеш-код.

Що я повинен зробити в Python для цього? Чи повинен я навіть?

(У простому випадку, як, наприклад, тут, можливо, було б краще просто вказати кордон (ім’я, місцезнаходження) як ключ - але врахуйте, що я хочу, щоб ключ був об’єктом)


Що не так у використанні хешу?
Рейф Кеттлер

5
Можливо, тому, що він хоче двох MyThing, якщо вони однакові, nameі location, щоб індексувати словник повертав однакове значення, навіть якщо вони були створені окремо, як два різних "об'єкти".
Санта-

1
"можливо, було б краще просто поставити кордон (ім'я, місцеположення) як ключовий - але подумайте, я б хотів, щоб ключ був об'єктом)" Ви маєте на увазі: об'єкт НЕ-КОМПОЗИТНИЙ?
ейкуем

Відповіді:


220

Вам потрібно додати 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 диктує документація визначає ці вимоги до ключових об'єктів, тобто вони повинні бути hashable .


17
hash(self.name)виглядає приємніше, ніж self.name.__hash__(), і якщо ви це зробите і можете зробити, hash((x, y))щоб уникнути XORing себе.
Рош Оксиморон

5
В якості додаткової примітки я щойно виявив, що x.__hash__()подібні дзвінки також є неправильними , оскільки це може призвести до неправильних результатів: pastebin.com/C9fSH7eF
Rosh Oxymoron

@Rosh Oxymoron: дякую за коментар. При написанні я використовував явно andдля , __eq__але потім я подумав : «чому не використовувати кортежі?» тому що я часто це роблю в будь-якому випадку (я думаю, що це читабельніше). З якоїсь дивної причини мої очі все ж не повернулися до питання __hash__.
6502 р.

1
@ user877329: чи намагаєтесь ви використовувати якусь структуру даних блендера як ключі? Очевидно, від деяких репостів певні об’єкти вимагають, щоб ви їх "заморозили" спочатку, щоб уникнути змін (мутація об'єкта на основі значень, який використовувався як ключ у словнику python, не дозволено)
6502,

1
@ kawing- chiu pythonfiddle.com/eq-method-needs-ne-method <- це показує "помилку" в Python 2. Python 3 не має цієї проблеми : за замовчуванням __ne__()було "виправлено" .
Боб Штейн

34

Альтернативою в 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та використовувати =на класах користувачів, які не перекривають ці методи, один із них повинен бути реалізований для задоволення вимоги запитувача про те, що екземпляри з подібним іменем та місцеположенням мають однаковий ключ словника.
Обен Сонне
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.