วิธีที่ถูกต้องและดีในการติดตั้ง __hash __ () คืออะไร


150

วิธีที่ถูกต้องและดีในการใช้__hash__()คืออะไร?

ฉันกำลังพูดถึงฟังก์ชั่นที่คืนค่าแฮชโค้ดที่ใช้แทรกวัตถุลงในพจนานุกรมแฮชเทเบิลหรือที่รู้จักกันในชื่อ

เมื่อ__hash__()คืนค่าจำนวนเต็มและใช้สำหรับวัตถุ "binning" ใน hashtables ฉันคิดว่าค่าของจำนวนเต็มที่ส่งคืนควรกระจายอย่างสม่ำเสมอสำหรับข้อมูลทั่วไป (เพื่อลดการชน) การปฏิบัติที่ดีในการรับค่าดังกล่าวคืออะไร การชนเป็นปัญหาหรือไม่ ในกรณีของฉันฉันมีชั้นเรียนขนาดเล็กซึ่งทำหน้าที่เป็นคลาสคอนเทนเนอร์ที่มี ints บางอย่างลอยบางและสตริง

คำตอบ:


185

วิธีที่ง่ายและถูกต้องในการนำไปใช้__hash__()คือการใช้คีย์ทูเปิล มันจะไม่เร็วเท่ากับแฮชแบบพิเศษ แต่ถ้าคุณต้องการสิ่งนั้นคุณควรใช้ชนิดใน C

นี่คือตัวอย่างของการใช้คีย์สำหรับแฮชและความเท่าเทียมกัน:

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))กำลังจะเกิดขึ้นอย่างรวดเร็วอย่างน่าประหลาดใจ (และถูกต้อง ) เนื่องจากการสร้างtuples ขนาดเล็กนั้นได้รับการปรับแต่งเป็นพิเศษและจะผลักดันการทำงานของการรวมและแฮชเข้ากับ C builtins ซึ่งโดยทั่วไปแล้วจะเร็วกว่ารหัสระดับ Python
ShadowRanger

สมมติว่ามีการใช้วัตถุของคลาส A เป็นคีย์สำหรับพจนานุกรมและหากแอตทริบิวต์ของคลาส A เปลี่ยนแปลงค่าแฮชของมันก็จะเปลี่ยนเช่นกัน มันจะไม่สร้างปัญหาเหรอ?
นายเมทริกซ์

1
ตามคำตอบของ @ loves.by.Jesus ด้านล่างกล่าวถึงวิธีแฮชไม่ควรกำหนด / เขียนทับสำหรับวัตถุที่ไม่แน่นอน (กำหนดโดยค่าเริ่มต้นและใช้รหัสเพื่อความเท่าเทียมกันและการเปรียบเทียบ)
นายเมทริกซ์

@Miguel ฉันพบปัญหาแน่นอนสิ่งที่เกิดขึ้นคือพจนานุกรมส่งคืนNoneเมื่อการเปลี่ยนแปลงที่สำคัญ วิธีที่ฉันแก้ไขมันคือการจัดเก็บ id ของวัตถุเป็นกุญแจแทนที่จะเป็นแค่วัตถุ
Jaswant P

@JaswantP Python โดยค่าเริ่มต้นใช้ id ของวัตถุเป็นกุญแจสำคัญสำหรับวัตถุ hashable ใด ๆ
นายเมทริกซ์

22

John Millikin เสนอวิธีแก้ปัญหาคล้ายกับ:

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))ปัญหาเกี่ยวกับการแก้ปัญหานี้ก็คือว่า กล่าวอีกนัยหนึ่งแฮชชนกับสิ่งอันดับของสมาชิกหลัก บางทีนี่อาจไม่สำคัญในทางปฏิบัติบ่อยนัก?

อัปเดต: ตอนนี้เอกสาร Python แนะนำให้ใช้สิ่งอันดับในตัวอย่างด้านบน โปรดทราบว่าเอกสารประกอบระบุ

คุณสมบัติที่จำเป็นเท่านั้นคือวัตถุที่เปรียบเทียบเท่ากับมีค่าแฮชเดียวกัน

โปรดทราบว่าสิ่งที่ตรงกันข้ามไม่เป็นความจริง วัตถุที่ไม่ได้เปรียบเทียบกันอาจมีค่าแฮชเหมือนกัน ดังกล่าวชนกัญชาจะไม่ได้เป็นสาเหตุหนึ่งวัตถุอื่นมาแทนเมื่อใช้เป็นคีย์ Dict หรือองค์ประกอบชุดตราบเท่าที่วัตถุที่ทำไม่ได้นอกจากนี้ยังเปรียบเทียบเท่ากับ

ทางออกที่ล้าสมัย / ไม่ดี

เอกสารหลามบน__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)))

