Mengapa Python dict memiliki banyak kunci dengan hash yang sama?

90

Saya mencoba memahami hashfungsi Python di bawah tenda. Saya membuat kelas khusus di mana semua contoh mengembalikan nilai hash yang sama.

class C:
    def __hash__(self):
        return 42

Saya hanya berasumsi bahwa hanya satu instance dari kelas di atas yang dapat berada dalam a dictkapan saja, tetapi sebenarnya a dictdapat memiliki banyak elemen dengan hash yang sama.

c, d = C(), C()
x = {c: 'c', d: 'd'}
print(x)
# {<__main__.C object at 0x7f0824087b80>: 'c', <__main__.C object at 0x7f0823ae2d60>: 'd'}
# note that the dict has 2 elements

Saya bereksperimen sedikit lebih banyak dan menemukan bahwa jika saya mengganti __eq__metode sedemikian rupa sehingga semua contoh kelas sebanding, maka dictsatu - satunya memungkinkan satu contoh.

class D:
    def __hash__(self):
        return 42
    def __eq__(self, other):
        return True

p, q = D(), D()
y = {p: 'p', q: 'q'}
print(y)
# {<__main__.D object at 0x7f0823a9af40>: 'q'}
# note that the dict only has 1 element

Jadi saya ingin tahu bagaimana a dictbisa memiliki banyak elemen dengan hash yang sama.

Praveen Gollakota
sumber
3
Saat Anda menemukan diri Anda sendiri, set dan dicts dapat berisi banyak objek dengan hash yang sama jika objek itu sendiri tidak sama. Apa yang kamu tanyakan? Bagaimana cara kerja tabel? Itu pertanyaan yang cukup umum dengan banyak materi yang ada ...
@ Delnan Saya lebih memikirkan tentang ini setelah saya memposting pertanyaan; bahwa perilaku ini tidak dapat dibatasi untuk Python. Dan kamu benar. Saya kira saya harus mempelajari lebih dalam literatur tabel Hash secara umum. Terima kasih.
Praveen Gollakota

Jawaban:

55

Untuk penjelasan rinci tentang cara kerja hashing Python, lihat jawaban saya untuk Mengapa pengembalian awal lebih lambat dari yang lain?

Pada dasarnya ini menggunakan hash untuk memilih slot di tabel. Jika ada nilai di slot dan hash cocok, itu membandingkan item untuk melihat apakah mereka sama.

Jika hash tidak cocok atau item tidak sama, maka ia mencoba slot lain. Ada rumus untuk memilih ini (yang saya jelaskan dalam jawaban referensi), dan secara bertahap menarik bagian yang tidak terpakai dari nilai hash; tetapi setelah menggunakan semuanya, ini pada akhirnya akan bekerja melalui semua slot di tabel hash. Itu menjamin pada akhirnya kami menemukan item yang cocok atau slot kosong. Ketika pencarian menemukan slot kosong, itu memasukkan nilai atau menyerah (tergantung apakah kita menambahkan atau mendapatkan nilai).

Hal penting yang perlu diperhatikan adalah tidak ada daftar atau keranjang: hanya ada tabel hash dengan jumlah slot tertentu, dan setiap hash digunakan untuk menghasilkan urutan slot kandidat.

Duncan
sumber
7
Terima kasih telah mengarahkan saya ke arah yang benar tentang implementasi tabel Hash. Saya telah membaca lebih banyak daripada yang saya inginkan tentang tabel hash dan saya menjelaskan temuan saya dalam jawaban terpisah. stackoverflow.com/a/9022664/553995
Praveen Gollakota
112

