Cách tốt nhất để thực hiện từ điển lồng nhau trong Python là gì?
Đây là một ý tưởng tồi, đừng làm điều đó. Thay vào đó, hãy sử dụng một từ điển thông thường và sử dụng dict.setdefault
nơi apropos, vì vậy khi thiếu các khóa trong sử dụng bình thường, bạn sẽ nhận được mong đợi KeyError
. Nếu bạn khăng khăng nhận hành vi này, đây là cách tự bắn vào chân mình:
Thực hiện __missing__
trên một dict
lớp con để thiết lập và trả về một thể hiện mới.
Cách tiếp cận này đã có sẵn (và được ghi lại) kể từ Python 2.5 và (đặc biệt có giá trị đối với tôi), nó in đẹp như một bản chính tả , thay vì in ấn xấu xí của một bản mặc định tự động:
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
(Lưu ý self[key]
ở phía bên trái của bài tập, vì vậy không có đệ quy ở đây.)
và nói rằng bạn có một số dữ liệu:
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}
Đây là mã sử dụng của chúng tôi:
vividict = Vividict()
for (state, county, occupation), number in data.items():
vividict[state][county][occupation] = number
Và bây giờ:
>>> 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}}}
Sự chỉ trích
Một lời chỉ trích về loại container này là nếu người dùng viết sai một khóa, mã của chúng tôi có thể thất bại trong âm thầm:
>>> vividict['new york']['queens counyt']
{}
Và ngoài ra, bây giờ chúng tôi có một quận sai chính tả trong dữ liệu của chúng tôi:
>>> 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': {}}}
Giải trình:
Chúng tôi chỉ cung cấp một thể hiện lồng nhau khác của lớp chúng tôi Vividict
bất cứ khi nào khóa được truy cập nhưng bị thiếu. (Trả lại việc gán giá trị là hữu ích vì nó tránh cho chúng tôi gọi thêm getter trên dict, và thật không may, chúng tôi không thể trả lại nó khi nó đang được đặt.)
Lưu ý, đây là những ngữ nghĩa giống như câu trả lời được đánh giá cao nhất nhưng trong một nửa dòng mã - triển khai của 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
Trình diễn sử dụng
Dưới đây chỉ là một ví dụ về cách có thể dễ dàng sử dụng dict này để tạo ra một cấu trúc chính tả lồng nhau khi đang bay. Điều này có thể nhanh chóng tạo ra một cấu trúc cây phân cấp sâu như bạn muốn đi.
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)
Đầu ra nào:
{'fizz': {'buzz': {}},
'foo': {'bar': {}, 'baz': {}},
'primary': {'secondary': {'tertiary': {'quaternary': {}}}}}
Và như dòng cuối cùng cho thấy, nó in khá đẹp và để kiểm tra thủ công. Nhưng nếu bạn muốn kiểm tra trực quan dữ liệu của mình, triển khai __missing__
để đặt một thể hiện mới của lớp của nó thành khóa và trả lại thì đó là một giải pháp tốt hơn nhiều.
Các lựa chọn thay thế khác, tương phản:
dict.setdefault
Mặc dù người hỏi nghĩ rằng điều này không sạch, nhưng tôi thấy nó tốt hơn cho Vividict
bản thân mình.
d = {} # or dict()
for (state, county, occupation), number in data.items():
d.setdefault(state, {}).setdefault(county, {})[occupation] = number
và bây giờ:
>>> 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}}}
Một lỗi chính tả sẽ thất bại một cách ồn ào và không làm lộn xộn dữ liệu của chúng tôi với thông tin xấu:
>>> d['new york']['queens counyt']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'queens counyt'
Ngoài ra, tôi nghĩ setdefault hoạt động rất tốt khi được sử dụng trong các vòng lặp và bạn không biết bạn sẽ lấy gì cho khóa, nhưng việc sử dụng lặp đi lặp lại trở nên khá nặng nề và tôi không nghĩ ai sẽ muốn theo kịp:
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', {})
Một chỉ trích khác là setdefault yêu cầu một thể hiện mới cho dù nó được sử dụng hay không. Tuy nhiên, Python (hoặc ít nhất là CPython) khá thông minh trong việc xử lý các trường hợp mới không được sử dụng và không được kiểm tra, ví dụ, nó sử dụng lại vị trí trong bộ nhớ:
>>> id({}), id({}), id({})
(523575344, 523575344, 523575344)
Một defaultdict tự động sinh động
Đây là một triển khai tìm kiếm gọn gàng và việc sử dụng trong một tập lệnh mà bạn không kiểm tra dữ liệu trên sẽ hữu ích như triển khai __missing__
:
from collections import defaultdict
def vivdict():
return defaultdict(vivdict)
Nhưng nếu bạn cần kiểm tra dữ liệu của mình, kết quả của một defaultdict được tự động hóa được điền với dữ liệu theo cách tương tự như sau:
>>> 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>, {})})})
Đầu ra này là không phù hợp, và kết quả là không thể đọc được. Giải pháp thường được đưa ra là chuyển đổi đệ quy trở lại thành một lệnh để kiểm tra thủ công. Giải pháp không tầm thường này được để lại như một bài tập cho người đọc.
Hiệu suất
Cuối cùng, hãy nhìn vào hiệu suất. Tôi đang trừ chi phí khởi tạo.
>>> 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
Dựa trên hiệu suất, dict.setdefault
hoạt động tốt nhất. Tôi rất muốn giới thiệu nó cho mã sản xuất, trong trường hợp bạn quan tâm đến tốc độ thực hiện.
Nếu bạn cần điều này để sử dụng tương tác (có thể là trong một máy tính xách tay IPython) thì hiệu suất không thực sự quan trọng - trong trường hợp đó, tôi sẽ đi với Vividict để dễ đọc đầu ra. So với đối tượng AutoVivification (sử dụng __getitem__
thay vì __missing__
, được tạo ra cho mục đích này), nó vượt trội hơn nhiều.
Phần kết luận
Việc triển khai __missing__
trên một lớp con dict
để thiết lập và trả về một thể hiện mới khó hơn một chút so với các lựa chọn thay thế nhưng có lợi ích của
- khởi tạo dễ dàng
- dân số dữ liệu dễ dàng
- xem dữ liệu dễ dàng
và bởi vì nó ít phức tạp hơn và hiệu quả hơn so với sửa đổi __getitem__
, nên nó được ưa thích hơn phương pháp đó.
Tuy nhiên, nó có nhược điểm:
- Tra cứu xấu sẽ thất bại âm thầm.
- Tra cứu xấu sẽ vẫn còn trong từ điển.
Vì vậy, cá nhân tôi thích setdefault
các giải pháp khác, và trong mọi tình huống mà tôi cần loại hành vi này.
Vividict
không? Ví dụ3
vàlist
cho một dict of dict of dict của danh sách có thể được điền vớid['primary']['secondary']['tertiary'].append(element)
. Tôi có thể định nghĩa 3 lớp khác nhau cho mỗi độ sâu nhưng tôi muốn tìm một giải pháp sạch hơn.