วิธีที่ดีที่สุดในการใช้พจนานุกรมที่ซ้อนกันใน Python คืออะไร
นี่เป็นความคิดที่ไม่ดีอย่าทำ แต่ให้ใช้พจนานุกรมปกติและการใช้งานที่เรื่องดังนั้นเมื่อกุญแจหายไปภายใต้การใช้งานปกติคุณได้รับการคาดหวังdict.setdefault
KeyError
หากคุณยืนยันที่จะรับพฤติกรรมนี้ต่อไปนี้เป็นวิธียิงตัวคุณเองด้วยการเดินเท้า:
ใช้งาน__missing__
บนdict
คลาสย่อยเพื่อตั้งและส่งคืนอินสแตนซ์ใหม่
วิธีนี้ใช้ได้แล้ว (และบันทึกไว้)ตั้งแต่ Python 2.5 และ (โดยเฉพาะอย่างยิ่งมีค่าสำหรับฉัน) มันพิมพ์สวยเหมือนปกติ dictแทนที่จะพิมพ์น่าเกลียดของ autdivified defaultdict น่าเกลียด:
class Vividict(dict):
def __missing__(self, key):
value = self[key] = type(self)() # retain local pointer to value
return value # faster to return than dict lookup
(หมายเหตุself[key]
อยู่ที่ด้านซ้ายมือของการมอบหมายดังนั้นจึงไม่มีการเรียกซ้ำที่นี่)
และบอกว่าคุณมีข้อมูล:
data = {('new jersey', 'mercer county', 'plumbers'): 3,
('new jersey', 'mercer county', 'programmers'): 81,
('new jersey', 'middlesex county', 'programmers'): 81,
('new jersey', 'middlesex county', 'salesmen'): 62,
('new york', 'queens county', 'plumbers'): 9,
('new york', 'queens county', 'salesmen'): 36}
นี่คือรหัสการใช้งานของเรา:
vividict = Vividict()
for (state, county, occupation), number in data.items():
vividict[state][county][occupation] = number
และตอนนี้:
>>> import pprint
>>> pprint.pprint(vividict, width=40)
{'new jersey': {'mercer county': {'plumbers': 3,
'programmers': 81},
'middlesex county': {'programmers': 81,
'salesmen': 62}},
'new york': {'queens county': {'plumbers': 9,
'salesmen': 36}}}
คำวิจารณ์
บทวิจารณ์ของคอนเทนเนอร์ประเภทนี้คือถ้าผู้ใช้สะกดรหัสรหัสของเราอาจล้มเหลวอย่างเงียบ ๆ :
>>> vividict['new york']['queens counyt']
{}
และตอนนี้เรามีเขตที่สะกดผิดในข้อมูลของเรา:
>>> pprint.pprint(vividict, width=40)
{'new jersey': {'mercer county': {'plumbers': 3,
'programmers': 81},
'middlesex county': {'programmers': 81,
'salesmen': 62}},
'new york': {'queens county': {'plumbers': 9,
'salesmen': 36},
'queens counyt': {}}}
คำอธิบาย:
พวกเราแค่ให้ชั้นเรียนของเราแบบซ้อน ๆ Vividict
เมื่อใดก็ตามที่มีการเข้าถึงคีย์ แต่หายไป (การส่งคืนการกำหนดค่าจะมีประโยชน์เพราะจะหลีกเลี่ยงการโทรหาผู้เขียนบน dict เพิ่มเติมและน่าเสียดายที่เราไม่สามารถส่งคืนได้ในขณะที่ตั้งค่าไว้)
หมายเหตุเหล่านี้เป็นความหมายเดียวกันกับคำตอบ upvoted ที่สุด แต่ในครึ่งบรรทัดของรหัส - การใช้งานของ nosklo:
class AutoVivification(dict):
"""Implementation of perl's autovivification feature."""
def __getitem__(self, item):
try:
return dict.__getitem__(self, item)
except KeyError:
value = self[item] = type(self)()
return value
การสาธิตการใช้งาน
ด้านล่างนี้เป็นเพียงตัวอย่างของวิธีที่ dict นี้สามารถใช้เพื่อสร้างโครงสร้าง dict ที่ซ้อนกันได้อย่างรวดเร็ว สิ่งนี้สามารถสร้างโครงสร้างต้นไม้แบบลำดับชั้นได้อย่างรวดเร็วเท่าที่คุณต้องการ
import pprint
class Vividict(dict):
def __missing__(self, key):
value = self[key] = type(self)()
return value
d = Vividict()
d['foo']['bar']
d['foo']['baz']
d['fizz']['buzz']
d['primary']['secondary']['tertiary']['quaternary']
pprint.pprint(d)
ผลลัพธ์ใด:
{'fizz': {'buzz': {}},
'foo': {'bar': {}, 'baz': {}},
'primary': {'secondary': {'tertiary': {'quaternary': {}}}}}
และตามบรรทัดสุดท้ายแสดงให้เห็นว่ามันพิมพ์ออกมาสวยและเพื่อการตรวจสอบด้วยตนเอง แต่ถ้าคุณต้องการตรวจสอบข้อมูลของคุณด้วยตาเปล่าการติด__missing__
ตั้งอินสแตนซ์ใหม่ของคลาสเป็นคีย์และส่งคืนเป็นโซลูชันที่ดีกว่ามาก
ทางเลือกอื่นสำหรับความคมชัด:
dict.setdefault
แม้ว่าผู้ถามจะคิดว่านี่ไม่สะอาด แต่ฉันคิดว่าVividict
ตัวเองดีกว่า
d = {} # or dict()
for (state, county, occupation), number in data.items():
d.setdefault(state, {}).setdefault(county, {})[occupation] = number
และตอนนี้:
>>> pprint.pprint(d, width=40)
{'new jersey': {'mercer county': {'plumbers': 3,
'programmers': 81},
'middlesex county': {'programmers': 81,
'salesmen': 62}},
'new york': {'queens county': {'plumbers': 9,
'salesmen': 36}}}
การสะกดผิดจะล้มเหลวอย่างเสียงดังและไม่ถ่วงข้อมูลของเราด้วยข้อมูลที่ไม่ดี:
>>> d['new york']['queens counyt']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'queens counyt'
นอกจากนี้ฉันคิดว่า setdefault ใช้งานได้ดีเมื่อใช้ในลูปและคุณไม่รู้ว่าคุณจะได้รับกุญแจอะไร
d = dict()
d.setdefault('foo', {}).setdefault('bar', {})
d.setdefault('foo', {}).setdefault('baz', {})
d.setdefault('fizz', {}).setdefault('buzz', {})
d.setdefault('primary', {}).setdefault('secondary', {}).setdefault('tertiary', {}).setdefault('quaternary', {})
ข้อวิจารณ์อีกข้อหนึ่งคือ setdefault ต้องการอินสแตนซ์ใหม่ไม่ว่าจะใช้หรือไม่ก็ตาม อย่างไรก็ตาม Python (หรืออย่างน้อย CPython) ค่อนข้างฉลาดเกี่ยวกับการจัดการอินสแตนซ์ใหม่ที่ไม่ได้ใช้และไม่อ้างอิงเช่นมันจะนำตำแหน่งในหน่วยความจำกลับมาใช้ใหม่:
>>> id({}), id({}), id({})
(523575344, 523575344, 523575344)
defaultdict ที่กำหนดให้เป็นอัตโนมัติ
นี่เป็นการใช้งานที่ดูเรียบร้อยและการใช้งานในสคริปต์ที่คุณไม่ได้ตรวจสอบข้อมูลจะมีประโยชน์เท่ากับการใช้งาน__missing__
:
from collections import defaultdict
def vivdict():
return defaultdict(vivdict)
แต่ถ้าคุณต้องการตรวจสอบข้อมูลของคุณผลลัพธ์ของ defaultdivified อัตโนมัติที่บรรจุด้วยข้อมูลในลักษณะเดียวกันจะมีลักษณะดังนี้:
>>> d = vivdict(); d['foo']['bar']; d['foo']['baz']; d['fizz']['buzz']; d['primary']['secondary']['tertiary']['quaternary']; import pprint;
>>> pprint.pprint(d)
defaultdict(<function vivdict at 0x17B01870>, {'foo': defaultdict(<function vivdict
at 0x17B01870>, {'baz': defaultdict(<function vivdict at 0x17B01870>, {}), 'bar':
defaultdict(<function vivdict at 0x17B01870>, {})}), 'primary': defaultdict(<function
vivdict at 0x17B01870>, {'secondary': defaultdict(<function vivdict at 0x17B01870>,
{'tertiary': defaultdict(<function vivdict at 0x17B01870>, {'quaternary': defaultdict(
<function vivdict at 0x17B01870>, {})})})}), 'fizz': defaultdict(<function vivdict at
0x17B01870>, {'buzz': defaultdict(<function vivdict at 0x17B01870>, {})})})
ผลลัพธ์นี้ค่อนข้างล่าช้าและผลลัพธ์ไม่สามารถอ่านได้ โซลูชันที่ได้รับโดยทั่วไปคือแปลงกลับเป็น dict ซ้ำเพื่อตรวจสอบด้วยตนเอง วิธีการแก้ปัญหาที่ไม่สำคัญนี้เหลือไว้สำหรับการออกกำลังกายสำหรับผู้อ่าน
ประสิทธิภาพ
สุดท้ายมาดูประสิทธิภาพกัน ฉันลบค่าใช้จ่ายในการเริ่มต้น
>>> import timeit
>>> min(timeit.repeat(lambda: {}.setdefault('foo', {}))) - min(timeit.repeat(lambda: {}))
0.13612580299377441
>>> min(timeit.repeat(lambda: vivdict()['foo'])) - min(timeit.repeat(lambda: vivdict()))
0.2936999797821045
>>> min(timeit.repeat(lambda: Vividict()['foo'])) - min(timeit.repeat(lambda: Vividict()))
0.5354437828063965
>>> min(timeit.repeat(lambda: AutoVivification()['foo'])) - min(timeit.repeat(lambda: AutoVivification()))
2.138362169265747
ขึ้นอยู่กับประสิทธิภาพการdict.setdefault
ทำงานที่ดีที่สุด ฉันขอแนะนำอย่างยิ่งให้ใช้รหัสการผลิตในกรณีที่คุณสนใจความเร็วการดำเนินการ
หากคุณต้องการสิ่งนี้สำหรับการใช้งานแบบอินเทอร์แอคทีฟ (ในโน้ตบุ๊ก IPython อาจจะ) ประสิทธิภาพนั้นไม่สำคัญ - ในกรณีนี้ฉันจะใช้ Vividict เพื่ออ่านเอาต์พุต เปรียบเทียบกับวัตถุ AutoVivification (ซึ่งใช้__getitem__
แทน__missing__
ซึ่งทำขึ้นเพื่อจุดประสงค์นี้) มันเหนือกว่ามาก
ข้อสรุป
การนำไปใช้__missing__
กับคลาสย่อยdict
เพื่อตั้งค่าและส่งคืนอินสแตนซ์ใหม่นั้นยากกว่าตัวเลือกอื่นเล็กน้อย แต่มีประโยชน์
- instantiation ง่าย
- ประชากรข้อมูลง่าย
- ดูข้อมูลได้ง่าย
และเนื่องจากมีความซับซ้อนน้อยกว่าและมีประสิทธิภาพมากกว่าการปรับเปลี่ยน __getitem__
จึงควรเลือกใช้วิธีดังกล่าว
อย่างไรก็ตามมันมีข้อเสีย:
- การค้นหาที่ไม่ดีจะล้มเหลวอย่างเงียบ ๆ
- การค้นหาที่ไม่ดีจะยังคงอยู่ในพจนานุกรม
ดังนั้นฉันเองชอบsetdefault
โซลูชันอื่น ๆ และมีในทุกสถานการณ์ที่ฉันต้องการพฤติกรรมแบบนี้
Vividict
หรือไม่? เช่น3
และlist
สำหรับ Dict ของ Dict ของ Dictd['primary']['secondary']['tertiary'].append(element)
ของรายการซึ่งอาจจะมีประชากรที่มี ฉันสามารถนิยามคลาสที่แตกต่างกัน 3 คลาสสำหรับแต่ละความลึก แต่ฉันชอบที่จะหาทางแก้ไขที่สะอาดกว่า