มีการใช้พจนานุกรมในตัวของ Python อย่างไร?


294

ไม่มีใครรู้วิธีการใช้งานพจนานุกรมในตัวสำหรับไพ ธ อน? ความเข้าใจของฉันคือมันเป็นตารางแฮช แต่ฉันไม่สามารถหาคำตอบที่ชัดเจนได้


4
นี่คือการพูดคุยที่ลึกซึ้งเกี่ยวกับพจนานุกรม Python ตั้งแต่ 2.7 ถึง 3.6 Link
Sören

คำตอบ:


494

นี่คือทุกอย่างเกี่ยวกับ Python ที่ฉันสามารถรวบรวมได้ (อาจจะมากกว่าที่ทุกคนต้องการทราบ แต่คำตอบนั้นครอบคลุม)

  • พจนานุกรมหลามจะดำเนินการตามตารางแฮช
  • ตารางแฮชต้องอนุญาตให้มีการแฮชชนเช่นแม้ว่าสองคีย์ที่แตกต่างกันจะมีค่าแฮชเหมือนกัน แต่การใช้งานของตารางจะต้องมีกลยุทธ์ในการแทรกและดึงคู่คีย์และค่าอย่างไม่น่าสงสัย
  • Python dictใช้การกำหนดแอดเดรสแบบเปิดเพื่อแก้ไขการชนกันของแฮช (อธิบายไว้ด้านล่าง) (ดูdictobject.c: 296-297 )
  • ตารางแฮช Python เป็นเพียงบล็อกหน่วยความจำต่อเนื่อง (เรียงลำดับเหมือนอาร์เรย์ดังนั้นคุณสามารถO(1)ค้นหาโดยใช้ดัชนี)
  • แต่ละช่องในตารางสามารถจัดเก็บได้เพียงหนึ่งรายการเท่านั้น นี้เป็นสิ่งสำคัญ.
  • แต่ละรายการในตารางจริงรวมกันของทั้งสามค่า: <กัญชาที่สำคัญค่า> สิ่งนี้ถูกนำไปใช้เป็นโครงสร้าง C (ดูdictobject.h: 51-56 )
  • รูปด้านล่างเป็นการแสดงตรรกะของตารางแฮช Python ในรูปด้านล่าง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 dictจะถูกปรับขนาดหากเต็มสองในสาม วิธีนี้จะทำให้การค้นหาช้าลง (ดูdictobject.h: 64-65 )

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


8
คุณพูดว่าเมื่อทั้งแฮชและการจับคู่คีย์มัน (insert op) ยอมแพ้และเดินหน้าต่อไป ไม่ได้แทรกทับรายการที่มีอยู่ในกรณีนี้หรือไม่
0xc0de

65

มีการใช้พจนานุกรมในตัวของ Python อย่างไร?

นี่คือหลักสูตรระยะสั้น:

  • พวกเขาเป็นตารางแฮช (ดูด้านล่างสำหรับข้อมูลเฉพาะของการดำเนินการของ Python)
  • โครงร่างและอัลกอริทึมใหม่ใน Python 3.6 สร้างขึ้นมา
    • จัดเรียงโดยการแทรกกุญแจและ
    • ใช้พื้นที่น้อยลง
    • แทบไม่มีค่าใช้จ่ายในประสิทธิภาพ
  • การปรับให้เหมาะสมอีกอย่างช่วยประหยัดพื้นที่เมื่อ dicts แบ่งปันคีย์ (ในกรณีพิเศษ)

ด้านที่ได้รับคำสั่งเป็นทางการในฐานะของงูใหญ่ 3.6 (เพื่อให้การใช้งานอื่น ๆ ที่มีโอกาสที่จะให้ทัน) แต่อย่างเป็นทางการในหลาม 3.7

พจนานุกรมของ Python คือ Hash Tables

เป็นเวลานานมันทำงานอย่างนี้ Python จะจัดสรรล่วงหน้า 8 แถวที่ว่างเปล่าและใช้แฮชเพื่อกำหนดตำแหน่งที่จะติดคู่ของคีย์ - ค่า ตัวอย่างเช่นหากแฮชของคีย์สิ้นสุดลงใน 001 มันจะติดอยู่ในดัชนี 1 (เช่นที่ 2) (เช่นตัวอย่างด้านล่าง)

   <hash>       <key>    <value>
     null        null    null