Berikut adalah segala sesuatu tentang Python dicts yang bisa saya kumpulkan (mungkin lebih dari siapa pun yang ingin tahu; tetapi jawabannya komprehensif). Teriakan kepada Duncan karena menunjukkan bahwa penis Python menggunakan slot dan membawa saya ke lubang kelinci ini.

  • Kamus Python diimplementasikan sebagai tabel hash .
  • Tabel hash harus memungkinkan tabrakan hash, misalnya jika dua kunci memiliki nilai hash yang sama, implementasi tabel harus memiliki strategi untuk memasukkan dan mengambil pasangan kunci dan nilai dengan jelas.
  • Dict Python menggunakan pengalamatan terbuka untuk menyelesaikan benturan hash (dijelaskan di bawah) (lihat dictobject.c: 296-297 ).
  • Tabel hash Python hanyalah blok memori yang bersebelahan (semacam array, sehingga Anda dapat melakukan O(1)pencarian berdasarkan indeks).
  • Setiap slot di tabel dapat menyimpan satu dan hanya satu entri. Ini penting
  • Setiap entri dalam tabel sebenarnya merupakan kombinasi dari tiga nilai -. Ini diimplementasikan sebagai struct C (lihat dictobject.h: 51-56 )
  • Gambar di bawah ini adalah representasi logis dari tabel hash python. Pada gambar di bawah ini, 0, 1, ..., i, ... di sebelah kiri adalah indeks dari slot di tabel hash (mereka hanya untuk tujuan ilustrasi dan tidak disimpan bersama dengan tabel jelas!).

    # Logical model of Python Hash table
    -+-----------------+
    0| <hash|key|value>|
    -+-----------------+
    1|      ...        |
    -+-----------------+
    .|      ...        |
    -+-----------------+
    i|      ...        |
    -+-----------------+
    .|      ...        |
    -+-----------------+
    n|      ...        |
    -+-----------------+
    
  • Saat dikt baru diinisialisasi, ia dimulai dengan 8 slot . (lihat dictobject.h: 49 )

  • Saat menambahkan entri ke tabel, kita mulai dengan beberapa slot, iyang didasarkan pada hash kunci. CPython menggunakan inisial i = hash(key) & mask. Di mana mask = PyDictMINSIZE - 1, tapi itu tidak terlalu penting). Perhatikan bahwa slot awal, i, yang diperiksa tergantung pada hash kunci.
  • Jika slot itu kosong, entri ditambahkan ke slot (menurut entri, maksud saya, <hash|key|value>). Tapi bagaimana jika slot itu ditempati !? Kemungkinan besar karena entri lain memiliki hash yang sama (benturan hash!)
  • Jika slot ditempati, CPython (dan bahkan PyPy) membandingkan hash DAN kunci (dengan membandingkan maksud saya ==perbandingan bukan isperbandingan) dari entri di slot terhadap kunci entri saat ini yang akan dimasukkan ( dictobject.c: 337 , 344-345 ). Jika keduanya cocok, maka dianggap entri sudah ada, menyerah dan pindah ke entri berikutnya untuk disisipkan. Jika salah satu hash atau kunci tidak cocok, itu mulai menyelidiki .
  • Probing berarti mencari slot demi slot untuk menemukan slot kosong. Secara teknis kita bisa pergi satu per satu, i + 1, i + 2, ... dan menggunakan yang pertama tersedia (yaitu probing linier). Tetapi untuk alasan yang dijelaskan dengan indah di komentar (lihat dictobject.c: 33-126 ), CPython menggunakan probing acak . Dalam probing acak, slot berikutnya dipilih dalam urutan acak semu. Entri ditambahkan ke slot kosong pertama. Untuk diskusi ini, algoritma sebenarnya yang digunakan untuk memilih slot berikutnya tidaklah terlalu penting (lihat dictobject.c: 33-126 untuk algoritma probing). Yang penting adalah bahwa slot tersebut diperiksa sampai slot kosong pertama ditemukan.
  • Hal yang sama terjadi untuk pencarian, hanya dimulai dengan slot awal i (di mana saya bergantung pada hash kunci). Jika hash dan kunci keduanya tidak cocok dengan entri di slot, itu mulai menyelidiki, sampai menemukan slot dengan kecocokan. Jika semua slot habis, ini melaporkan kegagalan.
  • BTW, dikt akan diubah ukurannya jika dua pertiga penuh. Ini menghindari perlambatan pencarian. (lihat dictobject.h: 64-65 )

Ini dia! Implementasi Python dari dict memeriksa persamaan hash dari dua kunci dan persamaan normal ( ==) dari kunci saat memasukkan item. Jadi Singkatnya, jika ada dua kunci, adan bdan hash(a)==hash(b), namun a!=b, kemudian keduanya dapat eksis secara harmonis dalam dict Python. Tetapi jika hash(a)==hash(b) dan a==b , maka keduanya tidak bisa berada dalam dikt yang sama.

Karena kita harus menyelidiki setelah setiap tabrakan hash, satu efek samping dari tabrakan hash yang terlalu banyak adalah pencarian dan penyisipan akan menjadi sangat lambat (seperti yang ditunjukkan Duncan di komentar ).

Saya kira jawaban singkat untuk pertanyaan saya adalah, "Karena begitulah cara penerapannya dalam kode sumber;)"

Meskipun ini bagus untuk diketahui (untuk poin geek?), Saya tidak yakin bagaimana ini bisa digunakan dalam kehidupan nyata. Karena kecuali Anda mencoba untuk secara eksplisit merusak sesuatu, mengapa dua objek yang tidak sama memiliki hash yang sama?

