__Hash __ () को लागू करने का एक सही और अच्छा तरीका क्या है?


150

लागू करने का एक सही और अच्छा तरीका क्या है __hash__()?

मैं उस फ़ंक्शन के बारे में बात कर रहा हूं जो एक हैशकोड लौटाता है जो तब हैशटैब उर्फ ​​शब्दकोशों में वस्तुओं को डालने के लिए उपयोग किया जाता है।

जैसा कि __hash__()एक पूर्णांक देता है और "हैनिंग" ऑब्जेक्ट के लिए हैशटेबल्स में उपयोग किया जाता है, मुझे लगता है कि लौटे पूर्णांक के मूल्यों को समान रूप से सामान्य डेटा (टकरावों को कम करने के लिए) में वितरित किया जाना चाहिए। ऐसे मूल्यों को प्राप्त करने के लिए एक अच्छा अभ्यास क्या है? क्या टकराव एक समस्या है? मेरे मामले में मेरे पास एक छोटा वर्ग है जो कंटेनर क्लास के रूप में काम करता है, जिसमें कुछ इन्ट्स, कुछ फ्लोट्स और एक स्ट्रिंग होती है।

जवाबों:


185

लागू __hash__()करने का एक आसान, सही तरीका एक कुंजी टपल का उपयोग करना है। यह एक विशेष हैश की तरह तेज़ नहीं होगा, लेकिन यदि आपको इसकी आवश्यकता है तो आपको सी में टाइप लागू करना चाहिए।

यहाँ हैश और समानता के लिए एक कुंजी का उपयोग करने का एक उदाहरण दिया गया है:

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

इसके अलावा, प्रलेखन के__hash__ पास अधिक जानकारी है, जो कुछ विशेष परिस्थितियों में मूल्यवान हो सकती है।


1
__keyसमारोह में फैक्टरिंग से मामूली ओवरहेड के अलावा , यह किसी भी हैश के रूप में तेजी से हो सकता है। निश्चित रूप से, यदि विशेषताओं को पूर्णांक के रूप में जाना जाता है, और उनमें से बहुत सारे नहीं हैं, तो मुझे लगता है कि आप संभावित रूप से कुछ होम-रोल्ड हैश के साथ थोड़ा तेज दौड़ सकते हैं , लेकिन यह संभवतः वितरित नहीं होगा। hash((self.attr_a, self.attr_b, self.attr_c))आश्चर्यजनक रूप से तेज़ (और सही ) होने जा रहा है , क्योंकि छोटे tupleएस का निर्माण विशेष रूप से अनुकूलित है, और यह सी बिल्डरों में हैश को प्राप्त करने और संयोजन करने के काम को आगे बढ़ाता है, जो आमतौर पर पायथन स्तर के कोड की तुलना में तेज है।
शैडो रेंजर

मान लीजिए कि कक्षा A की एक वस्तु का उपयोग एक शब्दकोश के लिए एक कुंजी के रूप में किया जा रहा है और यदि वर्ग A की विशेषता बदल जाती है, तो इसका हैश मान भी बदल जाएगा। क्या इससे कोई समस्या नहीं होगी?
श्री मैट्रिक्स

1
जैसा कि @ lov.by.Jesus के उत्तर में उल्लेख किया गया है, हैश विधि को एक उत्परिवर्तित वस्तु के लिए परिभाषित / अतिरंजित नहीं किया जाना चाहिए (डिफ़ॉल्ट रूप से परिभाषित और समानता और तुलना के लिए आईडी का उपयोग करता है)।
श्री मैट्रिक्स

@ मिगुएल, मैं सटीक समस्या में भाग गया , क्या होता है Noneएक बार महत्वपूर्ण परिवर्तन के बाद शब्दकोश रिटर्न होता है । जिस तरह से मैंने इसे हल किया वह ऑब्जेक्ट के आईडी को केवल ऑब्जेक्ट के बजाय कुंजी के रूप में संग्रहीत करके था।
जसवंत पी

@JaswantP पायथन डिफ़ॉल्ट रूप से किसी भी हैशेबल ऑब्जेक्ट के लिए कुंजी के रूप में आईडी का उपयोग करता है।
श्री मैट्रिक्स

