__Hash __ () 'yi uygulamanın doğru ve iyi yolu nedir?


150

Uygulamanın doğru ve iyi bir yolu __hash__()nedir?

Daha sonra hashtables aka sözlükler içine nesneler eklemek için kullanılan bir hashcode döndüren işlevi hakkında konuşuyorum.

Olarak __hash__()bir geri tamsayı değerleri eşit ortak veri dağıtılmalıdır varsayalım döner bir tamsayı ve hashtables içine "gruplama" nesneler için kullanılan (çarpışmasını en aza indirmek için). Bu değerleri elde etmek için iyi bir uygulama nedir? Çarpışmalar bir sorun mu var? Benim durumumda, bazı ints, bazı şamandıralar ve bir dize tutan bir konteyner sınıfı gibi davranan küçük bir sınıfım var.

Yanıtlar:


185

Uygulamanın kolay ve doğru bir yolu, __hash__()bir anahtar grubu kullanmaktır. Özel bir karma kadar hızlı olmayacaktır, ancak buna ihtiyacınız varsa, türü muhtemelen C'de uygulamalısınız.

İşte karma ve eşitlik için bir anahtar kullanma örneği:

class A:
    def __key(self):
        return (self.attr_a, self.attr_b, self.attr_c)

    def __hash__(self):
        return hash(self.__key())

    def __eq__(self, other):
        if isinstance(other, A):
            return self.__key() == other.__key()
        return NotImplemented

Ayrıca, dokümantasyonunda__hash__ bazı özel durumlarda değerli olabilecek daha fazla bilgi vardır.


1
__keyİşlevi çarpanlarına ayırmak için küçük ek yükün yanı sıra , bu herhangi bir karma olabildiğince hızlıdır. Tabii, özniteliklerin tamsayı olduğu biliniyorsa ve çok fazla yoksa, sanırım bazı evde haddelenmiş karma ile biraz daha hızlı çalışabilirsiniz , ancak muhtemelen iyi dağıtılmayacaktır. küçük s'lerin oluşturulması özel olarak optimize edildiğinden ve hashleri ​​alma ve birleştirme işini genellikle Python seviye kodundan daha hızlı olan C yerleşiklerine itme hash((self.attr_a, self.attr_b, self.attr_c))şaşırtıcı derecede hızlı (ve doğru ) olacaktır tuple.
ShadowRanger

Diyelim ki A sınıfı bir nesne bir sözlük için anahtar olarak kullanılıyor ve A sınıfı bir öznitelik değişirse, karma değeri de değişecektir. Bu bir sorun yaratmaz mı?
Bay Matrix

1
@ Loved.by.Jesus'un aşağıdaki cevabından bahsedildiği gibi, değişken bir nesne için karma yöntem tanımlanmamalı / geçersiz kılmamalıdır (varsayılan olarak tanımlanır ve eşitlik ve karşılaştırma için id kullanır).
Bay Matrix

@Miguel, tam sorunla karşılaştım , ne olur Noneanahtar değiştiğinde sözlük geri döner . Çözdüğüm yol, nesnenin kimliğini sadece nesne yerine bir anahtar olarak saklamaktı.
Jaswant P

@JaswantP Python varsayılan olarak herhangi bir yıkanabilir nesnenin anahtarı olarak nesnenin kimliğini kullanır.
Bay Matrix

22

John Millikin buna benzer bir çözüm önerdi:

class A(object):

    def __init__(self, a, b, c):
        self._a = a
        self._b = b
        self._c = c

    def __eq__(self, othr):
        return (isinstance(othr, type(self))
                and (self._a, self._b, self._c) ==
                    (othr._a, othr._b, othr._c))

    def __hash__(self):
        return hash((self._a, self._b, self._c))

Bu çözüm ile ilgili sorun hash(A(a, b, c)) == hash((a, b, c)). Başka bir deyişle, karma, anahtar üyelerinin demetininki ile çarpışır. Belki bu pratikte çok önemli değil?

Güncelleme: Python belgeleri şimdi yukarıdaki örnekte olduğu gibi bir demet kullanılmasını önermektedir. Belgelerin belirttiği gibi

Gereken tek özellik, eşitliği karşılaştıran nesnelerin aynı karma değerine sahip olmasıdır

Bunun tersinin doğru olmadığını unutmayın. Eşit karşılaştırılmayan nesneler aynı karma değerine sahip olabilir . Böyle bir karma çarpışma, dikte anahtarı veya set öğesi olarak kullanıldığında, nesneler de eşit olmadığı sürece bir nesnenin diğerinin yerini almasına neden olmaz .

Eski / kötü çözüm

