Bir Python diktesinin neden aynı karma değerine sahip birden çok anahtarı olabilir?


92

hashKaputun altındaki Python işlevini anlamaya çalışıyorum . Tüm örneklerin aynı hash değerini döndürdüğü özel bir sınıf oluşturdum.

class C:
    def __hash__(self):
        return 42

Sadece yukarıdaki sınıfın yalnızca bir örneğinin dictherhangi bir zamanda bir içinde olabileceğini varsaydım , ama aslında a dictaynı karma ile birden fazla öğeye sahip olabilir.

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

Biraz daha deney yaptım ve __eq__yöntemi, sınıfın tüm örnekleri eşit olacak şekilde geçersiz kılarsam, dictyalnızca bir örneğe izin verdiğini gördüm .

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

Bu yüzden dict, aynı hash ile birden fazla öğeye nasıl sahip olabileceğimi merak ediyorum .


3
Kendinizi keşfettiğiniz gibi, setler ve dikteler, nesneler kendileri eşit değilse, eşit karmalara sahip birden çok nesne içerebilir. Ne soruyorsun? Tablolar nasıl çalışır? Bu, birçok mevcut malzemeyle oldukça genel bir soru ...

@delnan Soruyu gönderdikten sonra bunun hakkında daha çok düşünüyordum; bu davranışın Python ile sınırlandırılamayacağı. Ve haklısın. Sanırım genel Hash tablosu literatürünü daha derinlemesine incelemeliyim. Teşekkürler.
Praveen Gollakota

Yanıtlar:


58

Python'un hash işleminin nasıl çalıştığına dair ayrıntılı bir açıklama için, Neden erken dönüş diğerlerinden daha yavaş?

Temel olarak, tablodaki bir yuvayı seçmek için karmayı kullanır. Yuvada bir değer varsa ve karma eşleşiyorsa, eşit olup olmadıklarını görmek için öğeleri karşılaştırır.

Hash eşleşmezse veya öğeler eşit değilse, başka bir yuvayı dener. Bunu seçmek için bir formül var (başvurulan cevapta anlattığım) ve yavaş yavaş karma değerin kullanılmayan kısımlarını çekiyor; ancak hepsini bir kez kullandığında, sonunda karma tablodaki tüm slotlarda yoluna devam edecektir. Bu, sonunda ya eşleşen bir öğe ya da boş bir yuva bulacağımızı garanti eder. Arama boş bir alan bulduğunda, değeri ekler veya vazgeçer (bir değer ekleyip eklememize veya aldığımıza bağlı olarak).

Unutulmaması gereken önemli nokta, hiçbir liste veya kova olmamasıdır: sadece belirli sayıda yuvaya sahip bir karma tablo vardır ve her karma, bir dizi aday alan oluşturmak için kullanılır.


7
Hash tablosu uygulaması konusunda beni doğru yönlendirdiğiniz için teşekkür ederiz. Hash tabloları hakkında istediğimden çok daha fazlasını okudum ve bulgularımı ayrı bir cevapla açıkladım. stackoverflow.com/a/9022664/553995
Praveen Gollakota

117

