2017-02-08 1 views
5

TL; DR
Как я могу получить superkeys быть autovivified в Словаре Python при присвоении значений подразделов, без также получать их autovivified при проверке подразделы?Как реализовать автовивитацию для вложенного словаря ТОЛЬКО при назначении значений?

фона: Обычно в Python, устанавливая значения в гнездовой словаря требует вручную обеспечения того, чтобы ключи более высокого уровня существуют перед назначением их суб-ключей. То есть,

my_dict[1][2] = 3 

не будет надежно работать, как предполагалось, не первый делать что-то вроде

if 1 not in my_dict: 
    my_dict[1] = {} 

Теперь можно настроить вид autovivification, сделав my_dict экземпляром класса, который переопределяет __missing__, как показано, например в https://stackoverflow.com/a/19829714/6670909.

Вопрос: Тем не менее, это решение молча автогенерирует ключи более высокого уровня, если вы проверяете наличие под-ключа в таком вложенном dict. Это приводит к следующей неудовлетворенности:

>>> vd = Vividict() 
>>> 1 in vd 
False 
>>> 2 in vd[1] 
False 
>>> 1 in vd 
True 

Как я могу избежать этого вводящего в заблуждение результата? В Perl, кстати, я могу получить желаемое поведение, делая

no autovivification qw/exists/; 

И в основном я хотел бы повторить это поведение в Python, если это возможно.

+1

Вы не можете. Нет никакой разницы между доступом 'vd [1]', потому что вы назначаете ему и потому, что видите, что он содержит, насколько это касается 'vd'. Также это не вводящий в заблуждение результат - после того, как вы посмотрели в 'vd [1]', '1' ** - **' in vd'. – jonrsharpe

+0

Право: я надеюсь, что может быть какой-то способ сделать это, например, путем создания класса для вложенных словарей, который ** ** чувствителен к различию между (а) получением элемента просто для того, чтобы проверить существование подпункта и (b) получение элемента в контексте попытки установить значение подпункта. Я думаю, что различие должно было бы быть сделано, фактически, перед неявным вызовом '__getitem__'. –

+0

Нет. '__getitem__' не знает, что делается с результатом при вызове. Раньше не было крючка. Например, вам нужно будет предоставить свой собственный метод, а не использовать 'x in y' -' y.contains (x) ', например. – jonrsharpe

ответ

1

Это не простая задача решить, потому что в вашем примере:

my_dict[1][2] = 3 

my_dict[1] приводит к __getitem__ вызова на словарь. В этот момент нет способа узнать, что задание выполняется. Только последний [] в последовательности представляет собой вызов __setitem__, и он не может быть успешным, если не существует mydict[1], потому что в противном случае какой объект вы назначаете?

Так что не используйте автоматическую трансмиссию. Вместо этого вы можете использовать setdefault(), с обычным dict.

my_dict.setdefault(1, {})[2] = 3 

Теперь это не совсем красиво, особенно, когда вы более глубоко вложенности, так что вы можете написать вспомогательный метод:

class MyDict(dict): 
    def nest(self, keys, value): 
     for key in keys[:-1]: 
      self = self.setdefault(key, {}) 
     self[keys[-1]] = value 

my_dict = MyDict() 
my_dict.nest((1, 2), 3)  # my_dict[1][2] = 3 

Но еще лучше, чтобы обернуть это в новый __setitem__, который принимает все индексы сразу, вместо того, чтобы требовать промежуточных вызовов __getitem__, которые вызывают автообработку. Таким образом, мы знаем с самого начала, что мы выполняем задание и можем продолжать, не полагаясь на автообработку.

class MyDict(dict): 
    def __setitem__(self, keys, value): 
     if not isinstance(keys, tuple): 
      return dict.__setitem__(self, keys, value) 
     for key in keys[:-1]: 
      self = self.setdefault(key, {}) 
     dict.__setitem__(self, keys[-1], value) 

my_dict = MyDict() 
my_dict[1, 2] = 3 

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

def __getitem__(self, keys): 
    if not isinstance(keys, tuple): 
     return dict.__getitem__(self, keys) 
    for key in keys: 
     self = dict.__getitem__(self, key) 
    return self 

