Який найкращий спосіб впровадити вкладені словники в Python?
Це погана ідея, не робіть цього. Натомість використовуйте звичайний словник і використовуйте dict.setdefault
там, де підходить, тож коли ключі відсутні при звичайному використанні, ви отримуєте очікуване KeyError
. Якщо ви наполягаєте на такій поведінці, ось як стріляти в ногу:
Реалізувати __missing__
на dict
підклас для набору і повертає новий екземпляр.
Цей підхід був доступний (і задокументований) ще з Python 2.5, і (особливо це цінно для мене), він досить друкує так само, як і звичайний дикт , замість негарного друку автоматизованого рішення за замовчуванням:
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
коли ключ доступний, але відсутній. (Повернення присвоєння значення корисно, оскільки дозволяє уникнути додаткового виклику геттера на дікт, і, на жаль, ми не можемо повернути його під час встановлення.)
Зауважте, це та сама семантика, що і найбільш відповідна відповідь, але в половині рядків коду - реалізація 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
Демонстрація використання
Нижче наведено лише приклад того, як цей дикт можна було б легко використати для створення вкладеної структури диктату на льоту. Це може швидко створити ієрархічну структуру дерева так глибоко, наскільки ви хочете піти.
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)
Автовізований вирок за замовчуванням
Це акуратно виглядає реалізація, а використання в сценарії, про який ви не перевіряєте дані, було б настільки ж корисним, як реалізація __missing__
:
from collections import defaultdict
def vivdict():
return defaultdict(vivdict)
Але якщо вам потрібно перевірити ваші дані, результати автоматичного оживлення за замовчуванням, заповнені даними, таким же чином виглядають так:
>>> 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>, {})})})
Цей результат є неелегантним, а результати - зовсім нечитабельними. Зазвичай пропонується рішення рекурсивно перетворювати назад у дікт для ручного огляду. Це нетривіальне рішення залишається вправою для читача.
Продуктивність
Наостанок давайте подивимось на продуктивність. Я віднімаю витрати на інстанції.
>>> 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
для встановлення та повернення нового екземпляра трохи складніше, ніж альтернативи, але має переваги
- легка інстанція
- легка сукупність даних
- простий перегляд даних
і оскільки він менш складний і більш ефективний, ніж модифікаційний __getitem__
, слід віддавати перевагу цьому методу.
Тим не менш, у нього є недоліки:
- Погані пошуки пройдуть беззвучно.
- Поганий пошук залишиться у словнику.
Таким чином, я особисто віддаю перевагу setdefault
іншим рішенням, і маю в кожній ситуації, коли мені потрібна така поведінка.
Vividict
? Напр.,3
Іlist
для складання дикту дикту списків, якими можна було б заповнитиd['primary']['secondary']['tertiary'].append(element)
. Я міг би визначити 3 різні класи на кожну глибину, але я хотів би знайти більш чисте рішення.