Existe uma maneira de subclassificar dict e collections.abc.MutableMapping juntos?

26

Vamos, por exemplo, supor que eu quero subclassear dicte ter todas as chaves em maiúsculas:

class capdict(dict):
    def __init__(self,*args,**kwds):
        super().__init__(*args,**kwds)
        mod = [(k.capitalize(),v) for k,v in super().items()]
        super().clear()
        super().update(mod)
    def __getitem__(self,key):
        return super().__getitem__(key.capitalize())
    def __setitem__(self,key,value):
        super().__setitem__(key.capitalize(),value)
    def __delitem__(self,key):
        super().__detitem__(key.capitalize())

Isso funciona até certo ponto,

>>> ex = capdict(map(reversed,enumerate("abc")))
>>> ex
{'A': 0, 'B': 1, 'C': 2}
>>> ex['a']
0

mas, é claro, apenas para métodos que lembrei de implementar, por exemplo

>>> 'a' in ex
False

não é o comportamento desejado.

Agora, a maneira preguiçosa de preencher todos os métodos que podem ser derivados dos "essenciais" estaria se misturando collections.abc.MutableMapping. Só que não funciona aqui. Presumo que os métodos em questão ( __contains__no exemplo) já sejam fornecidos por dict.

Existe uma maneira de comer meu bolo e comê-lo? Alguma mágica para deixar MutableMappingapenas ver os métodos que eu substituí para que ele reimplemente os outros com base nesses?

Paul Panzer
fonte
Você pode não precisar usar MutableMapping. Consulte Dicionário que não diferencia maiúsculas de minúsculas .
martineau 26/01
@martineau obrigado, como eu disse, era apenas um exemplo.
Paul Panzer
Você poderia usar os._Environ.
Peter Wood

Respostas:

26

O que você poderia fazer:

Isso provavelmente não funcionará bem (ou seja, não é o design mais limpo), mas você pode herdar do MutableMapping primeiro e depois do ditado segundo.

O MutableMapping usaria os métodos que você implementou (porque são os primeiros na cadeia de pesquisa):

>>> class D(MutableMapping, dict):
        def __getitem__(self, key):
            print(f'Intercepted a lookup for {key!r}')
            return dict.__getitem__(self, key)


>>> d = D(x=10, y=20)
>>> d.get('x', 0)
Intercepted a lookup for 'x'
10
>>> d.get('z', 0)
Intercepted a lookup for 'z'
0

Melhor maneira:

A abordagem mais limpa (fácil de entender e testar) é apenas herdar do MutableMapping e implementar os métodos necessários usando um ditado regular como armazenamento de dados base (com composição e não herança):

>>> class CapitalizingDict(MutableMapping):
        def __init__(self, *args, **kwds):
            self.store = {}
            self.update(*args, **kwds)
        def __getitem__(self, key):
            key = key.capitalize()
            return self.store[key]
        def __setitem__(self, key, value):
            key = key.capitalize()
            self.store[key] = value
        def __delitem__(self, key):
            del self.store[key]
        def __len__(self):
            return len(self.store)
        def __iter__(self):
            return iter(self.store)
        def __repr__(self):
            return repr(self.store)


>>> d = CapitalizingDict(x=10, y=20)
>>> d
{'X': 10, 'Y': 20}
>>> d['x']
10
>>> d.get('x', 0)
10
>>> d.get('z', 0)
0
>>> d['w'] = 30
>>> d['W']
30
Raymond Hettinger
fonte
Obrigado! Eu poderia jurar que tentei os dois pedidos ... Fora de interesse, quando eu uso o método "could do" trocando todos os supers por dicts explícitos , ele parece funcionar, exceto lenretornos 0. De onde isso está vindo?
Paul Panzer
2
A chamada super () a partir __len _ __ () vai para o próximo na OPR: (D, MutableMapping, dict). Esse é o método MutableMappiing .__ len __ () que sempre retorna 0. Não era para ser chamado diretamente - ele sempre deve ser substituído. É por isso que você precisa ligar dict.__len__(self)diretamente. E essa é uma das razões pelas quais eu disse "isso provavelmente não vai funcionar bem" ;-)
Raymond Hettinger