...010001    ffeb678c    633241c4 # addresses of the keys and values
     null        null    null
      ...         ...    ...

แต่ละแถวใช้ 24 ไบต์บนสถาปัตยกรรม 64 บิต 12 ใน 32 บิต (โปรดทราบว่าส่วนหัวคอลัมน์เป็นเพียงป้ายกำกับสำหรับจุดประสงค์ของเราที่นี่ - ไม่มีอยู่จริงในหน่วยความจำ)

หากแฮชสิ้นสุดเหมือนกับแฮชของคีย์ที่มีมาก่อนนี่คือการชนกันและจากนั้นจะติดคู่คีย์ - ค่าในตำแหน่งอื่น

หลังจากเก็บคีย์ - ค่า 5 ค่าแล้วเมื่อเพิ่มคู่คีย์ - ค่าอีกคู่ความน่าจะเป็นของการชนแฮชมีขนาดใหญ่เกินไปดังนั้นพจนานุกรมจึงมีขนาดเป็นสองเท่า ในกระบวนการ 64 บิตก่อนการปรับขนาดเรามี 72 ไบต์ว่างเปล่าและหลังจากนั้นเราจะสูญเสีย 240 ไบต์เนื่องจากแถวว่าง 10 แถว

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

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

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

Compact Hash Tables ใหม่

เราเริ่มต้นแทนโดยจัดสรรล่วงหน้าสำหรับดัชนีของการแทรก

เนื่องจากคู่คีย์ - ค่าแรกของเราไปในสล็อตที่สองเราจึงทำดัชนีดังนี้:

[null, 0, null, null, null, null, null, null]

และตารางของเราเพิ่งได้รับการเติมโดยลำดับการแทรก:

   <hash>       <key>    <value>
...010001    ffeb678c    633241c4 
      ...         ...    ...

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

เรารักษาเวลาค้นหาอย่างต่อเนื่องโดยมีการสูญเสียความเร็วเล็กน้อยในบางกรณีและเพิ่มขึ้นในส่วนอื่น ๆ ด้วย Upsides ที่เราประหยัดพื้นที่ได้ค่อนข้างมากจากการใช้งานที่มีอยู่แล้วและเรายังคงคำสั่งแทรก พื้นที่ว่างที่สูญเปล่าคือไบต์ว่างในอาร์เรย์ดัชนี

เรย์มอนด์ Hettinger แนะนำนี้ในหลาม devในเดือนธันวาคมของปี 2012 ในที่สุดมันก็เข้าไปใน CPython ในPython 3.6 การสั่งซื้อโดยการแทรกได้รับการพิจารณาว่าเป็นรายละเอียดการใช้งานสำหรับ 3.6 เพื่อให้โอกาสในการติดตั้ง Python อื่น ๆ

คีย์ที่ใช้ร่วมกัน

การเพิ่มประสิทธิภาพอื่นเพื่อประหยัดพื้นที่คือการใช้งานที่แบ่งปันคีย์ ดังนั้นแทนที่จะมีพจนานุกรมที่ซ้ำซ้อนที่ใช้พื้นที่ทั้งหมดนั้นเรามีพจนานุกรมที่นำคีย์ที่ใช้ร่วมกันและแฮชของคีย์มาใช้ซ้ำ คุณสามารถคิดแบบนี้:

     hash         key    dict_0    dict_1    dict_2...
...010001    ffeb678c    633241c4  fffad420  ...
      ...         ...    ...       ...       ...

สำหรับเครื่อง 64 บิตสิ่งนี้สามารถบันทึกได้มากถึง 16 ไบต์ต่อคีย์ต่อพจนานุกรมเสริม

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

เหล่านี้ dicts ที่ใช้ร่วมกันที่สำคัญมีความตั้งใจที่จะใช้สำหรับวัตถุที่กำหนดเอง __dict__เพื่อให้ได้พฤติกรรมนี้ฉันเชื่อว่าคุณต้องเติมข้อมูลให้เสร็จ__dict__ก่อนที่คุณจะสร้างอินสแตนซ์วัตถุถัดไปของคุณ ( ดู PEP 412 ) ซึ่งหมายความว่าคุณควรกำหนดคุณลักษณะทั้งหมดของคุณใน__init__หรือ__new__มิฉะนั้นคุณอาจไม่ได้รับการประหยัดพื้นที่ของคุณ

