Эффективный доступ к произвольно глубоким словарям

Предположим, у меня есть многоуровневый словарь, подобный этому

mydict = {
    'first': {
        'second': {
            'third': {
                'fourth': 'the end'
             }
         }
     }
}

Я бы хотел получить к нему доступ

test = get_entry(mydict, 'first.second.third.fourth')

То, что я до сих пор

def get_entry(dict, keyspec):
    keys = keyspec.split('.')

    result = dict[keys[0]]
    for key in keys[1:]:
       result = dict[key]

    return result

Есть ли более эффективные способы сделать это? Согласно% timeit, время выполнения функции равно 1.26us, при этом доступ к словарю осуществляется стандартным способом

foo = mydict['first']['second']['third']['fourth']

берет 541ns. Я ищу способы обрезать его до диапазона 800ns, если это возможно.

Спасибо

Ответ 1

Там действительно только одно решение. Перестройте словарь. Но сделайте это только один раз.

def recursive_flatten(mydict):
    d = {}
    for k, v in mydict.items():
        if isinstance(v, dict):
            for k2, v2 in recursive_flatten(v).items():
                d[k + '.' + k2] = v2 
        else:
            d[k] = v
    return d

In [786]: new_dict = recursive_flatten(mydict); new_dict
Out[786]: {'first.second.third.fourth': 'the end'}

(Еще несколько тестов)

In [788]: recursive_flatten({'x' : {'y' : 1, 'z' : 2}, 'y' : {'a' : 5}, 'z' : 2})
Out[788]: {'x.y': 1, 'x.z': 2, 'y.a': 5, 'z': 2}

In [789]: recursive_flatten({'x' : 1, 'y' : {'x' : 234}})
Out[789]: {'x': 1, 'y.x': 234}

Каждый доступ становится постоянным.

Теперь просто получите доступ к вашему значению с помощью new_dict['first.second.third.fourth']. Должен работать для любого произвольно вложенного словаря, который не содержит саморекламы.

Обратите внимание, что каждое решение имеет свою долю компромиссов, это не исключение. Если вы не уволили миллионы запросов по вашим данным, так что препроцессинг является приемлемым накладным расходами, то это все. С другими решениями вы только обошли проблему, а не обратились к ней, - что касается структуры словаря. OTOH, если вы собираетесь сделать это один раз во многих подобных структурах данных, нет смысла препроцессить только для одного запроса, и в этом случае вы можете предпочесть одно из других решений.

Ответ 2

Я получил повышение производительности на 20%, немного сократив код, но на 400% увеличил использование кеша для разделенных строк. Это только имеет значение, если вы используете один и тот же параметр несколько раз. Ниже приведены примеры реализации и сценарий профиля для тестирования.

test.py

mydict = {
    'first': {
        'second': {
            'third': {
                'fourth': 'the end'
             }
         }
     }
}

# original
def get_entry(dict, keyspec):
    keys = keyspec.split('.')

    result = dict[keys[0]]
    for key in keys[1:]:
       result = result[key]

    return result

# tighten up code
def get_entry_2(mydict, keyspec):
    for key in keyspec.split('.'):
        mydict = mydict[key]
    return mydict

# use a cache
cache = {}
def get_entry_3(mydict, keyspec):
    global cache
    try:
        spec = cache[keyspec]
    except KeyError:
        spec = tuple(keyspec.split('.'))
        cache[keyspec] = spec

    for key in spec:
        mydict = mydict[key]
    return mydict

if __name__ == "__main__":
    test = get_entry(mydict, 'first.second.third.fourth')
    print(test)

profile.py

from timeit import timeit
print("original get_entry")
print(timeit("get_entry(mydict, 'first.second.third.fourth')",
    setup="from test import get_entry, mydict"))

print("get_entry_2 with tighter code")
print(timeit("get_entry_2(mydict, 'first.second.third.fourth')",
    setup="from test import get_entry_2, mydict"))

print("get_entry_3 with cache of split spec")
print(timeit("get_entry_3(mydict, 'first.second.third.fourth')",
    setup="from test import get_entry_3, mydict"))

print("just splitting a spec")
print(timeit("x.split('.')", setup="x='first.second.third.fourth'"))

Время на моей машине

original get_entry
4.148535753000033
get_entry_2 with tighter code
3.2986323120003362
get_entry_3 with cache of split spec
1.3073233439990872
just splitting a spec
1.0949148639992927

Обратите внимание, что разделение спецификации является относительно дорогостоящей операцией для этой функции. Вот почему помогает кеширование.

