自定义字典以在 ** 上维护它的 __getitem__(星-星-拆包)

Custom dictionary to maintain it's __getitem__ on ** (star-star-unpacking)

大家圣诞快乐,

我正在实现一个允许属性访问的自定义字典,例如dct.attribute。字典可以嵌套,所以 dct.nested_dct.attribute 应该也是可以的。这已经很好用了,除了 star-star-unpacking。我认为我能够使用代码比文字更好地表达我想做的事情。所以这是我正在写的 class。测试应该非常清楚地解释它的作用:

class DotDict(dict):
    def __getattr__(self, item):
        return self.__getitem__(item)

    def __getitem__(self, item):
        item = super().__getitem__(item)
        if isinstance(item, dict):
            return self.__class__(item)
        return item


class TestDotDict:
    @pytest.fixture
    def dot_dict(self):
        input_dict = dict(
            a=1,
            b=dict(
                c=2,
                d=3,
            )
        )
        return DotDict(input_dict)

    def test_can_access_by_dot(self, dot_dict):
        assert dot_dict.a == 1

    def test_returned_dicts_are_dot_dicts(self, dot_dict):
        b_dict = dot_dict["b"]
        assert isinstance(b_dict, DotDict)
        assert b_dict.c == 2

    def test_getting_item_also_returns_dot_dicts(self, dot_dict):
        b_dict = dot_dict["b"]
        assert isinstance(b_dict, DotDict)
        assert b_dict.c == 2

    def test_unpack_as_function_arguments_yields_dot_dicts_for_children(self, dot_dict):
        # this is failing
        def checker(a, b):
            assert a == 1
            assert b.c == 2
        checker(**dot_dict)

如评论中所述,上次测试失败。有人知道如何解决吗?

根据这个问题的答案:,我认为我需要继承 collections.abc.Mappingdict。但是,这并没有解决问题。

我在想这可能与我不太清楚的 MRO 有关。但是无论我是否将 class 定义更改为

class DotDict(Mapping, item):

class DotDict(item, Mapping):

我的测试不会变绿。

test_star_star_mapping_maintains_child_dot_dicts 中,您正在创建 dict 而不是 DotDict 因此,重构为:

def test_star_star_mapping_maintains_child_dot_dicts(self, dot_dict):
    obtained_via_star = DotDict(dict(**dot_dict))
    b_dict = obtained_via_star["b"]
    assert b_dict.c == 2

将使测试通过,因为您现在正在创建 DotDict。也许你想删除部分 dict(**dot_dict) 所以这个版本也可以工作:

def test_star_star_mapping_maintains_child_dot_dicts(self, dot_dict):
    obtained_via_star = DotDict(**dot_dict)
    b_dict = obtained_via_star["b"]
    assert b_dict.c == 2

哇,尝试 运行 以下代码未注释 __iter__

class DotDict(dict):
#    def __iter__(self):
#        return super().__iter__()

    def __getattr__(self, item):
        return self.__getitem__(item)

    def __getitem__(self, item):
        item = super().__getitem__(item)
        if isinstance(item, dict):
            return self.__class__(item)
        return item

d = DotDict({'a': {'b':'c'}})

print(type(dict(**d)['a']))

非常非常奇怪

您面临的问题是您正在尝试构建原生 dict - 对于此 class,__getitem__ 只是其值的几种方式之一检索。由于字典在 Python 中的实现方式,出于历史和性能原因,有很多方法可以完全绕过 __getitem__,因此,嵌套字典永远不会 "wrapped"在 DotDict 中。 (例如:.values()items(),starmap甚至可能会绕过这些)

你真正想要的是 subclass collections.abc.MutableMapping - 它的构造方式确保任何项目检索都将通过 __getitem__,(你必须实施文档中指示的方法,包括 __delitem____setitem____iter__ - 建议将实际数据作为普通字典保存在 .data 中创建的属性中__init__ 方法)。

意识到这也可以让您更好地控制数据,例如,使您能够将数据包装在自定义的 class 中,直接放在 setitem 上,而不必关心属性检索 -或者,反过来,将任何映射存储为普通字典以节省内存和提高效率,并在检索时将其包装起来。