Objek jenis khusus sebagai kunci kamus

185

Apa yang harus saya lakukan untuk menggunakan objek saya dari jenis khusus sebagai kunci dalam kamus Python (di mana saya tidak ingin "objek id" untuk bertindak sebagai kunci), misalnya

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

Saya ingin menggunakan MyThing's sebagai kunci yang dianggap sama jika nama dan lokasinya sama. Dari C # / Java saya terbiasa harus menimpa dan memberikan metode kode hash dan sama, dan berjanji untuk tidak bermutasi apa pun tergantung kode hash.

Apa yang harus saya lakukan dengan Python untuk mencapai ini? Haruskah saya meratakan?

(Dalam kasus sederhana, seperti di sini, mungkin akan lebih baik untuk menempatkan tuple (nama, lokasi) sebagai kunci - tetapi pertimbangkan saya ingin kunci menjadi objek)

Orang dgn nama yg tdk dikenal
sumber
Apa yang salah dengan menggunakan hash?
Rafe Kettler
5
Mungkin karena dia ingin dua MyThing, jika mereka memiliki yang sama namedan location, untuk mengindeks kamus untuk mengembalikan nilai yang sama, bahkan jika mereka dibuat secara terpisah sebagai dua "objek" yang berbeda.
Santa
1
"mungkin lebih baik menempatkan tuple (nama, lokasi) sebagai kunci - tetapi pertimbangkan saya ingin kunci itu menjadi objek)" Maksud Anda: objek NON-KOMPOSIT?
eyquem

Jawaban:

221

Anda perlu menambahkan 2 metode , perhatikan __hash__dan __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)

Dokumentasi dict Python mendefinisikan persyaratan ini pada objek kunci, yaitu mereka harus hashable .

6502
sumber
17
hash(self.name)terlihat lebih bagus daripada self.name.__hash__(), dan jika Anda melakukannya dan Anda bisa lakukan hash((x, y))untuk menghindari XORing sendiri.
Rosh Oxymoron
5
Sebagai catatan tambahan, saya baru saja menemukan bahwa memanggil x.__hash__()seperti itu juga salah , karena dapat menghasilkan hasil yang salah : pastebin.com/C9fSH7eF
Rosh Oxymoron
@Rosh Oxymoron: terima kasih atas komentarnya. Saat menulis saya menggunakan eksplisit anduntuk __eq__tetapi kemudian saya berpikir "mengapa tidak menggunakan tuple?" karena saya sering melakukan itu (saya pikir itu lebih mudah dibaca). Untuk beberapa alasan aneh, mataku tidak kembali mempertanyakan __hash__.
6502
1
@ user877329: apakah Anda mencoba menggunakan beberapa struktur data blender sebagai kunci? Rupanya dari beberapa repo objek tertentu mengharuskan Anda untuk "membekukan" mereka terlebih dahulu untuk menghindari mutabilitas (mutasi objek berbasis nilai yang telah digunakan sebagai kunci dalam kamus python tidak diizinkan)
6502
1
@ kawing-chiu pythonfiddle.com/eq-method-needs-ne-method <- ini menunjukkan "bug" dalam Python 2. Python 3 tidak memiliki masalah ini : defaultnya __ne__()telah "diperbaiki" .
Bob Stein
34

Alternatif dengan Python 2.6 atau di atas adalah menggunakan collections.namedtuple()- menghemat Anda menulis metode khusus:

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

Anda menimpa __hash__jika Anda menginginkan semantik-hash khusus, dan __cmp__atau __eq__agar kelas Anda dapat digunakan sebagai kunci. Objek yang membandingkan kebutuhan yang sama untuk memiliki nilai hash yang sama.

Python mengharapkan __hash__untuk mengembalikan integer, pengembalian Banana()tidak dianjurkan :)

Kelas yang ditentukan pengguna __hash__secara default memanggil id(self), seperti yang Anda catat.

Ada beberapa tips tambahan dari dokumentasi :

Kelas-kelas yang mewarisi __hash__() metode dari kelas induk tetapi mengubah makna __cmp__()atau __eq__() sehingga nilai hash yang dikembalikan tidak lagi sesuai (misalnya dengan beralih ke konsep kesetaraan berbasis nilai alih-alih kesetaraan berbasis identitas standar) dapat secara eksplisit menandai diri mereka sebagai tidak bisa dihancurkan dengan menetapkan __hash__ = None dalam definisi kelas. Melakukannya berarti bahwa instance kelas tidak hanya akan meningkatkan TypeError yang sesuai ketika sebuah program mencoba untuk mengambil nilai hash mereka, tetapi mereka juga akan diidentifikasi dengan benar sebagai tidak dapat terluka ketika memeriksa isinstance(obj, collections.Hashable) (tidak seperti kelas yang menentukan kelasnya __hash__()untuk secara eksplisit meningkatkan TypeError).

Skurmedel
sumber
2
Hash saja tidak cukup, Anda juga perlu mengganti __eq__atau __cmp__.
Oben Sonne
@Oben Sonne: __cmp__diberikan kepada Anda oleh Python jika itu adalah kelas yang ditentukan pengguna, tetapi Anda mungkin ingin tetap menimpanya untuk mengakomodasi semantik baru.
Skurmedel
1
@ Skurmedel: Ya, tetapi meskipun Anda dapat menelepon cmpdan menggunakan =pada kelas pengguna yang tidak mengesampingkan metode ini, salah satunya harus diterapkan untuk memenuhi persyaratan penanya bahwa instance dengan nama dan lokasi yang sama memiliki kunci kamus yang sama.
Oben Sonne