Ответ 3

Я обновил ответ от " Как использовать точку". для доступа к членам словаря? использовать начальное преобразование, которое затем будет работать для вложенных словарей:

Вы можете использовать следующий класс, чтобы разрешить индексирование словарей:

class dotdict(dict):
    """dot.notation access to dictionary attributes"""
    __getattr__ = dict.get
    __setattr__ = dict.__setitem__
    __delattr__ = dict.__delitem__

Однако это поддерживает только вложенность, если все вложенные словари также имеют тип dotdict. То, что происходит со следующей вспомогательной функцией:

def dct_to_dotdct(d):
    if isinstance(d, dict):
        d = dotdict({k: dct_to_dotdct(v) for k, v in d.items()})
    return d

Эта функция должна запускаться один раз в вашем вложенном словаре, и результат может быть проиндексирован с помощью точечной индексации.

Вот некоторые примеры:

In [13]: mydict
Out[13]: {'first': {'second': {'third': {'fourth': 'the end'}}}}

In [14]: mydict = dct_to_dotdct(mydict)

In [15]: mydict.first.second
Out[15]: {'third': {'fourth': 'the end'}}

In [16]: mydict.first.second.third.fourth
Out[16]: 'the end'

Заметка о производительности: этот ответ медленный по сравнению со стандартным доступом к словарю, я просто хотел представить вариант, который фактически использовал "точечный доступ" к словарю.

Ответ 4

Вот решение, похожее на chrisz's, но вам не нужно ничего для вашего dict a-before. :

class dictDotter(dict):
    def __getattr__(self,key):
        val = self[key]
        return val if type(val) != dict else dictDotter(val)