Единственным недостатком я могу думать о том, что мы не можем использовать кортежи в качестве словаря ключей, как легко: мы должны написать это как, например my_dict[(1, 2),].

+2

Как это решить проблему OP для автообновления ключей при доступе? то есть 'my_dict [2]' не следует добавлять ключ '2' ... – dhke

+1

Я не понимаю ваше возражение. 'my_dict [2]' не добавляет ключ '2'. – kindall

+0

Это, например, для 'defaultdict (dict)' или если вы переопределите '__missing __()' как значение defaultdict в конечном счете. Следовательно, '1 в my_dict [2]' вызывает появление mydict [2] == {} '. – dhke

1

Правильный ответ: не делайте этого в Python, так как явное лучше, чем неявное.

Но если вы действительно хотите, чтобы автовивитация не содержала пустых суб-словарей, можно эмулировать поведение в Python.

try: 
    from collections import MutableMapping 
except: 
    from collections.abc import MutableMapping 


class AutoDict(MutableMapping, object): 
    def __init__(self, *args, **kwargs): 
     super(AutoDict, self).__init__() 
     self.data = dict(*args, **kwargs) 

    def __getitem__(self, key): 
     if key in self.data: 
      return self.data.__getitem__(key) 
     else: 
      return ChildAutoDict(parent=self, parent_key=key) 

    def __setitem__(self, key, value): 
     return self.data.__setitem__(key, value) 

    def __delitem__(self, key): 
     return self.data.__delitem__(key) 

    def __iter__(self): 
     return self.data.__iter__() 

    def __len__(self): 
     return self.data.__len__() 

    def keys(self): 
     return self.data.keys() 

    def __contains__(self, key): 
     return data.__contains__(key) 

    def __str__(self): 
     return str(self.data) 

    def __unicode__(self): 
     return unicode(self.data) 

    def __repr__(self): 
     return repr(self.data) 

class ChildAutoDict(AutoDict): 
    def __init__(self, parent, parent_key): 
     super(ChildAutoDict, self).__init__() 
     self.parent = parent 
     self.parent_key = parent_key 

    def __setitem__(self, key, value): 
     if self.parent is not None and not self.parent_key in self.parent: 
      # if parent got a new key in the meantime, 
      # don't add ourselves 
      self.parent.data[self.parent_key] = self 
     else: 
      self.parent = None 
     return self.data.__setitem__(key, value) 

    def __delitem__(self, key): 
     ret = self.data.__delitem__(key) 
     # only remove ourselves from the parent if we are 
     # still occupying our slot. 
     if not self and self.parent and self is self.parent[parent_key]: 
      self.parent.data.pop(self.parent_key) 
     return ret 

Что вы получите обратно от __getitem__(), по существу, словарем фасад, который добавляет себя в родительском словаре только если сам не пусто и удаляет себя, когда он становится пустым.

Все это - конечно - перестает работать, как только вы назначаете «нормальный» словарь где-то посередине, т. Е. d[2] = {}, d[2][3] = {} больше не работает и т. Д.

Я не очень тщательно протестировал это, поэтому остерегайтесь больше подводных камней.

d = AutoDict() 

print(1 in d) 
>>> False 
print(d) 
>>> {} 

print(d[2][3]) 
>>> {} 
print(d[2]) 
>>> {} 
print(d) 
>>> {} 

d[2][3] = 1 
print(d) 
>>> {2: {3: 1}} 

del d[2][3] 
print(d) 
>>> {} 
+0

Хм. Это решение, по-видимому, заставляет '1 in d' всегда оценивать значение' True'. Мы получаем очевидную автовивификацию при назначении под-ключам, а также при проверке их присутствия в словаре. Желание, однако, состоит в том, чтобы получить автовычисление супер-ключей при назначении под-ключам и не получить его при проверке существования под-ключей. Для пустого dict, '2 in my_dict [1]' должен возвращать False (no KeyError), а последующее '1 в my_dict' должно возвращать False. –

+0

@ J.Lerman Хм, вы правы, для этого требуется хотя бы дополнительная '__contains __()'. – dhke

+0

'__contains __()' исправлено. Также необходимо вывести из 'object' в Python 2, чтобы у нас появился новый класс стиля (и, таким образом,' __contains __() 'фактически работает). – dhke