Praveen Gollakota
sumber
8
Ini menjelaskan cara kerja mengisi kamus. Tapi bagaimana jika ada benturan hash selama pengambilan pasangan key_value. Katakanlah kita memiliki 2 objek A dan B, keduanya memiliki hash ke 4. Jadi, pertama A diberi slot 4 dan kemudian B diberi slot melalui probing acak. Apa yang terjadi ketika saya ingin mengambil B. B hash ke 4, jadi python pertama memeriksa slot 4, tetapi kuncinya tidak cocok sehingga tidak bisa mengembalikan A. Karena slot B ditugaskan oleh probing acak, bagaimana B dikembalikan lagi dalam waktu O (1)?
sayantankhan
4
@ Bolt64 pemeriksaan acak tidak benar-benar acak. Untuk nilai kunci yang sama selalu mengikuti urutan probe yang sama sehingga pada akhirnya akan menemukan B. Kamus tidak dijamin menjadi O (1), jika Anda mendapatkan banyak tabrakan, maka kamus dapat memakan waktu lebih lama. Dengan versi Python yang lebih lama, mudah untuk membuat serangkaian kunci yang akan bertabrakan dan dalam hal ini pencarian kamus menjadi O (n). Ini adalah vektor yang mungkin untuk serangan DoS sehingga versi Python yang lebih baru memodifikasi hashing agar lebih sulit untuk melakukan ini dengan sengaja.
Duncan
2
@Duncan bagaimana jika A dihapus dan kemudian kita melakukan pencarian di B? Saya kira Anda tidak benar-benar menghapus entri tetapi menandainya sebagai dihapus? Itu berarti bahwa dict tidak cocok untuk terus menerus menyisipkan dan menghapus ....
gen-ys
2
@ gen-ys yes dihapus dan tidak digunakan ditangani secara berbeda untuk pencarian. Unused menghentikan pencarian kecocokan tetapi tidak dihapus. Saat menyisipkan baik yang dihapus atau tidak digunakan diperlakukan sebagai slot kosong yang dapat digunakan. Sisipan dan penghapusan terus menerus diperbolehkan. Ketika jumlah slot yang tidak digunakan (tidak dihapus) turun terlalu rendah, tabel hash akan dibangun kembali dengan cara yang sama seolah-olah telah tumbuh terlalu besar untuk tabel saat ini.
Duncan
1
Ini bukanlah jawaban yang sangat baik tentang titik benturan yang coba diperbaiki Duncan. Ini adalah jawaban yang sangat buruk untuk referensi implementasi dari pertanyaan Anda. Hal utama untuk memahami hal ini adalah jika terjadi benturan, Python mencoba lagi menggunakan rumus untuk menghitung offset berikutnya dalam tabel hash. Saat pengambilan jika kuncinya tidak sama, rumus tersebut menggunakan rumus yang sama untuk mencari offset berikutnya. Tidak ada yang acak tentang itu.
Evan Carroll
20

Sunting : jawaban di bawah ini adalah salah satu cara yang mungkin untuk menangani tabrakan hash, namun itu bukan cara Python melakukannya. Wiki Python yang direferensikan di bawah ini juga salah. Sumber terbaik yang diberikan oleh @Duncan di bawah ini adalah implementasinya sendiri: https://github.com/python/cpython/blob/master/Objects/dictobject.c Saya minta maaf atas kesalahannya .


Ini menyimpan daftar (atau ember) elemen di hash kemudian mengulang melalui daftar itu sampai menemukan kunci sebenarnya dalam daftar itu. Sebuah gambar mengatakan lebih dari seribu kata:

Tabel hash

Di sini Anda melihat John Smithdan Sandra Deekeduanya hash 152. Ember 152berisi keduanya. Saat mencarinya Sandra Dee, pertama kali menemukan daftar di keranjang 152, lalu mengulang melalui daftar itu hingga Sandra Deeditemukan dan kembali 521-6955.

Berikut ini salah, hanya di sini untuk konteks: Di wiki Python, Anda dapat menemukan kode (pseudo?) Bagaimana Python melakukan pencarian.

Sebenarnya ada beberapa solusi yang mungkin untuk masalah ini, lihat artikel wikipedia untuk tinjauan yang bagus: http://en.wikipedia.org/wiki/Hash_table#Collision_resolution