и просто x=dictDotter(originalDict) позволит вам произвольно получить точку ("x.first.second...). Я отмечу, что это вдвое медленнее, чем решение chrisz, и его в 9 раз меньше, чем у вас (на моей машине примерно).

Итак, если вы настаиваете на том, чтобы сделать эту работу, @tdananey, похоже, обеспечил единственное реальное улучшение производительности.

Другой вариант, который лучше, чем у вас (с точки зрения времени выполнения):

class dictObjecter:
    def __init__(self,adict):
        for k,v in adict.items():
            self.__dict__[k] = v
            if type(v) == dict: self.__dict__[k] = dictObjecter(v)

который сделает объект из вашего dict, поэтому нотация точки обычна. Это улучшит время выполнения до 3-х раз, что у вас есть, так что неплохо, но за счет перехода на ваш dict и замены его чем-то другим.

Вот полный тестовый код:

from timeit import timeit

class dictObjecter:
    def __init__(self,adict):
        for k,v in adict.items():
            self.__dict__[k] = v
            if type(v) == dict: self.__dict__[k] = dictObjecter(v)

class dictDotter(dict):
    def __getattr__(self,key):
        val = self[key]
        return val if type(val) != dict else dictDotter(val)

def get_entry(dict, keyspec):
    keys = keyspec.split('.')

    result = dict[keys[0]]
    for key in keys[1:]:
        result = result[key]

    return result

class dotdict(dict):
    """dot.notation access to dictionary attributes"""
    __getattr__ = dict.get
    __setattr__ = dict.__setitem__
    __delattr__ = dict.__delitem__

def dct_to_dotdct(d):
    if isinstance(d, dict):
        d = dotdict({k: dct_to_dotdct(v) for k, v in d.items()})
    return d

x = {'a':{'b':{'c':{'d':1}}}}
y = dictDotter(x)
z = dct_to_dotdct(x)
w = dictObjecter(x)
print('{:15} : {}'.format('dict dotter',timeit('y.a.b.c.d',globals=locals(),number=1000)))
print('{:15} : {}'.format('dot dict',timeit('z.a.b.c.d',globals=locals(),number=1000)))
print('{:15} : {}'.format('dict objecter',timeit('w.a.b.c.d',globals=locals(),number=1000)))
print('{:15} : {}'.format('original',timeit("get_entry(x,'a.b.c.d')",globals=locals(),number=1000)))
print('{:15} : {:.20f}'.format('best ref',timeit("x['a']['b']['c']['d']",globals=locals(),number=1000)))

Я предоставил последний регулярный поиск как наилучшую ссылку. Результаты в подсистеме Windows Ubuntu:

dict dotter     : 0.0035500000003594323
dot dict        : 0.0017939999997906853
dict objecter   : 0.00021699999979318818
original        : 0.0006629999998040148
best ref        : 0.00007999999979801942

поэтому объективированный dict в 3 раза медленнее обычного поиска в словаре - так что если скорость важна, зачем вам это нужно?

Ответ 5

У меня была такая же потребность, поэтому я создал Продик.

Для вашего случая вы можете сделать это в одной строке:

mydict = {
    'first': {
        'second': {
            'third': {
                'fourth': 'the end'
             }
         }
     }
}
dotdict = Prodict.from_dict(mydict)
print(dotdict.first.second.third.fourth) # "the end"

После этого используйте dotdict точно так же, как dict, потому что это подкласс dict:

dotdict.first == dotdict['first'] # True

Вы также можете добавить дополнительные клавиши динамически с помощью точечной нотации:

dotdict.new_key = 'hooray'
print(dotdict.new_key) # "hooray"

Он работает, даже если новые ключи являются вложенными словарями:

dotdict.it = {'just': 'works'}
print(dotdict.it.just)  # "works"

Наконец, если вы заранее определите свои ключи, вы получите автоматическое завершение и автоматическое преобразование типов:

class User(Prodict):
    user_id: int
    name: str

user = User(user_id="1", "name":"Ramazan")
type(user.user_id) # <class 'int'>
# IDE will be able to auto complete 'user_id' and 'name' properties

ОБНОВЛЕНИЕ:

Это результат теста для того же кода, написанного @kabanus:

x = {'a': {'b': {'c': {'d': 1}}}}
y = dictDotter(x)
z = dct_to_dotdct(x)
w = dictObjecter(x)
p = Prodict.from_dict(x)

print('{:15} : {}'.format('dict dotter', timeit('y.a.b.c.d', globals=locals(), number=10000)))
print('{:15} : {}'.format('prodict', timeit('p.a.b.c.d', globals=locals(), number=10000)))
print('{:15} : {}'.format('dot dict', timeit('z.a.b.c.d', globals=locals(), number=10000)))
print('{:15} : {}'.format('dict objecter', timeit('w.a.b.c.d', globals=locals(), number=10000)))
print('{:15} : {}'.format('original', timeit("get_entry(x,'a.b.c.d')", globals=locals(), number=10000)))
print('{:15} : {:.20f}'.format('prodict getitem', timeit("p['a']['b']['c']['d']", globals=locals(), number=10000)))
print('{:15} : {:.20f}'.format('best ref', timeit("x['a']['b']['c']['d']", globals=locals(), number=10000)))

И результаты:

dict dotter     : 0.04535976458466595
prodict         : 0.02860781018446784
dot dict        : 0.019078164088831673
dict objecter   : 0.0017378700050722368
original        : 0.006594238310349346
prodict getitem : 0.00510931794975705289
best ref        : 0.00121740293554022105

Как вы можете видеть, его производительность находится между "dict dotter" и "dot dict". Любые предложения по повышению производительности будут оценены.

Ответ 6

Код должен быть менее итеративным и более динамичным!

данные

mydict = {
    'first': {
        'second': {
            'third': {
                'fourth': 'the end'
             }
         }
     }
}

функция

def get_entry(dict, keyspec):
    for keys in keyspec.split('.'):
        dict = dict[keys]
    return dict

вызвать функцию

res = get_entry(mydict, 'first.second.third.fourth')

это займет меньше времени, чтобы выполнить даже динамическое выполнение кода!

Ответ 7

Вы можете использовать reduce (functools.reduce в Python3):

import operator
def get_entry(dct, keyspec):
    return reduce(operator.getitem, keyspec.split('.'), dct)

Это более красиво выглядит, но с немного меньшей производительностью.

Время в вашей версии:

>>> timeit("get_entry_original(mydict, 'first.second.third.fourth')",
           "from __main__ import get_entry_original, mydict", number=1000000)
0.5646841526031494

с уменьшением:

>>> timeit("get_entry(mydict, 'first.second.third.fourth')",
           "from __main__ import get_entry, mydict")
0.6140949726104736

Поскольку уведомление tdelaney - раскол потребляет почти столько же мощности процессора, сколько и ключ в dict:

def split_keys(keyspec):
    keys = keyspec.split('.')

timeit("split_keys('first.second.third.fourth')",
       "from __main__ import split_keys")
0.28857898712158203

Просто переместите строку, get_entry от функции get_entry :

def get_entry(dct, keyspec_list):
    return reduce(operator.getitem, keyspec_list, dct)


timeit("get_entry(mydict, ['first', 'second', 'third', 'fourth'])",
       "from __main__ import get_entry, mydict")
0.37825703620910645