ทำไม Python dict จึงมีหลายคีย์ที่มีแฮชเดียวกัน


92

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

class C:
    def __hash__(self):
        return 42

ฉันเพิ่งสันนิษฐานว่าอินสแตนซ์เดียวของคลาสข้างต้นสามารถอยู่ในdictช่วงเวลาใดก็ได้ แต่ในความเป็นจริงdictสามารถมีหลายองค์ประกอบที่มีแฮชเดียวกัน

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

ฉันทดลองเพิ่มเติมอีกเล็กน้อยและพบว่าถ้าฉันแทนที่__eq__วิธีการเพื่อให้อินสแตนซ์ทั้งหมดของคลาสเปรียบเทียบเท่ากันจะมีdictเพียงอินสแตนซ์เดียวเท่านั้นที่อนุญาต

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

ดังนั้นฉันจึงอยากรู้ว่า a dictcan มีหลายองค์ประกอบที่มีแฮชเดียวกันได้อย่างไร


3
ในขณะที่คุณค้นพบตัวเองชุดและคำสั่งสามารถมีหลายวัตถุที่มีแฮชเท่ากันหากวัตถุนั้นไม่เท่ากัน คุณจะถามอะไร? ตารางทำงานอย่างไร? นั่นเป็นคำถามทั่วไปที่มีเนื้อหามากมาย ...

@ เดลแนนฉันคิดถึงเรื่องนี้มากขึ้นหลังจากโพสต์คำถาม ว่าพฤติกรรมนี้ไม่สามารถ จำกัด เฉพาะ Python และคุณพูดถูก ฉันเดาว่าฉันควรเจาะลึกลงไปในวรรณกรรมเกี่ยวกับตารางแฮชทั่วไป ขอบคุณ.
Praveen Gollakota

คำตอบ:


58

สำหรับคำอธิบายโดยละเอียดเกี่ยวกับวิธีการทำงานของการแฮชของ Python โปรดดูคำตอบของฉันว่าเหตุใดการส่งคืนก่อนเวลาจึงช้ากว่าที่อื่น

โดยทั่วไปจะใช้แฮชเพื่อเลือกช่องในตาราง หากมีค่าในสล็อตและแฮชตรงกันระบบจะเปรียบเทียบรายการเพื่อดูว่ามีค่าเท่ากันหรือไม่

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

สิ่งสำคัญที่ควรทราบก็คือไม่มีรายการหรือที่เก็บข้อมูล: มีเพียงตารางแฮชที่มีจำนวนสล็อตเฉพาะและแต่ละแฮชจะใช้เพื่อสร้างลำดับของสล็อตผู้สมัคร


7
ขอขอบคุณที่ชี้ทางที่ถูกต้องเกี่ยวกับการใช้งานตารางแฮช ฉันอ่านมากขึ้นกว่าที่ฉันเคยต้องการเกี่ยวกับตารางแฮชและฉันอธิบายสิ่งที่ค้นพบด้วยคำตอบแยกต่างหาก stackoverflow.com/a/9022664/553995
Praveen Gollakota

117

