ไม่มีใครรู้วิธีการใช้งานพจนานุกรมในตัวสำหรับไพ ธ อน? ความเข้าใจของฉันคือมันเป็นตารางแฮช แต่ฉันไม่สามารถหาคำตอบที่ชัดเจนได้
ไม่มีใครรู้วิธีการใช้งานพจนานุกรมในตัวสำหรับไพ ธ อน? ความเข้าใจของฉันคือมันเป็นตารางแฮช แต่ฉันไม่สามารถหาคำตอบที่ชัดเจนได้
คำตอบ:
นี่คือทุกอย่างเกี่ยวกับ Python ที่ฉันสามารถรวบรวมได้ (อาจจะมากกว่าที่ทุกคนต้องการทราบ แต่คำตอบนั้นครอบคลุม)
dict
ใช้การกำหนดแอดเดรสแบบเปิดเพื่อแก้ไขการชนกันของแฮช (อธิบายไว้ด้านล่าง) (ดูdictobject.c: 296-297 )O(1)
ค้นหาโดยใช้ดัชนี)รูปด้านล่างเป็นการแสดงตรรกะของตารางแฮช 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>
) แต่ถ้าช่องนั้นว่าง! เป็นไปได้มากเนื่องจากรายการอื่นมีแฮชเดียวกัน (การชนกันของแฮช!)==
การเปรียบเทียบไม่ใช่การis
เปรียบเทียบ) ของรายการในช่องกับแฮชและคีย์ของรายการปัจจุบันที่จะแทรก ( dictobject.c : 337,344-345 ) ตามลำดับ หากทั้งคู่ตรงกันจะคิดว่ามีรายการอยู่แล้วยกเลิกและย้ายไปยังรายการถัดไปที่จะแทรก หากทั้งสองกัญชาหรือคีย์ไม่ตรงก็จะเริ่มแหย่i+1, i+2, ...
และใช้อันแรกที่มี (นั่นคือการวัดเชิงเส้น) แต่สำหรับเหตุผลที่อธิบายได้อย่างสวยงามในการแสดงความคิดเห็น (ดูdictobject.c: 33-126 ) CPython ใช้สุ่มละเอียด ในการตรวจสอบแบบสุ่มสล็อตถัดไปจะถูกเลือกโดยการสุ่มหลอก รายการจะถูกเพิ่มในช่องว่างแรก สำหรับการสนทนานี้อัลกอริทึมจริงที่ใช้ในการเลือกสล็อตถัดไปนั้นไม่สำคัญ (ดูdictobject.c: 33-126สำหรับอัลกอริทึมสำหรับการตรวจสอบ) สิ่งสำคัญคือสล็อตจะถูกตรวจสอบจนกว่าจะพบสล็อตว่างก่อนdict
จะถูกปรับขนาดหากเต็มสองในสาม วิธีนี้จะทำให้การค้นหาช้าลง (ดูdictobject.h: 64-65 )หมายเหตุ: ฉันได้ทำการวิจัยเกี่ยวกับการนำ Pict Dict มาใช้เพื่อตอบคำถามของฉันเองว่ารายการหลายรายการใน Dict สามารถมีค่าแฮชเดียวกันได้อย่างไร ฉันโพสต์รุ่นแก้ไขเล็กน้อยของการตอบสนองที่นี่เพราะการวิจัยทั้งหมดมีความเกี่ยวข้องมากสำหรับคำถามนี้เช่นกัน
มีการใช้พจนานุกรมในตัวของ Python อย่างไร?
นี่คือหลักสูตรระยะสั้น:
ด้านที่ได้รับคำสั่งเป็นทางการในฐานะของงูใหญ่ 3.6 (เพื่อให้การใช้งานอื่น ๆ ที่มีโอกาสที่จะให้ทัน) แต่อย่างเป็นทางการในหลาม 3.7
เป็นเวลานานมันทำงานอย่างนี้ 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 ใหม่แต่ละกระบวนการ
พื้นที่ที่สูญเปล่าดังที่อธิบายไว้ข้างต้นทำให้เราปรับเปลี่ยนการใช้งานพจนานุกรมด้วยคุณลักษณะใหม่ที่น่าตื่นเต้นที่พจนานุกรมสั่งให้วางโดยการแทรก
เราเริ่มต้นแทนโดยจัดสรรล่วงหน้าสำหรับดัชนีของการแทรก
เนื่องจากคู่คีย์ - ค่าแรกของเราไปในสล็อตที่สองเราจึงทำดัชนีดังนี้:
[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__
, ดูคำตอบของฉันที่นี่
**kwargs
ในฟังก์ชั่นfind_empty_slot
: github.com/python/cpython/cbython/blob/master/Objects/dictobject.c # L969 - และเริ่มต้นที่บรรทัดที่ 134 มีร้อยแก้วที่อธิบายไว้
Python Dictionaries ใช้Open addressing ( อ้างอิงจากโค้ดที่สวยงาม )
NB! การพูดถึงที่อยู่แบบเปิดหรือปิดการแฮ็ชแบบปิดควรดังที่กล่าวไว้ในวิกิพีเดียอย่าสับสนกับการแฮ็กแบบเปิดที่ตรงกันข้าม!
การกำหนดแอดเดรสแบบเปิดหมายความว่า dict ใช้สล็อตอาเรย์และเมื่อตำแหน่งหลักของวัตถุถูกใช้ใน dict จุดค้นหาของวัตถุจะถูกค้นหาที่ดัชนีที่แตกต่างกันในอาเรย์เดียวกันโดยใช้รูปแบบ "การก่อกวน" ซึ่งค่าแฮชของวัตถุ .