22

जॉन मिलिकिन ने इसके समान एक समाधान प्रस्तावित किया:

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))

इस समाधान के साथ समस्या यह है कि है hash(A(a, b, c)) == hash((a, b, c))। दूसरे शब्दों में, हैश अपने प्रमुख सदस्यों के टपल से टकराता है। शायद यह बहुत बार अभ्यास में नहीं होता है?

अद्यतन: पायथन डॉक्स अब ऊपर के उदाहरण के रूप में एक टपल का उपयोग करने की सलाह देते हैं। ध्यान दें कि दस्तावेज़ बताता है

केवल आवश्यक संपत्ति यह है कि जो वस्तुएं समान तुलना करती हैं उनका समान हैश मूल्य होता है

ध्यान दें कि विपरीत सच नहीं है। जो वस्तुएं समान की तुलना नहीं करती हैं, उनका मूल्य समान हैश हो सकता है। इस तरह की हैश टक्कर एक वस्तु को दूसरे की जगह लेने का कारण नहीं बनेगी, जब एक प्रमुख कुंजी या सेट तत्व के रूप में उपयोग किया जाता है जब तक कि वस्तुओं की तुलना न हो

खराब / खराब समाधान

पर पायथन प्रलेखन__hash__ XOR की तरह कुछ का उपयोग कर उप घटकों के हैश गठबंधन करने के लिए पता चलता है , जो हमें इस देता है:

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)))

अपडेट: ब्लैंकथन बताते हैं कि, a, b और c के क्रम को बदलने से समस्या हो सकती है। मैंने ^ hash((self._a, self._b, self._c))हैश किए जाने वाले मूल्यों के क्रम को पकड़ने के लिए एक अतिरिक्त जोड़ा । इस अंतिम ^ hash(...)मान जा रहा है संयुक्त पुन: व्यवस्थित नहीं किया जा सकता है, तो हटाया जा सकता है (उदाहरण के लिए, वे विभिन्न प्रकार के और इसलिए का मूल्य है, तो _aको सौंपा जा कभी नहीं होगा _bया _c, आदि)।


5
आप आमतौर पर विशेषताओं को एक साथ सीधा करना नहीं चाहते हैं, क्योंकि यदि आप मूल्यों के क्रम को बदलते हैं तो आपको टक्कर देंगे। है यही कारण है, hash(A(1, 2, 3))के बराबर हो जाएगा hash(A(3, 1, 2))(और वे दोनों हैश किसी अन्य के बराबर जाएगा Aका क्रमपरिवर्तन साथ उदाहरण 1, 2और 3अपने मूल्यों के रूप में)। यदि आप अपने उदाहरणों को अपने तर्कों के एक ही हैश के रूप में रखने से बचना चाहते हैं, तो बस एक सेंटिनल वैल्यू (या तो एक वर्ग चर, या एक वैश्विक के रूप में) बनाएं और फिर इसे हैश में रखने के लिए ट्यूपल में शामिल करें: हैश (_ सेंटिनल) , स्व._ए, स्व._ बी, स्व ._सी))
ब्लॅकथाँग

1
आपका उपयोग isinstanceसमस्याग्रस्त हो सकता है, क्योंकि किसी उपवर्ग की वस्तु type(self)अब किसी वस्तु के बराबर हो सकती है type(self)। आपको लगता है कि एक जोड़ने मिल सकता है तो Carऔर एक Fordएक करने के लिए set()केवल एक ही वस्तु डाला, प्रविष्टि के आदेश के आधार पर हो सकता है। इसके अतिरिक्त, आप ऐसी स्थिति में भाग सकते हैं जहां a == bसत्य है लेकिन b == aगलत है।
मराट

1
यदि आप उपवर्ग कर रहे हैं B, तो आप उसे बदल सकते हैंisinstance(othr, B)
मिलरदेव

7
एक विचार: कुंजी टपल में कक्षा का प्रकार शामिल हो सकता है, जो अन्य वर्गों को समान कुंजी सेट के साथ विशेषताओं को समान दिखाने से रोक देगा hash((type(self), self._a, self._b, self._c)):।
बेन मोशेर