İşte Python kuralları hakkında bir araya getirebildiğim her şey (muhtemelen herkesin bilmek isteyeceğinden daha fazla; ancak cevap kapsamlı). Duncan'a , Python aygıtlarının yuvaları kullandığına işaret ettiği ve beni bu tavşan deliğine yönlendirdiği için bir not .

  • Python sözlükleri karma tablolar olarak uygulanır .
  • Karma tablolar, karma çarpışmalara izin vermelidir, yani, iki anahtar aynı karma değerine sahip olsa bile, tablonun uygulanmasının, anahtar ve değer çiftlerini açık bir şekilde eklemek ve almak için bir stratejisi olmalıdır.
  • Python dict, hash çarpışmalarını çözmek için açık adresleme kullanır (aşağıda açıklanmıştır) (bakınız dictobject.c: 296-297 ).
  • Python hash tablosu sadece ardışık bir bellek bloğudur (bir dizi gibi, böylece O(1)dizine göre arama yapabilirsiniz ).
  • Tablodaki her yuva bir ve yalnızca bir giriş saklayabilir. Bu önemli
  • Tablodaki her giriş aslında üç değerin bir kombinasyonudur -. Bu bir C yapısı olarak uygulanır (bakınız dictobject.h: 51-56 )
  • Aşağıdaki şekil bir python hash tablosunun mantıksal bir temsilidir. Aşağıdaki şekilde, 0, 1, ..., i, ... soldaki karma tablodaki slotların indeksleridir (bunlar sadece açıklama amaçlıdır ve açıkça tablo ile birlikte depolanmazlar!).

    # Logical model of Python Hash table
    -+-----------------+
    0| <hash|key|value>|
    -+-----------------+
    1|      ...        |
    -+-----------------+
    .|      ...        |
    -+-----------------+
    i|      ...        |
    -+-----------------+
    .|      ...        |
    -+-----------------+
    n|      ...        |
    -+-----------------+
    
  • Yeni bir dict başlatılır zaman 8 ile başlar yuvaları . (bkz. dictobject.h: 49 )

  • Tabloya girişler eklerken i, anahtarın karmasını temel alan bir yuvayla başlarız . CPython ilk kullanır i = hash(key) & mask. Nerede mask = PyDictMINSIZE - 1, ama bu gerçekten önemli değil). Sadece kontrol edilen ilk yuvanın, i, anahtarın karmasına bağlı olduğuna dikkat edin .
  • Bu yuva boşsa, giriş yuvaya eklenir (girdi olarak, yani <hash|key|value>). Ama ya bu yuva doluysa !? Büyük olasılıkla başka bir girişin aynı hash'e sahip olması (hash collision!)
  • Yuva dolu ise, CPython (ve hatta PyPy), yuvadaki girişin karma VE anahtarını ( ==karşılaştırma değil, iskarşılaştırma) eklenecek geçerli girişin anahtarı ile karşılaştırır ( dictobject.c: 337 , 344-345 ). Her ikisi de eşleşirse, girişin zaten var olduğunu düşünür, vazgeçer ve eklenecek bir sonraki girişe geçer. Karma veya anahtar eşleşmezse, araştırmaya başlar .
  • Prob, boş bir yuva bulmak için yuvaları yuvaya göre araması anlamına gelir. Teknik olarak tek tek gidebiliriz, i + 1, i + 2, ... ve ilk mevcut olanı kullanabiliriz (bu doğrusal problama). Ancak yorumlarda güzelce açıklanan nedenlerden dolayı (bkz. Dictobject.c: 33-126 ), CPython rastgele araştırma kullanır . Rastgele araştırmada, sonraki aralık sözde rastgele bir sırayla seçilir. Giriş, ilk boş yuvaya eklenir. Bu tartışma için, bir sonraki yuvayı seçmek için kullanılan gerçek algoritma gerçekten önemli değildir ( problama algoritması için bkz. Dictobject.c: 33-126 ). Önemli olan, yuvaların ilk boş yuva bulunana kadar incelenmesidir.
  • Aynı şey aramalar için de olur, sadece ilk yuva i ile başlar (burada i anahtarın karmasına bağlıdır). Karma ve anahtarın ikisi de yuvadaki girişle eşleşmezse, eşleşen bir yuva bulana kadar araştırmaya başlar. Tüm yuvalar tükenirse, bir başarısızlık bildirir.
  • BTW, üçte ikisi doluysa dikte yeniden boyutlandırılacaktır. Bu, aramaların yavaşlamasını önler. (bkz. dictobject.h: 64-65 )

İşte gidiyorsun! Python dict uygulaması, ==öğeler eklerken hem iki anahtarın hash eşitliğini hem de anahtarların normal eşitliğini ( ) kontrol eder. Özetle, eğer iki anahtar varsa ave bve hash(a)==hash(b), ama a!=bo zaman her ikisi de bir Python diktesinde uyumlu bir şekilde var olabilir. Ama eğer hash(a)==hash(b) ve a==b , o zaman ikisi de aynı diktede olamaz.

Her karma çarpışma sonrasında soruşturma zorunda olduğundan, çok fazla karma çarpışmaların bir yan etkisi (Duncan işaret ettiği gibi aramaları ve eklemeleri çok yavaş olacak olmasıdır yorumlarla ).