Python belgelerine__hash__ XOR gibi bir şey kullanarak alt bileşenlerinin karmaları birleştirmek için önermektedir bu bizi veren:

class B(object):

    def __init__(self, a, b, c):
        self._a = a
        self._b = b
        self._c = c

    def __eq__(self, othr):
        if isinstance(othr, type(self)):
            return ((self._a, self._b, self._c) ==
                    (othr._a, othr._b, othr._c))
        return NotImplemented

    def __hash__(self):
        return (hash(self._a) ^ hash(self._b) ^ hash(self._c) ^
                hash((self._a, self._b, self._c)))

Güncelleme: Blckknght'ın işaret ettiği gibi, a, b ve c sırasını değiştirmek sorunlara neden olabilir. ^ hash((self._a, self._b, self._c))Karma olan değerlerin sırasını yakalamak için bir ek ekledim . Bu final ^ hash(...), birleştirilen değerler yeniden düzenlenemezse kaldırılabilir (örneğin, farklı türleri varsa ve bu nedenle değeri _ahiçbir zaman _bveya _c, vb.'ye atanmayacaksa ).


5
Nitelikleri birlikte düz XOR yapmak istemezsiniz, çünkü değerlerin sırasını değiştirirseniz size çarpışma olur. Yani, hash(A(1, 2, 3))eşit olacaktır hash(A(3, 1, 2))(ve her ikisi de , Apermütasyonu olan ve değerleri olarak başka bir örneğe eşit olacaktır ). Örneğinizin bağımsız değişkenlerinin bir demetiyle aynı karma değerine sahip olmasını önlemek istiyorsanız, bir sentinel değeri (bir sınıf değişkeni olarak veya bir global olarak) oluşturmanız yeterlidir. , self._a, self._b, self._c))123
Blckknght

1
Kullanımınız isinstancesorunlu olabilir, çünkü bir alt sınıfının bir nesnesi type(self)artık bir nesnesine eşit olabilir type(self). Bir ekleme bulabilirsiniz Yani Carve bir Forda set()ekleme sırasına bağlı olarak eklenen tek nesne ile sonuçlanabilir. Ayrıca, a == bDoğru ancak b == aYanlış olan bir durumla karşılaşabilirsiniz .
MaratC

1
Alt sınıflama yapıyorsanız B, bunu şu şekilde değiştirmek isteyebilirsinizisinstance(othr, B)
millerdev

7
Bir düşünülmektedir: Anahtar tuple eşit olduğu gösterilmesini ayrıntılarının aynı anahtar seti ile diğer sınıfları önleyecek sınıf tipi, şunları içerebilir: hash((type(self), self._a, self._b, self._c)).
Ben Mosher

2
Kullanmayla ilgili noktasının yanında Byerine type(self), aynı zamanda sık sık geri dönmek için daha iyi uygulama olarak kabul edilir NotImplementedbeklenmedik bir tür karşılaştığında __eq__yerine False. Bu, diğer kullanıcı tanımlı türlerin , istedikleri takdirde __eq__bilen Bve bunlara eşit olanları uygulamalarını sağlar.
Mark Amery

16

Microsoft Research'ten Paul Larson, çok çeşitli karma işlevleri inceledi. O bana şöyle söyledi

for c in some_string:
    hash = 101 * hash  +  ord(c)

çok çeşitli dizeler için şaşırtıcı derecede iyi çalıştı. Benzer polinom tekniklerinin, farklı alt alanların bir karmasını hesaplamak için iyi çalıştığını buldum.


8
Görünüşe göre Java aynı şekilde yapıyor ama 101 yerine 31 kullanıyor
user229898

3
Bu sayıları kullanmanın ardındaki mantık nedir? 101 veya 31'i seçmek için bir neden var mı?
bigblind

1
Başbakan çarpanları için bir açıklama: stackoverflow.com/questions/3613102/… . 101, Paul Larsson'un deneylerine dayanarak özellikle iyi çalışıyor gibi görünüyor.
George V. Reilly

4
Python, (hash * 1000003) XOR ord(c)32 bit sarma çevrimli çoğaltmaya sahip dizeler için kullanır . [Alıntı ]
tylerl

4
Bu doğru olsa bile, yerleşik Python dize türleri zaten bir __hash__yöntem sağladığı için bu bağlamda pratik bir kullanım yoktur ; kendimizi devirmemize gerek yok. Soru, __hash__bu cevabın hiç ele almadığı tipik bir kullanıcı tanımlı sınıf için (yerleşik türlere veya belki de bu tür kullanıcı tanımlı diğer sınıflara işaret eden bir grup özellik ile) nasıl uygulanacağıdır .
Mark Amery