2
इसके Bबजाय का उपयोग करने के बारे में बिंदु के अलावा type(self), यह अक्सर बेहतर अभ्यास माना जाता है NotImplementedजब बदले में एक अप्रत्याशित प्रकार का सामना करना पड़ता __eq__है False। यह अन्य उपयोगकर्ता-परिभाषित प्रकारों को लागू करने की अनुमति देता है , __eq__जिनके बारे में जानता है Bऔर यदि वे चाहें तो इसके बराबर की तुलना कर सकते हैं।
मार्क अमेरी

16

Microsoft अनुसंधान के पॉल लार्सन ने कई प्रकार के हैश कार्यों का अध्ययन किया। उसने मुझे बोला की

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

आश्चर्यजनक रूप से अच्छी तरह से स्ट्रिंग्स की एक विस्तृत विविधता के लिए काम किया। मैंने पाया है कि इसी तरह की बहुपत्नी तकनीक असमान उपक्षेत्रों के हैश की गणना के लिए अच्छी तरह से काम करती है।


8
जाहिरा तौर पर जावा इसे उसी तरह करता है लेकिन 101 के बजाय 31 का उपयोग कर रहा है
user229898

3
इन नंबरों का उपयोग करने के पीछे तर्क क्या है? क्या 101, या 31 चुनने का कोई कारण है?
bigblind

1
यहां प्राइम मल्टीप्लायरों के लिए स्पष्टीकरण दिया गया है: stackoverflow.com/questions/3613102/… । पॉल लार्सन के प्रयोगों के आधार पर 101 विशेष रूप से अच्छी तरह से काम करता है।
जॉर्ज वी। रीली

4
पायथन (hash * 1000003) XOR ord(c)32-बिट रैपराउंड गुणन के साथ तार के लिए उपयोग करता है। [उद्धरण ]
टाइलर १४'१३

4
यहां तक ​​कि अगर यह सच है तो इस संदर्भ में कोई व्यावहारिक उपयोग नहीं है क्योंकि अंतर्निहित पायथन स्ट्रिंग प्रकार पहले से ही एक __hash__विधि प्रदान करते हैं ; हमें अपना रोल करने की जरूरत नहीं है। सवाल यह है कि __hash__एक विशिष्ट उपयोगकर्ता-परिभाषित वर्ग के लिए कैसे लागू किया जाए (गुणों के एक समूह के साथ अंतर्निहित प्रकारों या शायद अन्य ऐसे उपयोगकर्ता-परिभाषित वर्गों के लिए), जो इस उत्तर को बिल्कुल भी संबोधित नहीं करता है।
मार्क अमेरी

3

मैं आपके प्रश्न के दूसरे भाग का उत्तर देने का प्रयास कर सकता हूं।

टक्करों का परिणाम हैश कोड से ही नहीं होगा, बल्कि एक संग्रह में हैश कोड को एक इंडेक्स से मैप करने से होगा। उदाहरण के लिए आपका हैश फ़ंक्शन 1 से 10000 तक यादृच्छिक मान लौटा सकता है, लेकिन यदि आपकी हैश तालिका में केवल 32 प्रविष्टियाँ हैं, तो आपको सम्मिलन के साथ टकराव मिलेगा।

इसके अलावा, मुझे लगता है कि टकराव को आंतरिक रूप से संग्रह द्वारा हल किया जाएगा, और टक्करों को हल करने के लिए कई तरीके हैं। सबसे सरल (और सबसे खराब), इंडेक्स i में सम्मिलित करने के लिए एक प्रविष्टि दी गई है, 1 से 1 जोड़ें जब तक कि आप एक खाली स्थान नहीं पाते और वहां डालें। पुनर्प्राप्ति तब उसी तरह काम करती है। यह कुछ प्रविष्टियों के लिए अयोग्य पुनर्प्राप्ति का परिणाम है, क्योंकि आपके पास एक प्रविष्टि हो सकती है जिसे खोजने के लिए पूरे संग्रह की आवश्यकता होती है!