Sanırım sorumun kısa cevabı, "Çünkü kaynak kodda bu şekilde uygulanıyor;)"

Bunu bilmek iyi olsa da (geek noktaları için?), Gerçek hayatta nasıl kullanılabileceğinden emin değilim. Çünkü açıkça bir şeyi kırmaya çalışmadığınız sürece, neden eşit olmayan iki nesne aynı hash'e sahip olsun?


9
Bu, sözlüğü doldurmanın nasıl çalıştığını açıklar. Ama ya bir anahtar_değer çiftinin alınması sırasında bir hash çakışması olursa? Diyelim ki, her ikisi de 4'e hash olan 2 A ve B nesnemiz var. Yani, önce A'ya 4 numaralı yuvaya ve sonra B'ye rastgele problama ile yuva atanır. B'nin karmalarını 4'e getirmek istediğimde ne olur, yani python önce 4. yuvayı kontrol eder, ancak anahtar eşleşmez, bu yüzden A'ya geri dönemez. B'nin yuvası rastgele araştırmayla atandığından, B yeniden nasıl döndürülür O (1) süresinde?
sayantankhan

4
@ Bolt64 rastgele araştırma gerçekten rastgele değil. Aynı anahtar değerleri için, her zaman aynı araştırma sırasını takip eder, böylece sonunda B'yi bulacaktır. Sözlüklerin O (1) olması garanti edilmez, çok fazla çarpışma alırsanız daha uzun sürebilirler. Python'un eski sürümlerinde, çarpışacak bir dizi anahtar oluşturmak kolaydır ve bu durumda sözlük aramaları O (n) olur. Bu, DoS saldırıları için olası bir vektördür, bu nedenle daha yeni Python sürümleri, bunu kasıtlı olarak yapmayı zorlaştırmak için karmayı değiştirir.
Duncan

3
@Duncan ya A silinir ve sonra B üzerinde bir arama yaparsak? Sanırım girdileri gerçekten silmiyorsunuz ama onları silinmiş olarak işaretliyorsunuz? Bu, dicts'in sürekli ekleme ve silme işlemleri için uygun olmadığı anlamına gelir ....
gen-ys

2
@ gen-ys evet silindi ve kullanılmayanlar, arama için farklı şekilde ele alınır. Kullanılmayan bir eşleşme aramayı durdurur ancak silinmiş değildir. Ekte silinmiş veya kullanılmamış, kullanılabilecek boş yuvalar olarak kabul edilir. Sürekli eklemeler ve silmeler iyidir. Kullanılmayan (silinmemiş) yuvaların sayısı çok düştüğünde, hash tablosu, mevcut tablo için çok büyümüş gibi yeniden oluşturulacaktır.
Duncan

1
Duncan'ın düzeltmeye çalıştığı çarpışma noktasında bu çok iyi bir cevap değil. Sorunuzdan uygulama için referans olarak özellikle zayıf bir cevap. Bunu anlamanın en önemli yanı, bir çarpışma olduğunda Python'un hash tablosundaki bir sonraki ofseti hesaplamak için bir formül kullanarak tekrar denemesidir. Anahtar aynı değilse, alma sırasında bir sonraki ofseti aramak için aynı formülü kullanır. Bunda rastgele bir şey yok.
Evan Carroll

20

Düzenleme : Aşağıdaki cevap karma çarpışmaları ile başa çıkmak için mümkün yollarından biridir, bununla birlikte olduğu değil Python nasıl yapıyor. Python'un aşağıda atıfta bulunulan wiki'si de yanlıştır. Aşağıda @Duncan tarafından verilen en iyi kaynak uygulamanın kendisidir: https://github.com/python/cpython/blob/master/Objects/dictobject.c Karışım için özür dilerim.


Karma öğedeki öğelerin bir listesini (veya kova) depolar, ardından bu listede gerçek anahtarı bulana kadar bu listede yinelenir. Bir resim bin kelimeden fazla diyor:

Hash tablosu

Burada gördüğünüz John Smithve Sandra Deeher iki karma 152. Kova 152ikisini de içerir. Arama yaparken Sandra Deeönce kova içindeki listeyi bulur 152, ardından Sandra Deebulunana ve geri dönene kadar bu liste boyunca ilerler 521-6955.

