Nested dictionary that acts as defaultdict when setting items but not when getting items

梦想的初衷 提交于 2020-05-08 04:45:46

问题


I want to implement a dict-like data structure that has the following properties:

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'

The problem, to my knowledge, is that python does not know whether __getitem__ is being called in the context of setting an item, i.e. the first example, or in the context of getting and item, the second example.

I have already seen Python `defaultdict`: Use default when setting, but not when getting, but I do not think that this question is a duplicate, or that it answers my question.

Please let me know if you have any ideas.

Thanks in advance.

EDIT:

It is possible to achieve something similar using:

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:


It's impossible.

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

is semantically equivalent to:

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

But... if determined to implement it anyway, you could inspect the Python stack to grab/parse the calling line of code, and then vary the behaviour depending on whether the calling line of code contains an assignment! Unfortunately, sometimes the calling code isn't available in the stack trace (e.g. when called interactively), in which case you need to work with Python bytecode.

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'

The above is just a partial solution. It can still be fooled if there are other dictionary/array assignments on the same line, e.g. other['key'] = test_dict['unknown_key']. A more complete solution would need to actually parse the line of code to figure out where the variable occurs in the assignment.



来源:https://stackoverflow.com/questions/61118676/nested-dictionary-that-acts-as-defaultdict-when-setting-items-but-not-when-getti

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!