Obiekt typu niestandardowego jako klucz słownika


185

Co muszę zrobić, aby użyć obiektów niestandardowego typu jako kluczy w słowniku Pythona (gdzie nie chcę, aby „identyfikator obiektu” działał jako klucz), np.

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

Chciałbym użyć MyThing jako kluczy, które są uważane za takie same, jeśli nazwa i lokalizacja są takie same. Od C # / Java jestem przyzwyczajony do przesłonięcia i podania metody equals i hashcode, i obiecuję, że nie będę mutować niczego, od czego zależy hashcode.

Co muszę zrobić w Pythonie, aby to osiągnąć? Powinienem w ogóle?

(W prostym przypadku, tak jak tutaj, być może lepiej byłoby po prostu wstawić krotkę (nazwa, lokalizacja) jako klucz - ale weź pod uwagę, że chcę, aby klucz był obiektem)


Co jest złego w używaniu skrótu?
Rafe Kettler

5
Prawdopodobnie dlatego, że chce dwóch MyThing, jeśli mają takie same namei location, aby zaindeksować słownik, aby zwrócić tę samą wartość, nawet jeśli zostały utworzone osobno jako dwa różne „obiekty”.
Mikołaj

1
„być może lepiej byłoby po prostu wstawić krotkę (nazwa, lokalizacja) jako klucz - ale weź pod uwagę, że chciałbym, aby klucz był obiektem)„ Masz na myśli: obiekt NIEKOMPONOWANY?
eyquem

Odpowiedzi:


220

Musisz dodać 2 metody , uwagę __hash__i __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)

Dokumentacja języka Python definiuje te wymagania dotyczące kluczowych obiektów, tzn. Muszą być możliwe do skrócenia .


17
hash(self.name)wygląda ładniej niż self.name.__hash__(), a jeśli tak, to możesz zrobić, hash((x, y))aby uniknąć XOR.
Rosh Oxymoron

5
Jako dodatkową notatkę właśnie odkryłem, że x.__hash__()takie dzwonienie jest również nieprawidłowe , ponieważ może dawać niepoprawne wyniki: pastebin.com/C9fSH7eF
Rosh Oxymoron

@Rosh Oxymoron: dziękuję za komentarz. Pisząc używałem jawne anddla __eq__ale potem pomyślałem „dlaczego nie używając krotki?” ponieważ i tak często to robię (myślę, że jest to bardziej czytelne). Z jakiegoś dziwnego powodu moje oczy nie wróciły jednak do pytania __hash__.
6502

1
@ user877329: czy próbujesz użyć struktury danych blendera jako kluczy? Najwyraźniej z niektórych repozytoriów niektóre obiekty wymagają „zamrożenia” ich w pierwszej kolejności, aby uniknąć zmienności (mutowanie obiektu opartego na wartości, który został użyty jako klucz w słowniku Pythona, jest niedozwolone)
6502

1
@ kawing-chiu pythonfiddle.com/eq-method-needs-ne-method <- pokazuje „błąd” w Pythonie 2. Python 3 nie ma tego problemu : domyślnie __ne__()został „naprawiony” .
Bob Stein

34

Alternatywą w Pythonie 2.6 lub nowszym jest użycie collections.namedtuple()- oszczędza ci to pisania specjalnych metod:

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

Zastępujesz, __hash__jeśli chcesz specjalnej semantyki mieszania i / __cmp__lub __eq__aby twoja klasa była użyteczna jako klucz. Obiekty, które porównują równe, muszą mieć tę samą wartość skrótu.

Python spodziewa __hash__się zwrócić liczbę całkowitą, zwracanie Banana()nie jest zalecane :)

Jak zauważyłeś, klasy zdefiniowane __hash__przez użytkownika mają domyślnie te wywołania id(self).

Dokumentacja zawiera dodatkowe wskazówki :

Klasy, które dziedziczą __hash__() metodę z klasy nadrzędnej, ale zmieniają znaczenie __cmp__()lub __eq__() takie, że zwracana wartość skrótu nie jest już odpowiednia (np. Przez przejście do koncepcji równości opartej na wartości zamiast domyślnej równości opartej na tożsamości) mogą jawnie oznaczać się jako jest nie do powstrzymania przez ustawienie __hash__ = None w definicji klasy. Takie postępowanie oznacza, że ​​instancje klasy nie tylko zgłoszą odpowiedni błąd TypeError, gdy program próbuje odzyskać wartość skrótu, ale zostaną również poprawnie zidentyfikowane jako nieuczesane podczas sprawdzania isinstance(obj, collections.Hashable) (w przeciwieństwie do klas, które definiują własne, __hash__()aby jawnie wywoływać błąd TypeError).


2
Sam hash nie wystarczy, dodatkowo musisz albo przesłonić, __eq__albo __cmp__.
Oben Sonne

@Oben Sonne: __cmp__jest ci dane przez Python, jeśli jest to klasa zdefiniowana przez użytkownika, ale prawdopodobnie i tak chcesz je zastąpić, aby uwzględnić nową semantykę.
Skurmedel

1
@ Skurmedel: Tak, ale chociaż możesz wywoływać cmpi używać =klas użytkowników, które nie zastępują tych metod, jedną z nich należy zaimplementować, aby spełnić wymaganie pytającego, że instancje o podobnej nazwie i lokalizacji mają ten sam klucz słownika.
Oben Sonne
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.