อัปเดต: ตามที่ Blckknght ชี้ให้เห็นการเปลี่ยนลำดับของ a, b และ c อาจทำให้เกิดปัญหา ฉันเพิ่มส่วนเพิ่มเติม^ hash((self._a, self._b, self._c))เพื่อจับลำดับของค่าที่ถูกแฮช ขั้นสุดท้ายนี้^ hash(...)สามารถลบออกได้หากค่าที่รวมกันไม่สามารถจัดเรียงใหม่ได้ (ตัวอย่างเช่นหากมีประเภทที่แตกต่างกันดังนั้นค่าของ_aจะไม่ถูกกำหนดให้กับ_bหรือ_cเป็นต้น)


5
โดยปกติแล้วคุณไม่ต้องการที่จะทำ XOR ตรงเข้าด้วยกันเพราะมันจะทำให้คุณชนกันถ้าคุณเปลี่ยนลำดับของค่า นั่นคือhash(A(1, 2, 3))จะเท่ากับhash(A(3, 1, 2))(และพวกเขาทั้งสองจะกัญชาเท่ากับอื่น ๆAเช่นมีการเปลี่ยนแปลงของ1, 2และ3เป็นค่าของมัน) หากคุณต้องการหลีกเลี่ยงอินสแตนซ์ของคุณที่มีแฮชเหมือนกับ tuple ของข้อโต้แย้งเพียงแค่สร้างค่า Sentinel (เป็นตัวแปรคลาสหรือระดับโลก) จากนั้นรวมไว้ใน tuple ที่จะแฮช: return hash ((_ sentinel , self._a, self._b, self._c))
Blckknght

1
การใช้งานของisinstanceอาจจะมีปัญหาเนื่องจากวัตถุของคลาสย่อยของหนึ่งขณะนี้คุณสามารถเท่ากับวัตถุของtype(self) type(self)ดังนั้นคุณอาจพบว่าการเพิ่ม a Carและ a Fordไปยังset()อาจส่งผลให้มีเพียงวัตถุเดียวที่แทรกขึ้นอยู่กับคำสั่งของการแทรก นอกจากนี้คุณอาจพบสถานการณ์ที่a == bเป็นจริง แต่b == aเป็นเท็จ
MaratC

1
หากคุณเป็นคลาสย่อยBคุณอาจต้องการเปลี่ยนเป็นisinstance(othr, B)
millerdev

7
ความคิด: tuple ของคีย์อาจรวมถึงประเภทของคลาสซึ่งจะป้องกันไม่ให้คลาสอื่น ๆ ที่มีชุดคีย์ของแอตทริบิวต์เดียวกันแสดงให้เห็นว่ามีค่าเท่ากัน: hash((type(self), self._a, self._b, self._c)).
Ben Mosher

2
นอกจากประเด็นเกี่ยวกับการใช้Bแทนtype(self)ก็ยังมักจะคิดว่าการปฏิบัติที่ดีกว่าที่จะกลับมาNotImplementedเมื่อพบชนิดที่ไม่คาดคิดในแทน__eq__ Falseที่ช่วยให้ผู้ใช้กำหนดประเภทอื่น ๆเพื่อใช้งาน__eq__ที่รู้เกี่ยวกับBและสามารถเปรียบเทียบเท่ากับถ้าพวกเขาต้องการ
Mark Amery

16

Paul Larson จาก Microsoft Research ศึกษาฟังก์ชั่นแฮชที่หลากหลาย เขาบอกฉันว่า

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

ทำงานได้ดีอย่างน่าประหลาดใจสำหรับสตริงที่หลากหลาย ฉันพบว่าเทคนิคพหุนามที่คล้ายคลึงกันทำงานได้ดีสำหรับการคำนวณแฮชของฟิลด์ย่อยที่แตกต่างกัน


8
เห็นได้ชัดว่า Java ใช้วิธีเดียวกัน แต่ใช้ 31 แทน 101
user229898

3
เหตุผลเบื้องหลังการใช้ตัวเลขเหล่านี้คืออะไร? มีเหตุผลให้เลือก 101 หรือ 31 หรือไม่?
bigblind

1
นี่คือคำอธิบายสำหรับตัวคูณที่สำคัญ: stackoverflow.com/questions/3613102/... 101 ดูเหมือนจะทำงานได้ดีโดยเฉพาะอย่างยิ่งจากการทดลองของ Paul Larson
George V. Reilly

4
Python ใช้(hash * 1000003) XOR ord(c)สำหรับสตริงที่มีการทวีคูณแบบ 32 บิต [การอ้างอิง ]
tylerl

4
แม้ว่าสิ่งนี้จะเป็นความจริง แต่ก็ไม่มีประโยชน์ในบริบทนี้เนื่องจากชนิดสตริง Python ในตัวมี__hash__วิธีการใช้อยู่แล้ว เราไม่จำเป็นต้องม้วนตัวเอง คำถามคือวิธีการใช้งาน__hash__สำหรับคลาสที่ผู้ใช้กำหนดโดยทั่วไป (ที่มีคุณสมบัติมากมายที่ชี้ไปยังชนิดในตัวหรืออาจจะเป็นคลาสที่ผู้ใช้กำหนดอื่น ๆ ) ซึ่งคำตอบนี้ไม่ได้อยู่เลย
Mark Amery