3

Sorunuzun ikinci kısmını cevaplamaya çalışabilirim.

Çarpışmalar muhtemelen karma kodun kendisinden değil, karma kodun bir koleksiyondaki bir dizine eşlenmesinden kaynaklanır. Örneğin, hash fonksiyonunuz 1'den 10000'e rastgele değerler döndürebilir, ancak hash tablonuzda sadece 32 giriş varsa ekleme sırasında çarpışmalar elde edersiniz.

Ayrıca, çarpışmaların koleksiyon tarafından dahili olarak çözüleceğini ve çarpışmaları çözmek için birçok yöntem olduğunu düşünüyorum. En basit (ve en kötü), indeks i'ye eklenecek bir giriş verildiğinde, boş bir nokta bulana ve buraya ekleyene kadar 1'e i ekleyin. Daha sonra alma aynı şekilde çalışır. Bu, bazı koleksiyonlar için verimsiz alımlara neden olur, çünkü bulmak için tüm koleksiyonun geçişini gerektiren bir girişe sahip olabilirsiniz!

Diğer çarpışma çözümleme yöntemleri, işleri dağıtmak için bir öğe eklendiğinde karma tablodaki girişleri taşıyarak alma süresini azaltır. Bu, ekleme süresini artırır ancak eklediğinizden daha fazla okuduğunuzu varsayar. Farklı çarpışma girişlerini belirli bir noktada kümelemek için farklı çarpışma girişlerini denemeye ve dallamaya yönelik yöntemler de vardır.

Ayrıca, koleksiyonu yeniden boyutlandırmanız gerekiyorsa, her şeyi yeniden oluşturmanız veya dinamik bir karma yöntemi kullanmanız gerekir.

Kısacası, karma kodunu ne kullandığınıza bağlı olarak kendi çarpışma çözümleme yönteminizi uygulamak zorunda kalabilirsiniz. Bunları bir koleksiyonda saklamıyorsanız, muhtemelen çok geniş bir aralıkta karma kodları üreten bir karma işleviyle kurtulabilirsiniz. Öyleyse, bellek endişelerinize bağlı olarak kabınızın olması gerekenden daha büyük olduğundan (elbette daha büyük daha iyi) emin olabilirsiniz.

Daha fazla ilgilendiğiniz bazı bağlantılar şunlardır:

wikipedia üzerinde birleşmiş karma

Wikipedia ayrıca çeşitli çarpışma çözümü yöntemlerinin bir özetine sahiptir :

Ayrıca, Tharp tarafından " Dosya Düzenleme ve İşleme " çok sayıda çarpışma çözümleme yöntemini kapsamlı bir şekilde kapsamaktadır. IMO, karma algoritmalar için harika bir referans.


1

Programın web sitesinde__hash__ işlevin ne zaman ve nasıl uygulanacağına dair çok iyi bir açıklama :

Genel bir bakış sağlamak için sadece bir ekran görüntüsü: (Retrieved 2019-12-13)

Https://www.programiz.com/python-programming/methods/built-in/hash 2019-12-13 ekran görüntüsü

Yöntemin kişisel bir uygulamasına gelince, yukarıda belirtilen site millerdev'in cevabına uyan bir örnek sunmaktadır .

class Person:
def __init__(self, age, name):
    self.age = age
    self.name = name

def __eq__(self, other):
    return self.age == other.age and self.name == other.name

def __hash__(self):
    print('The hash is:')
    return hash((self.age, self.name))

person = Person(23, 'Adam')
print(hash(person))

0

Döndürdüğünüz karma değerinin boyutuna bağlıdır. Basit bir mantık, dört bitlik 32 bitlik karmaya dayalı bir 32 bit int döndürmeniz gerekirse, çarpışmalar elde edeceğinizdir.

Biraz operasyonları tercih ederim. Mesela, aşağıdaki C sözde kodu:

int a;
int b;
int c;
int d;
int hash = (a & 0xF000F000) | (b & 0x0F000F00) | (c & 0x00F000F0 | (d & 0x000F000F);

Böyle bir sistem şamandıralar için de işe yarayabilir, eğer onları gerçekte bir kayan nokta değerini temsil etmek yerine bit değeri olarak aldıysanız, belki daha iyi olabilir.

Teller için, hiçbir fikrim yok.


Çarpışmalar olacağını biliyorum. Ama bunların nasıl ele alındığına dair hiçbir fikrim yok. Dahası, kombinasyondaki öznitelik değerlerim çok seyrek dağıtıldığından akıllı bir çözüm arıyordum. Ve bir şekilde orada bir yerde en iyi uygulama olmasını bekledim.
user229898
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.