นี่คือทุกอย่างเกี่ยวกับ Python dicts ที่ฉันสามารถรวบรวมได้ (อาจมีมากกว่าที่ใคร ๆ ก็อยากรู้ แต่คำตอบนั้นครอบคลุม) ตะโกนบอกDuncanเพื่อชี้ให้เห็นว่า Python ใช้ช่องและพาฉันไปที่โพรงกระต่ายนี้

  • พจนานุกรมหลามจะดำเนินการตามตารางแฮช
  • ตารางแฮชต้องอนุญาตให้มีการชนกันของแฮชกล่าวคือแม้ว่าสองคีย์จะมีค่าแฮชเท่ากันก็ตามการใช้งานตารางจะต้องมีกลยุทธ์ในการแทรกและดึงคู่คีย์และค่าอย่างไม่น่าสงสัย
  • Python dict ใช้การกำหนดแอดเดรสแบบเปิดเพื่อแก้ไขปัญหาการชนกันของแฮช (อธิบายด้านล่าง) (ดูdictobject.c: 296-297 )
  • ตารางแฮชของ Python เป็นเพียงบล็อกหน่วยความจำที่จำเป็น (เรียงลำดับเหมือนอาร์เรย์ดังนั้นคุณสามารถO(1)ค้นหาด้วยดัชนี)
  • แต่ละช่องในตารางสามารถจัดเก็บได้หนึ่งรายการเท่านั้น นี้เป็นสิ่งสำคัญ
  • แต่ละรายการในตารางเป็นการรวมกันของค่าทั้งสาม -. สิ่งนี้ถูกนำไปใช้เป็นโครงสร้าง C (ดูdictobject.h: 51-56 )
  • รูปด้านล่างนี้เป็นการแสดงตรรกะของตาราง python hash ในรูปด้านล่าง 0, 1, ... , i, ... ทางด้านซ้ายคือดัชนีของช่องในตารางแฮช (ใช้เพื่อวัตถุประสงค์ในการอธิบายเท่านั้นและไม่ได้จัดเก็บไว้พร้อมกับตารางอย่างชัดเจน!)

    # Logical model of Python Hash table
    -+-----------------+
    0| <hash|key|value>|
    -+-----------------+
    1|      ...        |
    -+-----------------+
    .|      ...        |
    -+-----------------+
    i|      ...        |
    -+-----------------+
    .|      ...        |
    -+-----------------+
    n|      ...        |
    -+-----------------+
    
  • เมื่อ Dict ใหม่จะเริ่มต้นจะเริ่มต้นด้วย 8 ช่อง (ดูdictobject.h: 49 )

  • เมื่อเพิ่มรายการลงในตารางเราเริ่มต้นด้วยสล็อตบางส่วนiซึ่งขึ้นอยู่กับแฮชของคีย์ CPython i = hash(key) & maskใช้ครั้งแรก ที่ไหนmask = PyDictMINSIZE - 1แต่นั่นไม่สำคัญจริงๆ) เพียงสังเกตว่าสล็อตเริ่มต้น i ที่ตรวจสอบนั้นขึ้นอยู่กับแฮชของคีย์
  • หากช่องนั้นว่างรายการจะถูกเพิ่มลงในช่อง (โดยรายการฉันหมายถึง<hash|key|value>) แต่ถ้าช่องนั้นถูกครอบครองล่ะ!? ส่วนใหญ่เป็นเพราะรายการอื่นมีแฮชเหมือนกัน (แฮชชนกัน!)
  • หากช่องถูกครอบครอง CPython (และแม้แต่ PyPy) จะเปรียบเทียบแฮชและคีย์ (โดยการเปรียบเทียบฉันหมายถึง==การเปรียบเทียบไม่ใช่การisเปรียบเทียบ) ของรายการในสล็อตกับคีย์ของรายการปัจจุบันที่จะแทรก ( dictobject.c: 337 , 344-345 ) หากทั้งคู่ตรงกันแสดงว่ามีรายการอยู่แล้วให้ขึ้นและย้ายไปยังรายการถัดไปที่จะแทรก หากทั้งสองกัญชาหรือคีย์ไม่ตรงก็จะเริ่มแหย่
  • การตรวจสอบหมายความว่ามันค้นหาช่องตามช่องเพื่อค้นหาช่องว่าง ในทางเทคนิคเราสามารถไปทีละตัว i + 1, i + 2, ... และใช้อันแรกที่มี (นั่นคือการตรวจสอบเชิงเส้น) แต่สำหรับเหตุผลที่อธิบายได้อย่างสวยงามในการแสดงความคิดเห็น (ดูdictobject.c: 33-126 ) CPython ใช้สุ่มละเอียด ในการตรวจสอบแบบสุ่มช่องถัดไปจะถูกเลือกตามลำดับสุ่มหลอก รายการจะถูกเพิ่มลงในช่องว่างแรก สำหรับการสนทนานี้อัลกอริทึมจริงที่ใช้ในการเลือกช่องถัดไปไม่สำคัญจริงๆ (ดูdictobject.c: 33-126สำหรับอัลกอริทึมสำหรับการตรวจสอบ) สิ่งที่สำคัญคือช่องจะถูกตรวจสอบจนกว่าจะพบช่องว่างแรก
  • สิ่งเดียวกันนี้เกิดขึ้นสำหรับการค้นหาเพียงแค่เริ่มต้นด้วยสล็อตเริ่มต้น i (โดยที่ฉันขึ้นอยู่กับแฮชของคีย์) หากแฮชและคีย์ทั้งคู่ไม่ตรงกับรายการในสล็อตเครื่องจะเริ่มการตรวจสอบจนกว่าจะพบช่องที่ตรงกัน หากช่องทั้งหมดหมดจะรายงานว่าล้มเหลว
  • BTW คำสั่งจะถูกปรับขนาดหากเต็มสองในสาม วิธีนี้หลีกเลี่ยงการชะลอการค้นหา (ดูdictobject.h: 64-65 )

ไปเลย! การใช้งาน Python ของ dict จะตรวจสอบความเท่าเทียมกันของแฮชของสองคีย์และความเท่าเทียมกันปกติ ( ==) ของคีย์เมื่อแทรกรายการ ดังนั้นโดยสรุปหากมีสองคีย์aและbและhash(a)==hash(b)แต่a!=bทั้งสองสามารถมีอยู่อย่างกลมกลืนกันใน Python dict แต่ถ้าhash(a)==hash(b) และ a==bแล้วพวกเขาก็ไม่สามารถทั้งสองจะอยู่ใน Dict เดียวกัน