3

ฉันสามารถลองตอบคำถามตอนที่สองของคุณได้

การชนกันอาจไม่ได้มาจากรหัสแฮช แต่จากการแมปรหัสแฮชกับดัชนีในคอลเลกชัน ตัวอย่างเช่นฟังก์ชันแฮชของคุณสามารถส่งกลับค่าสุ่มจาก 1 ถึง 10,000 แต่หากตารางแฮชของคุณมีเพียง 32 รายการคุณจะได้รับการชนจากการแทรก

นอกจากนี้ฉันคิดว่าการชนจะได้รับการแก้ไขโดยการรวบรวมภายในและมีหลายวิธีในการแก้ไขการชน ที่ง่ายที่สุด (และเลวร้ายที่สุด) คือให้รายการที่จะแทรกที่ดัชนีฉันเพิ่ม 1 ถึงฉันจนกว่าคุณจะหาจุดที่ว่างเปล่าและแทรกที่นั่น การสืบค้นจากนั้นใช้วิธีเดียวกัน สิ่งนี้ส่งผลให้มีการดึงข้อมูลที่ไม่มีประสิทธิภาพสำหรับบางรายการเนื่องจากคุณอาจมีรายการที่ต้องผ่านการรวบรวมทั้งหมดเพื่อค้นหา!

วิธีการแก้ไขปัญหาการชนกันอื่น ๆ ช่วยลดเวลาในการดึงข้อมูลด้วยการย้ายรายการในตารางแฮชเมื่อมีการแทรกรายการเพื่อกระจายสิ่งต่าง ๆ สิ่งนี้จะเพิ่มเวลาการแทรก แต่จะถือว่าคุณอ่านมากกว่าการแทรก นอกจากนี้ยังมีวิธีการที่พยายามและแยกรายการการชนที่แตกต่างกันออกไปเพื่อให้รายการไปยังคลัสเตอร์ในจุดเดียว

นอกจากนี้หากคุณต้องการปรับขนาดคอลเลกชันคุณจะต้องทำการปรับปรุงใหม่ทุกอย่างหรือใช้วิธีการแฮชแบบไดนามิก

ในระยะสั้นขึ้นอยู่กับสิ่งที่คุณใช้รหัสแฮชสำหรับคุณอาจต้องใช้วิธีแก้ไขการชนกันของคุณเอง หากคุณไม่ได้เก็บไว้ในคอลเลกชันคุณอาจจะสามารถใช้ฟังก์ชันแฮชที่เพิ่งสร้างรหัสแฮชในช่วงที่มีขนาดใหญ่มาก ถ้าเป็นเช่นนั้นคุณสามารถตรวจสอบให้แน่ใจว่าคอนเทนเนอร์ของคุณใหญ่กว่าที่ต้องการ (ยิ่งใหญ่ยิ่งดีขึ้นแน่นอน) ขึ้นอยู่กับความจำของคุณ

นี่คือลิงค์บางส่วนหากคุณสนใจมากขึ้น:

รวมตัวกัน hashing บนวิกิพีเดีย

Wikipedia ยังมีสรุปวิธีการแก้ปัญหาการชนกันหลายวิธี:

นอกจากนี้ "การจัดระเบียบไฟล์และการประมวลผล " โดย Tharp ยังครอบคลุมวิธีการแก้ไขปัญหาการชนกันอย่างมาก IMO มันเป็นข้อมูลอ้างอิงที่ดีสำหรับอัลกอริทึมการแปลงแป้นพิมพ์


1

คำอธิบายที่ดีมากเกี่ยวกับเวลาและวิธีการใช้__hash__ฟังก์ชั่นอยู่บนเว็บไซต์ programiz :

เพียงแค่ภาพหน้าจอเพื่อให้ภาพรวม: (ดึงข้อมูล 2019-12-13)

ภาพหน้าจอของ https://www.programiz.com/python-programming/methods/built-in/hash 2019-12-13

ในฐานะที่เป็นสำหรับการดำเนินงานของส่วนบุคคลของวิธีการที่เว็บไซต์ดังกล่าวข้างต้นแสดงให้เห็นตัวอย่างที่ตรงกับคำตอบของที่millerdev

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

ระบบดังกล่าวสามารถใช้งานกับการลอยได้เช่นกันถ้าคุณเอามันมาเป็นค่าบิตแทนที่จะเป็นค่าทศนิยมจริง ๆ อาจจะดีกว่า

สำหรับสตริงฉันมีความคิดน้อย / ไม่มีเลย


ฉันรู้ว่าจะมีการชนกัน แต่ฉันไม่มีเงื่อนงำวิธีจัดการเหล่านี้ และยิ่งกว่านั้นค่าคุณสมบัติของฉันในการรวมกันมีการกระจายเบาบางมากดังนั้นฉันจึงมองหาทางออกที่ชาญฉลาด และอย่างใดฉันคาดว่าจะมีการปฏิบัติที่ดีที่สุดที่ไหนสักแห่ง
229898
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.