Який найкращий спосіб реалізувати вкладені словники?


201

У мене є структура даних, яка по суті становить вкладений словник. Скажімо, це виглядає приблизно так:

{'new jersey': {'mercer county': {'plumbers': 3,
                                  'programmers': 81},
                'middlesex county': {'programmers': 81,
                                     'salesmen': 62}},
 'new york': {'queens county': {'plumbers': 9,
                                'salesmen': 36}}}

Зараз підтримувати та створювати це досить болісно; кожного разу, коли у мене є нова держава / графство / професія, мені доводиться створювати словники нижнього рівня за допомогою неприємних блоків спробу / лову. Більше того, мені потрібно створити дратівливі вкладені ітератори, якщо я хочу перейти всі значення.

Я можу також використовувати кортежі як ключі, як-от такі:

{('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}

Це робить ітерацію над значеннями дуже простою і природною, але синтаксично болісніше робити такі речі, як агрегація та перегляд підмножини словника (наприклад, якщо я просто хочу переходити за станом за станом).

В основному, іноді я хочу вважати вкладений словник плоским словником, а іноді хочу вважати це справді сложною ієрархією. Я міг би все це обробити в класі, але, схоже, хтось це вже зробив. Крім того, може здатися, що для цього можуть бути справді елегантні синтаксичні конструкції.

Як я міг зробити це краще?

Додаток: Я знаю, setdefault()але це не дуже відповідає чистому синтаксису. Крім того, для кожного створеного під словника все ще потрібно встановити setdefault()вручну.

Відповіді:


178

Який найкращий спосіб впровадити вкладені словники в 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 різні класи на кожну глибину, але я хотів би знайти більш чисте рішення.
Ерік Думініл

@EricDuminil d['primary']['secondary'].setdefault('tertiary', []).append('element')- ?? Дякую за комплімент, але дозвольте бути чесним - я ніколи насправді не використовую __missing__- я завжди користуюся setdefault. Я, мабуть, повинен оновити свій висновок / вступ ...
Аарон Холл

@AaronHall Правильна поведінка полягає в тому, що код повинен створити дік, якщо це необхідно. У цьому випадку шляхом переосмислення попереднього призначеного значення.
nehem

@AaronHall Ви також можете допомогти мені зрозуміти, що мається на увазі під The bad lookup will remain in the dictionary.цим рішенням ?. Цінується. Thx
nehem

@AaronHall Проблема з нею вийде з ладу, setdefaultколи вона вклала більше двох рівнів глибини. Схоже, що жодна структура в Python не може запропонувати справжню активізацію, як описано. Мені довелося погодитися на два способи констатації, один для get_nested& один, для set_nestedякого прийняти посилання на dict та список вкладених атрибутів.
nehem

188
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

Тестування:

a = AutoVivification()

a[1][2][3] = 4
a[1][3][3] = 5
a[1][2]['test'] = 6

print a

Вихід:

{1: {2: {'test': 6, 3: 4}, 3: {3: 5}}}

У когось є ця проблема, коли вони перейшли на python 3.x? stackoverflow.com/questions/54622935 / ...
Ясон

@jason pickleжахливий між версіями python. Не використовуйте його для зберігання даних, які ви хочете зберегти. Використовуйте його лише для кешів та речей, які ви можете скинути та відновити за бажанням. Не як метод тривалого зберігання чи серіалізації.
nosklo

Що ви використовуєте для зберігання цих об’єктів? Мій об’єкт автовівізації містить просто рамки даних панди та рядок.
Ясон

@jason Залежно від даних, я люблю використовувати JSON, CSV файли або навіть sqliteбазу даних для їх зберігання.
nosklo

30

Тільки тому, що я не бачив жодної такої маленької, ось вислів, який стає таким вкладеним, як вам подобається, без поту:

# yo dawg, i heard you liked dicts                                                                      
def yodict():
    return defaultdict(yodict)

2
@wberry: насправді все, що вам потрібно, це yodict = lambda: defaultdict(yodict).
martineau

1
Прийнята версія є підкласом dict, тому для того, щоб бути повністю рівнозначним, нам потрібно було б x = Vdict(a=1, b=2)працювати.
wberry

@wberry: Незалежно від того, що є у прийнятій відповіді, бути підкласом dictне було вимогою, заявленою ОП, яка лише попросила "найкращий спосіб" їх реалізації - і до того ж, це не / не повинно Мало значення в Python в будь-якому випадку.
мартіно

24

Ви можете створити файл YAML і прочитати його за допомогою PyYaml .

Крок 1: Створіть файл YAML, "jobs.yml":

new jersey:
  mercer county:
    pumbers: 3
    programmers: 81
  middlesex county:
    salesmen: 62
    programmers: 81
new york:
  queens county:
    plumbers: 9
    salesmen: 36

Крок 2: Прочитайте його в Python

import yaml
file_handle = open("employment.yml")
my_shnazzy_dictionary = yaml.safe_load(file_handle)
file_handle.close()

і тепер my_shnazzy_dictionaryмає всі ваші цінності. Якщо вам потрібно було це зробити на ходу, ви можете створити YAML як рядок і ввести його в yaml.safe_load(...).


4
YAML - це, безумовно, мій вибір для введення безлічі глибоко вкладених даних (а також файлів конфігурації, макетів баз даних тощо). Якщо ОП не хоче, щоб додаткові файли лежали навколо, просто використовуйте звичайний рядок Python в якомусь файлі та розбирайте його з YAML.
kmelvn

Хороший момент у створенні рядків YAML: Це був би набагато більш чистий підхід, ніж використання модуля "tempfile" багаторазово.
Піт

18

Оскільки у вас є схема зіркової схеми, ви, можливо, захочете її структурувати більше як реляційну таблицю і менш схожу на словник.

import collections

class Jobs( object ):
    def __init__( self, state, county, title, count ):
        self.state= state
        self.count= county
        self.title= title
        self.count= count

facts = [
    Jobs( 'new jersey', 'mercer county', 'plumbers', 3 ),
    ...

def groupBy( facts, name ):
    total= collections.defaultdict( int )
    for f in facts:
        key= getattr( f, name )
        total[key] += f.count

Така річ може пройти довгий шлях до створення дизайну схожого на сховище даних без накладних витрат на SQL.


14

Якщо кількість гніздових рівнів невелика, я використовую collections.defaultdictдля цього:

from collections import defaultdict

def nested_dict_factory(): 
  return defaultdict(int)
def nested_dict_factory2(): 
  return defaultdict(nested_dict_factory)
db = defaultdict(nested_dict_factory2)

db['new jersey']['mercer county']['plumbers'] = 3
db['new jersey']['mercer county']['programmers'] = 81

Використовуючи , defaultdictяк це дозволяє уникнути багато брудних setdefault(), get()і т.д.


+1: вирок за замовчуванням - одне з найулюбленіших моїх доповнень до пітона. Більше .setdefault ()!
Джон Фухі

8

Це функція, яка повертає вкладений словник довільної глибини:

from collections import defaultdict
def make_dict():
    return defaultdict(make_dict)

Використовуйте його так:

d=defaultdict(make_dict)
d["food"]["meat"]="beef"
d["food"]["veggie"]="corn"
d["food"]["sweets"]="ice cream"
d["animal"]["pet"]["dog"]="collie"
d["animal"]["pet"]["cat"]="tabby"
d["animal"]["farm animal"]="chicken"

Повторіть все через щось подібне:

def iter_all(d,depth=1):
    for k,v in d.iteritems():
        print "-"*depth,k
        if type(v) is defaultdict:
            iter_all(v,depth+1)
        else:
            print "-"*(depth+1),v

iter_all(d)

Це видає:

- food
-- sweets
--- ice cream
-- meat
--- beef
-- veggie
--- corn
- animal
-- pet
--- dog
---- labrador
--- cat
---- tabby
-- farm animal
--- chicken

Ви, можливо, захочете зробити так, щоб нові елементи не могли бути додані до диктату. Легко рекурсивно перетворити всі ці defaultdicts у нормальний станdict s .

def dictify(d):
    for k,v in d.iteritems():
        if isinstance(v,defaultdict):
            d[k] = dictify(v)
    return dict(d)

7

Я вважаю setdefaultдосить корисним; Він перевіряє наявність ключа та додає його, якщо ні:

d = {}
d.setdefault('new jersey', {}).setdefault('mercer county', {})['plumbers'] = 3

setdefault завжди повертає відповідний ключ, тому ви фактично оновлюєте значення 'd " на місці.

Що стосується ітерації, я впевнений, що ви можете написати генератор досить легко, якщо такого ще немає в Python:

def iterateStates(d):
    # Let's count up the total number of "plumbers" / "dentists" / etc.
    # across all counties and states
    job_totals = {}

    # I guess this is the annoying nested stuff you were talking about?
    for (state, counties) in d.iteritems():
        for (county, jobs) in counties.iteritems():
            for (job, num) in jobs.iteritems():
                # If job isn't already in job_totals, default it to zero
                job_totals[job] = job_totals.get(job, 0) + num

    # Now return an iterator of (job, number) tuples
    return job_totals.iteritems()

# Display all jobs
for (job, num) in iterateStates(d):
    print "There are %d %s in total" % (job, num)

Мені подобається це рішення, але коли я намагаюся: count.setdefault (a, {}). Setdefault (b, {}). Setdefault (c, 0) + = 1 я отримую "незаконний вираз для розширеного призначення"
dfrankow

6

Як підказали інші, реляційна база даних може бути вам кориснішою. Ви можете використовувати базу даних sqlite3 в пам'яті як структуру даних для створення таблиць та їх запиту.

import sqlite3

c = sqlite3.Connection(':memory:')
c.execute('CREATE TABLE jobs (state, county, title, count)')

c.executemany('insert into jobs values (?, ?, ?, ?)', [
    ('New Jersey', 'Mercer County',    'Programmers', 81),
    ('New Jersey', 'Mercer County',    'Plumbers',     3),
    ('New Jersey', 'Middlesex County', 'Programmers', 81),
    ('New Jersey', 'Middlesex County', 'Salesmen',    62),
    ('New York',   'Queens County',    'Salesmen',    36),
    ('New York',   'Queens County',    'Plumbers',     9),
])

# some example queries
print list(c.execute('SELECT * FROM jobs WHERE county = "Queens County"'))
print list(c.execute('SELECT SUM(count) FROM jobs WHERE title = "Programmers"'))

Це лише простий приклад. Ви можете визначити окремі таблиці для штатів, графств та посади.


5

collections.defaultdictможна підкласифікувати, щоб скласти вкладений дікт. Потім додайте будь-які корисні методи ітерації до цього класу.

>>> from collections import defaultdict
>>> class nesteddict(defaultdict):
    def __init__(self):
        defaultdict.__init__(self, nesteddict)
    def walk(self):
        for key, value in self.iteritems():
            if isinstance(value, nesteddict):
                for tup in value.walk():
                    yield (key,) + tup
            else:
                yield key, value


>>> nd = nesteddict()
>>> nd['new jersey']['mercer county']['plumbers'] = 3
>>> nd['new jersey']['mercer county']['programmers'] = 81
>>> nd['new jersey']['middlesex county']['programmers'] = 81
>>> nd['new jersey']['middlesex county']['salesmen'] = 62
>>> nd['new york']['queens county']['plumbers'] = 9
>>> nd['new york']['queens county']['salesmen'] = 36
>>> for tup in nd.walk():
    print tup


('new jersey', 'mercer county', 'programmers', 81)
('new jersey', 'mercer county', 'plumbers', 3)
('new jersey', 'middlesex county', 'programmers', 81)
('new jersey', 'middlesex county', 'salesmen', 62)
('new york', 'queens county', 'salesmen', 36)
('new york', 'queens county', 'plumbers', 9)

1
Це відповідь, яка найближче до того, що я шукав. Але в ідеалі були б всілякі допоміжні функції, наприклад, walk_keys () або подібні. Я здивований, що в стандартних бібліотеках немає нічого для цього.
YGA

4

Що стосується "неприємних блоків спробувати / ловити":

d = {}
d.setdefault('key',{}).setdefault('inner key',{})['inner inner key'] = 'value'
print d

врожайність

{'key': {'inner key': {'inner inner key': 'value'}}}

Ви можете використовувати це для перетворення з плоского формату словника в структурований формат:

fd = {('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}

for (k1,k2,k3), v in fd.iteritems():
    d.setdefault(k1, {}).setdefault(k2, {})[k3] = v


4

defaultdict() твій друг!

Для двовимірного словника ви можете:

d = defaultdict(defaultdict)
d[1][2] = 3

Для отримання додаткових розмірів ви можете:

d = defaultdict(lambda :defaultdict(defaultdict))
d[1][2][3] = 4

Ця відповідь працює в кращому випадку лише на трьох рівнях. Для довільних рівнів врахуйте цю відповідь .
Acumenus

3

Для легкої ітерації над вкладеним словником, чому б просто не написати простий генератор?

def each_job(my_dict):
    for state, a in my_dict.items():
        for county, b in a.items():
            for job, value in b.items():
                yield {
                    'state'  : state,
                    'county' : county,
                    'job'    : job,
                    'value'  : value
                }

Отже, якщо у вас є складений вкладений словник, ітерація над ним стає простою:

for r in each_job(my_dict):
    print "There are %d %s in %s, %s" % (r['value'], r['job'], r['county'], r['state'])

Очевидно, ваш генератор може отримати будь-який формат даних, який вам корисний.

Чому для читання дерева ви використовуєте блоки спробу лову? Досить просто (і, мабуть, безпечніше) запитати, чи існує ключ у дикті, перш ніж спробувати його отримати. Функція, що використовує захисні пропозиції, може виглядати приблизно так:

if not my_dict.has_key('new jersey'):
    return False

nj_dict = my_dict['new jersey']
...

Або, можливо, дещо дослідним методом є використання методу get:

value = my_dict.get('new jersey', {}).get('middlesex county', {}).get('salesmen', 0)

Але для дещо кращого способу ви можете поглянути на використання collection.defaultdict , який є частиною стандартної бібліотеки, починаючи з python 2.5.

import collections

def state_struct(): return collections.defaultdict(county_struct)
def county_struct(): return collections.defaultdict(job_struct)
def job_struct(): return 0

my_dict = collections.defaultdict(state_struct)

print my_dict['new jersey']['middlesex county']['salesmen']

Я роблю припущення щодо сенсу вашої структури даних тут, але слід легко налаштувати те, що ви насправді хочете зробити.


2

Мені подобається ідея загортати це в клас та реалізовувати, __getitem__і __setitem__вони реалізували просту мову запитів:

>>> d['new jersey/mercer county/plumbers'] = 3
>>> d['new jersey/mercer county/programmers'] = 81
>>> d['new jersey/mercer county/programmers']
81
>>> d['new jersey/mercer country']
<view which implicitly adds 'new jersey/mercer county' to queries/mutations>

Якщо ви хочете пофантазувати, ви також можете реалізувати щось на кшталт:

>>> d['*/*/programmers']
<view which would contain 'programmers' entries>

але в основному я думаю, що таке можна було б реально розважити: D


Я думаю, що це погана ідея - ніколи не можна передбачити синтаксис ключів. Ви все одно перекриєте getitem і setitem, але змусите їх брати кортежі.
YGA

3
@YGA Ви, мабуть, праві, але цікаво роздумувати над такою міні-мовою.
Аарон Маенпаа

1

Якщо ваш набір даних не залишиться досить малим, ви можете розглянути можливість використання реляційної бази даних. Це зробить саме те, що ви хочете: полегшить додавання підрахунків, вибравши підмножини підрахунків і навіть зведені підрахунки за штатом, округом, окупацією чи будь-якою з них.


1
class JobDb(object):
    def __init__(self):
        self.data = []
        self.all = set()
        self.free = []
        self.index1 = {}
        self.index2 = {}
        self.index3 = {}

    def _indices(self,(key1,key2,key3)):
        indices = self.all.copy()
        wild = False
        for index,key in ((self.index1,key1),(self.index2,key2),
                                             (self.index3,key3)):
            if key is not None:
                indices &= index.setdefault(key,set())
            else:
                wild = True
        return indices, wild

    def __getitem__(self,key):
        indices, wild = self._indices(key)
        if wild:
            return dict(self.data[i] for i in indices)
        else:
            values = [self.data[i][-1] for i in indices]
            if values:
                return values[0]

    def __setitem__(self,key,value):
        indices, wild = self._indices(key)
        if indices:
            for i in indices:
                self.data[i] = key,value
        elif wild:
            raise KeyError(k)
        else:
            if self.free:
                index = self.free.pop(0)
                self.data[index] = key,value
            else:
                index = len(self.data)
                self.data.append((key,value))
                self.all.add(index)
            self.index1.setdefault(key[0],set()).add(index)
            self.index2.setdefault(key[1],set()).add(index)
            self.index3.setdefault(key[2],set()).add(index)

    def __delitem__(self,key):
        indices,wild = self._indices(key)
        if not indices:
            raise KeyError
        self.index1[key[0]] -= indices
        self.index2[key[1]] -= indices
        self.index3[key[2]] -= indices
        self.all -= indices
        for i in indices:
            self.data[i] = None
        self.free.extend(indices)

    def __len__(self):
        return len(self.all)

    def __iter__(self):
        for key,value in self.data:
            yield key

Приклад:

>>> db = JobDb()
>>> db['new jersey', 'mercer county', 'plumbers'] = 3
>>> db['new jersey', 'mercer county', 'programmers'] = 81
>>> db['new jersey', 'middlesex county', 'programmers'] = 81
>>> db['new jersey', 'middlesex county', 'salesmen'] = 62
>>> db['new york', 'queens county', 'plumbers'] = 9
>>> db['new york', 'queens county', 'salesmen'] = 36

>>> db['new york', None, None]
{('new york', 'queens county', 'plumbers'): 9,
 ('new york', 'queens county', 'salesmen'): 36}

>>> db[None, None, 'plumbers']
{('new jersey', 'mercer county', 'plumbers'): 3,
 ('new york', 'queens county', 'plumbers'): 9}

>>> db['new jersey', 'mercer county', None]
{('new jersey', 'mercer county', 'plumbers'): 3,
 ('new jersey', 'mercer county', 'programmers'): 81}

>>> db['new jersey', 'middlesex county', 'programmers']
81

>>>

Редагувати: Тепер повертаються словники під час запитів із підказками ( None) та одиничними значеннями в іншому випадку.


Чому повертаються списки? Здається, він повинен повернути словник (щоб ви знали, що означає кожне число), або суму (оскільки це все, що ви справді можете зробити зі списком).
Бен Бланк

0

У мене схожа річ. У мене дуже багато випадків, коли я роблю:

thedict = {}
for item in ('foo', 'bar', 'baz'):
  mydict = thedict.get(item, {})
  mydict = get_value_for(item)
  thedict[item] = mydict

Але заглиблюючись на багато рівнів. Це ".get (item, {})", це ключ, оскільки він зробить інший словник, якщо його ще немає. Тим часом я думав, як краще впоратися з цим. Зараз їх дуже багато

value = mydict.get('foo', {}).get('bar', {}).get('baz', 0)

Тому замість цього я зробив:

def dictgetter(thedict, default, *args):
  totalargs = len(args)
  for i,arg in enumerate(args):
    if i+1 == totalargs:
      thedict = thedict.get(arg, default)
    else:
      thedict = thedict.get(arg, {})
  return thedict

Що має такий же ефект, якщо ви робите:

value = dictgetter(mydict, 0, 'foo', 'bar', 'baz')

Краще? Я думаю так.


0

Ви можете використовувати рекурсію в лямбдах і за замовчуванням, не потрібно визначати імена:

a = defaultdict((lambda f: f(f))(lambda g: lambda:defaultdict(g(g))))

Ось приклад:

>>> a['new jersey']['mercer county']['plumbers']=3
>>> a['new jersey']['middlesex county']['programmers']=81
>>> a['new jersey']['mercer county']['programmers']=81
>>> a['new jersey']['middlesex county']['salesmen']=62
>>> a
defaultdict(<function __main__.<lambda>>,
        {'new jersey': defaultdict(<function __main__.<lambda>>,
                     {'mercer county': defaultdict(<function __main__.<lambda>>,
                                  {'plumbers': 3, 'programmers': 81}),
                      'middlesex county': defaultdict(<function __main__.<lambda>>,
                                  {'programmers': 81, 'salesmen': 62})})})

0

Раніше я використовував цю функцію. його безпечний, швидкий, легко піддається технічному обслуговуванню.

def deep_get(dictionary, keys, default=None):
    return reduce(lambda d, key: d.get(key, default) if isinstance(d, dict) else default, keys.split("."), dictionary)

Приклад:

>>> from functools import reduce
>>> def deep_get(dictionary, keys, default=None):
...     return reduce(lambda d, key: d.get(key, default) if isinstance(d, dict) else default, keys.split("."), dictionary)
...
>>> person = {'person':{'name':{'first':'John'}}}
>>> print (deep_get(person, "person.name.first"))
John
>>> print (deep_get(person, "person.name.lastname"))
None
>>> print (deep_get(person, "person.name.lastname", default="No lastname"))
No lastname
>>>
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.