Пользовательский словарь для поддержания его __getitem__ на ** (распаковка звезда-звезда) - PullRequest
0 голосов
/ 24 декабря 2018

С Рождеством всех,

Я реализую пользовательский словарь, который разрешает доступ к атрибутам, например, dct.attribute.Словари могут быть вложенными, поэтому dct.nested_dct.attribute также должно быть возможно.Это уже работает довольно хорошо, за исключением распаковки звезда-звезда.Я думаю, что я могу выразить то, что я пытаюсь сделать лучше, используя код, чем слова.Итак, вот класс, который я пишу.Тесты должны достаточно четко объяснить, что они делают:

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.Mapping и dict.Однако это не решило проблему.

Я думал, что это может быть связано с MRO, которое мне не совсем понятно.Но независимо от того, изменю ли я определение класса на

class DotDict(Mapping, item):

или

class DotDict(item, Mapping):

, мои тесты не станут зелеными.

Ответы [ 3 ]

0 голосов
/ 24 декабря 2018

вау, попробуйте запустить следующий код без комментария __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']))

очень, очень странно

0 голосов
/ 24 декабря 2018

Проблема, с которой вы сталкиваетесь, заключается в том, что вы пытаетесь использовать нативный dict - и для этого класса __getitem__ - это только один из нескольких способов получения его значений.Благодаря тому, что в Python реализованы дикты, как по историческим причинам, так и из-за производительности, существует множество способов, которые просто обойдут __getitem__, и поэтому вложенные словари никогда не будут «обернуты» в DotDict.(например: .values(), items() и starmap, возможно, обойдут даже эти)

То, что вы действительно хотите, это создать подкласс collection.abc.MutableMapping - он построен вспособ, который гарантирует, что любой поиск элемента будет проходить через __getitem__, (хотя вам придется реализовать методы, указанные в документации, включая __delitem__, __setitem__ и __iter__ - рекомендация состоит в том, чтобы сохранить фактические данныекак простая диктовка в атрибуте .data, созданном в методе __init__).

Представьте, что это также дает вам лучший контроль над вашими данными, позволяя, например, обернуть ваши данные в свой пользовательский-классировать непосредственно в setitem, и jsut не заботится о получении атрибутов - или, наоборот, хранить любые отображения в виде простых словарей для экономии памяти и эффективности и переносить их при извлечении.

0 голосов
/ 24 декабря 2018

В 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
...