Rob Wouters
sumber
Terima kasih atas penjelasannya dan terutama untuk tautan ke entri wiki Python dengan kode pseudo!
Praveen Gollakota
2
Maaf, tapi jawaban ini salah (begitu juga artikel wiki). Python tidak menyimpan daftar atau kelompok elemen di hash: ia menyimpan tepat satu objek di setiap slot tabel hash. Jika slot yang pertama kali coba digunakan terisi, maka slot tersebut akan mengambil slot lain (menarik bagian hash yang tidak terpakai selama mungkin) dan kemudian yang lain dan lainnya. Karena tidak ada tabel hash yang lebih dari sepertiga penuh, pada akhirnya harus menemukan slot yang tersedia.
Duncan
@ Duncan, wiki Python mengatakan itu diimplementasikan dengan cara ini. Saya akan senang menemukan sumber yang lebih baik. Halaman wikipedia.org jelas tidak salah, itu hanya salah satu solusi yang mungkin seperti yang dinyatakan.
Rob Wouters
@Duncan Bisakah Anda menjelaskan ... menarik bagian hash yang tidak digunakan selama mungkin? Semua hash dalam kasus saya bernilai 42. Terima kasih!
Praveen Gollakota
@PraveenGollakota Ikuti link di jawaban saya, yang menjelaskan secara mendetail bagaimana hash digunakan. Untuk hash 42 dan tabel dengan 8 slot pada awalnya hanya 3 bit terendah yang digunakan untuk menemukan slot nomor 2 tetapi jika slot itu sudah digunakan, bit yang tersisa ikut bermain. Jika dua nilai memiliki hash yang persis sama maka yang pertama masuk di slot pertama mencoba dan yang kedua mendapatkan slot berikutnya. Jika ada 1000 nilai dengan hash identik maka kita akhirnya mencoba 1000 slot sebelum kita menemukan nilainya dan pencarian kamus menjadi sangat lambat!
Duncan
4

Tabel hash, secara umum harus mengizinkan tabrakan hash! Anda akan menjadi tidak beruntung dan dua hal pada akhirnya akan berhubungan dengan hal yang sama. Di bawahnya, ada sekumpulan objek dalam daftar item yang memiliki kunci hash yang sama. Biasanya, hanya ada satu hal dalam daftar itu, tetapi dalam kasus ini, itu akan terus menumpuknya menjadi satu. Satu-satunya cara untuk mengetahui bahwa mereka berbeda adalah melalui operator yang sama.

Jika ini terjadi, performa Anda akan menurun seiring waktu, itulah sebabnya Anda ingin fungsi hash menjadi "seacak mungkin".

Donald Miner
sumber
2

Di utas saya tidak melihat apa sebenarnya yang dilakukan python dengan instance dari kelas yang ditentukan pengguna ketika kami memasukkannya ke dalam kamus sebagai kunci. Mari kita baca beberapa dokumentasi: ini mendeklarasikan bahwa hanya objek yang dapat di-hash yang dapat digunakan sebagai kunci. Hashable adalah semua kelas bawaan yang tidak dapat diubah dan semua kelas yang ditentukan pengguna.

Kelas yang ditentukan pengguna memiliki metode __cmp __ () dan __hash __ () secara default; dengan mereka, semua objek membandingkan tidak sama (kecuali dengan mereka sendiri) dan x .__ hash __ () mengembalikan hasil yang diturunkan dari id (x).

Jadi jika Anda memiliki __hash__ secara konstan di kelas Anda, tetapi tidak menyediakan metode __cmp__ atau __eq__, maka semua instance Anda tidak sama untuk kamus. Di sisi lain, jika Anda menyediakan metode __cmp__ atau __eq__, tetapi tidak menyediakan __hash__, instance Anda masih tidak sama dalam kamus.

class A(object):
    def __hash__(self):
        return 42


class B(object):
    def __eq__(self, other):
        return True


class C(A, B):
    pass


dict_a = {A(): 1, A(): 2, A(): 3}
dict_b = {B(): 1, B(): 2, B(): 3}
dict_c = {C(): 1, C(): 2, C(): 3}

print(dict_a)
print(dict_b)
print(dict_c)

Keluaran

{<__main__.A object at 0x7f9672f04850>: 1, <__main__.A object at 0x7f9672f04910>: 3, <__main__.A object at 0x7f9672f048d0>: 2}
{<__main__.B object at 0x7f9672f04990>: 2, <__main__.B object at 0x7f9672f04950>: 1, <__main__.B object at 0x7f9672f049d0>: 3}
{<__main__.C object at 0x7f9672f04a10>: 3}
checkraise
sumber