เนื่องจากเราต้องตรวจสอบทุกครั้งที่แฮชชนกันผลข้างเคียงอย่างหนึ่งของการชนแฮชมากเกินไปคือการค้นหาและการแทรกจะช้ามาก (ดังที่ Duncan ชี้ให้เห็นในความคิดเห็น )

ฉันเดาว่าคำตอบสั้น ๆ สำหรับคำถามของฉันคือ "เพราะนั่นคือวิธีการใช้งานในซอร์สโค้ด;)"

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


9
สิ่งนี้อธิบายวิธีการเติมพจนานุกรม แต่จะเกิดอะไรขึ้นถ้ามีการชนกันของแฮชระหว่างการดึงคู่ key_value สมมติว่าเรามีออบเจ็กต์ A และ B 2 ชิ้นซึ่งทั้งสองมีแฮชถึง 4 ดังนั้นอันดับแรก A จึงถูกกำหนดสล็อต 4 จากนั้น B จะถูกกำหนดสล็อตผ่านการตรวจสอบแบบสุ่ม จะเกิดอะไรขึ้นเมื่อฉันต้องการดึง B. B hashes เป็น 4 ดังนั้น python จะตรวจสอบช่อง 4 ก่อน แต่คีย์ไม่ตรงกันจึงไม่สามารถส่งคืน A ได้เนื่องจากช่องของ B ถูกกำหนดโดยการตรวจสอบแบบสุ่ม B จะกลับมาอีกครั้งได้อย่างไร ใน O (1) ครั้ง?
sayantankhan

4
@ Bolt64 การสุ่มตรวจไม่ได้สุ่มจริงๆ สำหรับค่าคีย์เดียวกันจะเป็นไปตามลำดับของโพรบเดียวกันเสมอดังนั้นในที่สุดก็จะพบว่าพจนานุกรม B ไม่รับประกันว่าจะเป็น O (1) หากคุณมีการชนกันมากอาจใช้เวลานานกว่านี้ ด้วย Python เวอร์ชันเก่ามันง่ายที่จะสร้างชุดของคีย์ที่จะชนกันและในกรณีนั้นการค้นหาพจนานุกรมจะกลายเป็น O (n) นี่เป็นเวกเตอร์ที่เป็นไปได้สำหรับการโจมตี DoS ดังนั้น Python เวอร์ชันใหม่กว่าจึงปรับเปลี่ยนการแฮชเพื่อให้จงใจทำสิ่งนี้ได้ยากขึ้น
Duncan

3
@ ดันแคนจะเกิดอะไรขึ้นถ้า A ถูกลบแล้วเราทำการค้นหาบน B? ฉันเดาว่าคุณไม่ได้ลบรายการ แต่ทำเครื่องหมายว่าลบแล้ว? นั่นหมายความว่าคำสั่งไม่เหมาะสำหรับการแทรกและการลบแบบต่อเนื่อง ....
gen-ys

2
@ gen-ys ใช่ลบและไม่ได้ใช้จะได้รับการจัดการที่แตกต่างกันสำหรับการค้นหา ไม่ได้ใช้หยุดการค้นหารายการที่ตรงกัน แต่ลบไม่ได้ เมื่อแทรกลบหรือไม่ได้ใช้จะถือว่าเป็นช่องว่างที่สามารถใช้ได้ การแทรกและการลบแบบต่อเนื่องทำได้ดี เมื่อจำนวนช่องที่ไม่ได้ใช้ (ไม่ได้ลบ) ลดลงต่ำเกินไปตารางแฮชจะถูกสร้างขึ้นใหม่ในลักษณะเดียวกับว่ามันมีขนาดใหญ่เกินไปสำหรับตารางปัจจุบัน
Duncan

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

20

แก้ไข : คำตอบดังต่อไปนี้เป็นหนึ่งในวิธีที่เป็นไปได้ที่จะจัดการกับชนกัญชามันเป็นอย่างไรไม่ว่างูใหญ่ไม่ได้ วิกิพีเดียของ Python ที่อ้างถึงด้านล่างก็ไม่ถูกต้องเช่นกัน แหล่งที่มาที่ดีที่สุดที่ได้รับจาก @Duncan ด้านล่างนี้คือการนำไปใช้งานเอง: https://github.com/python/cpython/blob/master/Objects/dictobject.cฉันขอโทษสำหรับการผสมผสาน


มันเก็บรายการ (หรือถัง) ขององค์ประกอบที่แฮชแล้ววนซ้ำผ่านรายการนั้นจนกว่าจะพบคีย์จริงในรายการนั้น รูปภาพพูดได้มากกว่าหนึ่งพันคำ:

ตารางแฮช

คุณอยู่ที่นี่John Smithและทั้งกัญชาไปSandra Dee 152Bucket 152มีทั้งสองอย่าง เมื่อค้นหาSandra Deeครั้งแรกจะพบรายการในที่เก็บข้อมูล152จากนั้นจึงวนรอบรายการนั้นจนกว่าSandra Deeจะพบและกลับ521-6955มา

สิ่งต่อไปนี้ผิดเฉพาะที่นี่สำหรับบริบท:ในวิกิพีเดียของ Pythonคุณสามารถค้นหาโค้ด (หลอก?) ว่า Python ดำเนินการค้นหาอย่างไร

มีวิธีแก้ปัญหาที่เป็นไปได้หลายประการสำหรับปัญหานี้โปรดดูบทความวิกิพีเดียสำหรับภาพรวมที่ดี: http://en.wikipedia.org/wiki/Hash_table#Collision_resolution


ขอบคุณสำหรับคำอธิบายและโดยเฉพาะอย่างยิ่งสำหรับลิงก์ไปยังรายการ Python wiki ด้วยรหัสหลอก!
Praveen Gollakota

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

@Duncan วิกิพีเดียของ Python กล่าวว่ามีการใช้งานด้วยวิธีนี้ ฉันยินดีที่จะหาแหล่งที่ดีกว่านี้ หน้า wikipedia.org ไม่ผิดแน่นอนเป็นเพียงหนึ่งในวิธีแก้ปัญหาที่เป็นไปได้ตามที่ระบุไว้
Rob Wouters

@ ดันแคนช่วยอธิบายหน่อยได้ไหมว่า ... ดึงส่วนที่ไม่ได้ใช้แฮชให้นานที่สุด แฮชทั้งหมดในกรณีของฉันประเมินเป็น 42 ขอบคุณ!
Praveen Gollakota

@PraveenGollakota ตามลิงค์ในคำตอบของฉันซึ่งอธิบายในรายละเอียดเกี่ยวกับการใช้แฮช สำหรับแฮช 42 และตารางที่มี 8 สล็อตเริ่มแรกจะใช้เพียง 3 บิตต่ำสุดเท่านั้นในการค้นหาสล็อตหมายเลข 2 แต่ถ้าสล็อตนั้นถูกใช้ไปแล้วบิตที่เหลือจะเข้ามาเล่น หากค่าสองค่ามีแฮชเหมือนกันทุกประการค่าแรกจะเข้าไปในช่องแรกและค่าที่สองจะได้รับช่องถัดไป หากมีค่า 1,000 ค่าที่มีแฮชที่เหมือนกันเราจะลองใช้ 1,000 ช่องก่อนที่เราจะพบค่าและการค้นหาพจนานุกรมจะช้ามาก !
Duncan

4

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

เมื่อเกิดเหตุการณ์นี้ประสิทธิภาพของคุณจะลดลงเมื่อเวลาผ่านไปซึ่งเป็นสาเหตุที่คุณต้องการให้ฟังก์ชันแฮชของคุณเป็นแบบ "สุ่มมากที่สุด"


2

ในเธรดฉันไม่เห็นว่า python ทำอะไรกับอินสแตนซ์ของคลาสที่ผู้ใช้กำหนดเองเมื่อเราใส่ลงในพจนานุกรมเป็นคีย์ มาอ่านเอกสารประกอบกันเถอะ: มีการประกาศว่าเฉพาะวัตถุที่สามารถใช้งานได้เป็นกุญแจ Hashable เป็นคลาสในตัวที่ไม่เปลี่ยนรูปและคลาสที่ผู้ใช้กำหนดเองทั้งหมด

คลาสที่ผู้ใช้กำหนดเองจะมีเมธอด __cmp __ () และ __hash __ () ตามค่าเริ่มต้น กับพวกเขาวัตถุทั้งหมดเปรียบเทียบไม่เท่ากัน (ยกเว้นกับตัวเอง) และ x .__ แฮช __ () ส่งคืนผลลัพธ์ที่ได้มาจาก id (x)

ดังนั้นหากคุณมี __hash__ อย่างต่อเนื่องในชั้นเรียน แต่ไม่มีเมธอด __cmp__ หรือ __eq__ อินสแตนซ์ทั้งหมดของคุณจะไม่เท่ากันสำหรับพจนานุกรม ในทางกลับกันหากคุณระบุเมธอด __cmp__ หรือ __eq__ แต่ไม่ได้ระบุ __hash__ อินสแตนซ์ของคุณจะยังไม่เท่ากันในแง่ของพจนานุกรม

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)

เอาต์พุต

{<__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}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.