อย่างไรก็ตามหากคุณรู้ว่าคุณลักษณะทั้งหมดของคุณในเวลาที่คุณ__init__ถูกประหารคุณสามารถจัดเตรียม__slots__สำหรับวัตถุของคุณและรับประกันว่า__dict__จะไม่ถูกสร้างขึ้นเลย (ถ้าไม่มีในผู้ปกครอง) หรือแม้กระทั่งอนุญาต__dict__แต่รับประกันว่าคุณลักษณะล่วงหน้า เก็บไว้ในช่องอย่างไรก็ตาม สำหรับข้อมูลเพิ่มเติมเกี่ยว__slots__, ดูคำตอบของฉันที่นี่

ดูสิ่งนี้ด้วย:


1
คุณพูดว่า "เรา" และ "เพื่อให้โอกาสในการติดตั้ง Python ของการใช้งานอื่น ๆ " นี่หมายความว่าคุณ "รู้อะไร" และนั่นอาจกลายเป็นคุณลักษณะถาวรหรือไม่ มีข้อเสียใด ๆ ที่ dicts ถูกสั่งโดยสเป็ค?
toonarmycaptain

ข้อเสียของการสั่งซื้อคือหากคาดว่าจะได้รับคำสั่งพวกเขาไม่สามารถสลับไปใช้การใช้งานที่ดีขึ้น / เร็วขึ้นซึ่งไม่ได้สั่ง ดูเหมือนว่าไม่น่าจะเป็นอย่างนั้น ฉัน "รู้เรื่อง" เพราะฉันดูการพูดคุยจำนวนมากและอ่านสิ่งต่าง ๆ มากมายที่เขียนโดยสมาชิกหลักและคนอื่น ๆ ที่มีชื่อเสียงในโลกแห่งความเป็นจริงที่ดีกว่าฉันดังนั้นแม้ว่าฉันจะไม่ได้มีแหล่งอ้างอิงทันที สิ่งที่ฉันพูดถึง แต่ฉันคิดว่าคุณสามารถรับประเด็นนั้นได้จากการพูดคุยของ Raymond Hettinger
Aaron Hall

1
คุณอธิบายได้ค่อนข้างคลุมเครือว่าการแทรกทำงานอย่างไร ("ถ้าแฮชสิ้นสุดเหมือนกับแฮชของคีย์ที่มีอยู่ก่อนหน้า ... แล้วมันจะติดคู่คีย์ - ค่าในตำแหน่งอื่น" - ก็ได้?) แต่คุณไม่ได้อธิบาย การค้นหาและการทดสอบการเป็นสมาชิกทำงานอย่างไร ยังไม่ชัดเจนว่าตำแหน่งถูกกำหนดโดยแฮชอย่างใดอย่างหนึ่ง แต่ฉันคิดว่าขนาดมักจะเป็นพลังของ 2 และคุณใช้เวลาสองสามบิตสุดท้ายของแฮช ...
Alexey

@Alexey ลิงก์สุดท้ายที่ฉันให้จะช่วยให้คุณสามารถนำไปใช้กับ dict ได้ซึ่งคุณสามารถค้นหาฟังก์ชั่นที่ทำได้ในบรรทัดปัจจุบัน 969 ​​ที่เรียกว่าfind_empty_slot: github.com/python/cpython/cbython/blob/master/Objects/dictobject.c # L969 - และเริ่มต้นที่บรรทัดที่ 134 มีร้อยแก้วที่อธิบายไว้
Aaron Hall

46

Python Dictionaries ใช้Open addressing ( อ้างอิงจากโค้ดที่สวยงาม )

NB! การพูดถึงที่อยู่แบบเปิดหรือปิดการแฮ็ชแบบปิดควรดังที่กล่าวไว้ในวิกิพีเดียอย่าสับสนกับการแฮ็กแบบเปิดที่ตรงกันข้าม!

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


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