अन्य टकराव के समाधान के तरीके हैश तालिका में प्रविष्टियों को स्थानांतरित करके पुनर्प्राप्ति समय को कम करते हैं जब कोई आइटम चीजों को फैलाने के लिए डाला जाता है। यह प्रविष्टि समय बढ़ाता है, लेकिन आपको लगता है कि आप सम्मिलित करने से अधिक पढ़ते हैं। ऐसे तरीके भी हैं जो अलग-अलग टकराने वाली प्रविष्टियों की कोशिश करते हैं और शाखा करते हैं ताकि एक विशेष स्थान पर क्लस्टर में प्रविष्टियां हो सकें।

इसके अलावा, यदि आपको संग्रह का आकार बदलने की आवश्यकता है तो आपको सब कुछ फिर से करना होगा या एक गतिशील हैशिंग विधि का उपयोग करना होगा।

संक्षेप में, आप अपने लिए हैश कोड का उपयोग कर रहे हैं, इसके आधार पर आपको अपने स्वयं के टकराव के समाधान के तरीके को लागू करना पड़ सकता है। यदि आप उन्हें एक संग्रह में संग्रहीत नहीं कर रहे हैं, तो आप शायद एक हैश फ़ंक्शन के साथ दूर हो सकते हैं जो कि बहुत बड़ी रेंज में हैश कोड उत्पन्न करता है। यदि ऐसा है, तो आप यह सुनिश्चित कर सकते हैं कि आपका कंटेनर आपकी मेमोरी चिंताओं के आधार पर होने की तुलना में बड़ा होना चाहिए (निश्चित रूप से बेहतर)।

यदि आप अधिक रुचि रखते हैं तो यहां कुछ लिंक दिए गए हैं:

wikipedia पर हैशिंग

विकिपीडिया में विभिन्न टक्कर संकल्प विधियों का सारांश भी है :

इसके अलावा, थारप द्वारा " फाइल ऑर्गनाइजेशन एंड प्रोसेसिंग " बड़े पैमाने पर टक्कर रिज़ॉल्यूशन के तरीकों को शामिल करता है। IMO यह हैशिंग एल्गोरिदम के लिए एक महान संदर्भ है।


1

प्रोग्रामाइज वेबसाइट__hash__ पर फ़ंक्शन को कब और कैसे लागू किया जाए, इस पर एक बहुत अच्छी व्याख्या :

बस एक स्क्रीनशॉट एक सिंहावलोकन प्रदान करने के लिए: (2019-12-13 को पुनःप्राप्त)

Https://www.programiz.com/python-programming/methods/built-in/hash9/9-13-13 का स्क्रीनशॉट

विधि के व्यक्तिगत कार्यान्वयन के लिए, उपर्युक्त साइट एक उदाहरण प्रदान करती है जो मिलरदेव के उत्तर से मेल खाती है ।

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

आपके द्वारा लौटाए गए हैश मान के आकार पर निर्भर करता है। यह सरल तर्क है कि यदि आपको चार 32 बिट्स के हैश के आधार पर एक 32 बिट इंटों को वापस करने की आवश्यकता है, तो आपको टक्कर मिलने वाली है।

मैं बिट ऑपरेशंस का पक्ष लूंगा। जैसे, निम्नलिखित C छद्म कोड:

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

इस तरह की प्रणाली फ्लोट्स के लिए भी काम कर सकती है, अगर आपने उन्हें वास्तव में फ्लोटिंग-पॉइंट वैल्यू का प्रतिनिधित्व करने के बजाय उनके बिट वैल्यू के रूप में लिया है, तो बेहतर होगा।

स्ट्रिंग्स के लिए, मुझे बहुत कम / कोई विचार नहीं मिला है।


मुझे पता है कि टकराव होंगे। लेकिन मुझे कोई सुराग नहीं है कि ये कैसे संभाले जाते हैं। और मेरे संयोजन में मेरे विशेषता मूल्यों को बहुत कम रूप से वितरित किया जाता है इसलिए मैं एक स्मार्ट समाधान की तलाश में था। और किसी तरह मुझे उम्मीद थी कि कहीं न कहीं सबसे अच्छा अभ्यास होगा।
user229898
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.