Вложенный словарь, который действует как defaultdict при установке элементов, но не при получении элементов - PullRequest
3 голосов
/ 09 апреля 2020

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

from collections import UserDict

class TestDict(UserDict):
    pass

test_dict = TestDict()

# Create empty dictionaries at 'level_1' and 'level_2' and insert 'Hello' at the 'level_3' key.
test_dict['level_1']['level_2']['level_3'] = 'Hello'

>>> test_dict
{
    'level_1': {
        'level_2': {
            'level_3': 'Hello'
        }
    }
}

# However, this should not return an empty dictionary but raise a KeyError.
>>> test_dict['unknown_key']
KeyError: 'unknown_key'

Проблема, насколько мне известно, заключается в том, что python не знает, вызывается ли __getitem__ в контексте установки элемента, то есть в первом примере, или в контексте получения и элемента, во втором примере.

Я уже видел Python `defaultdict`: использовать значение по умолчанию при настройке , но не при получении , но я не думаю, что этот вопрос является дубликатом или что он отвечает на мой вопрос.

Пожалуйста, дайте мне знать, если у вас есть какие-либо идеи.

Заранее спасибо.

РЕДАКТИРОВАТЬ:

Можно добиться чего-то похожего, используя:

def set_nested_item(dict_in: Union[dict, TestDict], value, keys):
    for i, key in enumerate(keys):
        is_last = i == (len(keys) - 1)
        if is_last:
            dict_in[key] = value
        else:
            if key not in dict_in:
                dict_in[key] = {}
            else:
                if not isinstance(dict_in[key], (dict, TestDict)):
                    dict_in[key] = {}

            dict_in[key] = set_nested_item(dict_in[key], value, keys[(i + 1):])
        return dict_in


class TestDict(UserDict):
    def __init__(self):
        super().__init__()

    def __setitem__(self, key, value):
        if isinstance(key, list):
            self.update(set_nested_item(self, value, key))
        else:
            super().__setitem__(key, value)

test_dict[['level_1', 'level_2', 'level_3']] = 'Hello'
>>> test_dict
{
    'level_1': {
        'level_2': {
            'level_3': 'Hello'
        }
    }
}



1 Ответ

1 голос
/ 10 апреля 2020

Это невозможно.

test_dict['level_1']['level_2']['level_3'] = 'Hello'

семантически эквивалентно:

temp1 = test_dict['level_1'] # Should this line fail?
temp1['level_2']['level_3'] = 'Hello'

Но ... если все-таки решено реализовать его, вы можете проверить стек Python, чтобы получить его / анализировать вызывающую строку кода, а затем изменять поведение в зависимости от того, содержит ли вызывающая строка кода присвоение! К сожалению, иногда вызывающий код недоступен в трассировке стека (например, при интерактивном вызове), и в этом случае вам нужно работать с Python байт-кодом.

import dis
import inspect
from collections import UserDict

def get_opcodes(code_object, lineno):
    """Utility function to extract Python VM opcodes for line of code"""
    line_ops = []
    instructions = dis.get_instructions(code_object).__iter__()
    for instruction in instructions:
        if instruction.starts_line == lineno:
            # found start of our line
            line_ops.append(instruction.opcode)
            break
    for instruction in instructions:
        if not instruction.starts_line:
            line_ops.append(instruction.opcode)
        else:
            # start of next line
            break
    return line_ops

class TestDict(UserDict):
    def __getitem__(self, key):
        try:
            return super().__getitem__(key)
        except KeyError:
            # inspect the stack to get calling line of code
            frame = inspect.stack()[1].frame
            opcodes = get_opcodes(frame.f_code, frame.f_lineno)
            # STORE_SUBSCR is Python opcode for TOS1[TOS] = TOS2
            if dis.opmap['STORE_SUBSCR'] in opcodes:
                # calling line of code contains a dict/array assignment
                default = TestDict()
                super().__setitem__(key, default)
                return default
            else:
                raise

test_dict = TestDict()
test_dict['level_1']['level_2']['level_3'] = 'Hello'
print(test_dict)
# {'level_1': {'level_2': {'level_3': 'Hello'}}}

test_dict['unknown_key']
# KeyError: 'unknown_key'

Выше приведено лишь частичное решение , Его все еще можно обмануть, если в той же строке есть другие назначения словаря / массива, например other['key'] = test_dict['unknown_key']. Более полное решение должно было бы фактически проанализировать строку кода, чтобы выяснить, где переменная встречается в присваивании.

...