Aşağıdaki bağlam burada olurdu yanlıştır: On Python'un wiki Python araması nasıl performans (? Sözde) kodunu bulabilirsiniz.

Aslında bu sorunun birkaç olası çözümü vardır, güzel bir genel bakış için wikipedia makalesine bakın: http://en.wikipedia.org/wiki/Hash_table#Collision_resolution


Açıklama ve özellikle sözde kod içeren Python wiki girişine bağlantı için teşekkürler!
Praveen Gollakota

2
Üzgünüm, ama bu cevap tamamen yanlış (wiki makalesi de öyle). Python, hash'de bir öğe listesi veya kova saklamaz: hash tablosunun her slotunda tam olarak bir nesne depolar. İlk kullanmaya çalıştığı yuva dolu ise, başka bir yuva (karmanın kullanılmayan kısımlarını mümkün olduğu kadar uzun süre içeri çekerek) ve sonra başka bir yuva seçer. Hiçbir hash tablosu hiçbir zaman üçte birinden fazla dolu olmadığından, sonunda uygun bir yuva bulması gerekir.
Duncan

@Duncan, Python'un wiki'sinin bu şekilde uygulandığını söylüyor. Daha iyi bir kaynak bulmaktan mutluluk duyarım. Wikipedia.org sayfası kesinlikle yanlış değil, belirtildiği gibi olası çözümlerden sadece biri.
Rob Wouters

@Duncan Lütfen karmanın kullanılmayan kısımlarını olabildiğince uzun süre çekip ... açıklar mısınız? Benim durumumdaki tüm karmalar 42 olarak değerlendirilir. Teşekkürler!
Praveen Gollakota

@PraveenGollakota Karmanın nasıl kullanıldığını kanlı ayrıntılı olarak açıklayan cevabımdaki bağlantıyı izleyin. 42'lik bir hash ve 8 slotlu bir tablo için başlangıçta sadece en düşük 3 bit 2 numaralı slotu bulmak için kullanılır, ancak bu slot zaten kullanılmışsa, kalan bitler oyuna girer. İki değer tam olarak aynı hash değerine sahipse, ilk denenen ilk yuvaya gider ve ikincisi bir sonraki yuvayı alır. Aynı hash değerlerine sahip 1000 değer varsa, değeri bulmadan önce 1000 yuvayı deneriz ve sözlük araması çok çok yavaşlar!
Duncan

4

Karma tablolar, genel olarak karma çarpışmalara izin vermelidir! Şanssız olacaksınız ve iki şey sonunda aynı şeye hash olacak. Altında, aynı karma anahtara sahip öğeler listesinde bir dizi nesne vardır. Genellikle, bu listede yalnızca bir şey vardır, ancak bu durumda, onları aynı şekilde biriktirmeye devam edecektir. Farklı olduklarını bilmenin tek yolu eşittir operatörü kullanmaktır.

Bu olduğunda, performansınız zamanla düşecektir, bu nedenle karma işlevinizin "olabildiğince rastgele" olmasını istersiniz.


2

İş parçacığında, python'un kullanıcı tanımlı sınıfların örnekleriyle tam olarak ne yaptığını, onu bir sözlüğe anahtar olarak koyduğumuzda görmedim. Bazı dokümantasyonu okuyalım: sadece hashable nesnelerin anahtar olarak kullanılabileceğini bildirir. Hashable, değişmez yerleşik sınıflar ve tüm kullanıcı tanımlı sınıflardır.

Kullanıcı tanımlı sınıfların varsayılan olarak __cmp __ () ve __hash __ () yöntemleri vardır; bunlarla birlikte, tüm nesneler eşit olmayanları karşılaştırır (kendileri hariç) ve x .__ hash __ (), id (x) 'den türetilen bir sonuç döndürür.

Dolayısıyla, sınıfınızda sürekli bir __hash__ varsa, ancak herhangi bir __cmp__ veya __eq__ yöntemi sağlamıyorsanız, o zaman tüm örnekleriniz sözlük için eşit değildir. Öte yandan, herhangi bir __cmp__ veya __eq__ yöntemi sağlar, ancak __hash__ sağlamazsanız, örnekleriniz sözlük açısından yine de eşit değildir.

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)

Çıktı

{<__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}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.