Como posso tornar a subclasse de ditado o mais "perfeita" possível?
O objetivo final é ter um ditado simples em que as chaves estejam em minúsculas.
Se eu substituir __getitem__
/ __setitem__
, obter / definir não funcionará. Como faço para fazê-los funcionar? Certamente não preciso implementá-los individualmente?
Estou impedindo que a decapagem funcione e preciso implementar
__setstate__
etc?
Preciso de repr, atualização e __init__
?
Devo apenas usar mutablemapping
(parece que não se deve usar UserDict
ou DictMixin
)? Se sim, como? Os documentos não são exatamente esclarecedores.
A resposta aceita seria minha primeira abordagem, mas como ela tem alguns problemas e como ninguém abordou a alternativa, subclassificou um dict
, eu vou fazer isso aqui.
O que há de errado com a resposta aceita?
Parece um pedido bastante simples para mim:
Como posso tornar a subclasse de ditado o mais "perfeita" possível? O objetivo final é ter um ditado simples em que as chaves estejam em minúsculas.
A resposta aceita não é realmente subclasse dict
e um teste para isso falha:
>>> isinstance(MyTransformedDict([('Test', 'test')]), dict)
False
Idealmente, qualquer código de verificação de tipo estaria testando a interface que esperamos ou uma classe base abstrata, mas se nossos objetos de dados estiverem sendo passados para funções que estão sendo testadas dict
- e não podemos "consertar" essas funções, esse código vai falhar.
Outras queixas que se pode fazer:
- A resposta aceita também está faltando o classmethod:
fromkeys
.
A resposta aceita também possui uma redundância __dict__
- portanto, ocupa mais espaço na memória:
>>> s.foo = 'bar'
>>> s.__dict__
{'foo': 'bar', 'store': {'test': 'test'}}
Na verdade, subclassificação dict
Podemos reutilizar os métodos de ditado por herança. Tudo o que precisamos fazer é criar uma camada de interface que garanta que as chaves sejam passadas para o dict em forma minúscula, se forem strings.
Se eu substituir __getitem__
/ __setitem__
, obter / definir não funcionará. Como faço para fazê-los funcionar? Certamente não preciso implementá-los individualmente?
Bem, implementá-los individualmente é a desvantagem dessa abordagem e a vantagem de usar MutableMapping
(veja a resposta aceita), mas na verdade não é muito mais trabalho.
Primeiro, vamos fatorar a diferença entre Python 2 e 3, criar um singleton ( _RaiseKeyError
) para garantir que saibamos se realmente obtemos um argumento dict.pop
e criar uma função para garantir que nossas chaves de string sejam minúsculas:
from itertools import chain
try: # Python 2
str_base = basestring
items = 'iteritems'
except NameError: # Python 3
str_base = str, bytes, bytearray
items = 'items'
_RaiseKeyError = object() # singleton for no-default behavior
def ensure_lower(maybe_str):
"""dict keys can be any hashable object - only call lower if str"""
return maybe_str.lower() if isinstance(maybe_str, str_base) else maybe_str
Agora implementamos - estou usando super
os argumentos completos para que esse código funcione para Python 2 e 3:
class LowerDict(dict): # dicts take a mapping or iterable as their optional first argument
__slots__ = () # no __dict__ - that would be redundant
@staticmethod # because this doesn't make sense as a global function.
def _process_args(mapping=(), **kwargs):
if hasattr(mapping, items):
mapping = getattr(mapping, items)()
return ((ensure_lower(k), v) for k, v in chain(mapping, getattr(kwargs, items)()))
def __init__(self, mapping=(), **kwargs):
super(LowerDict, self).__init__(self._process_args(mapping, **kwargs))
def __getitem__(self, k):
return super(LowerDict, self).__getitem__(ensure_lower(k))
def __setitem__(self, k, v):
return super(LowerDict, self).__setitem__(ensure_lower(k), v)
def __delitem__(self, k):
return super(LowerDict, self).__delitem__(ensure_lower(k))
def get(self, k, default=None):
return super(LowerDict, self).get(ensure_lower(k), default)
def setdefault(self, k, default=None):
return super(LowerDict, self).setdefault(ensure_lower(k), default)
def pop(self, k, v=_RaiseKeyError):
if v is _RaiseKeyError:
return super(LowerDict, self).pop(ensure_lower(k))
return super(LowerDict, self).pop(ensure_lower(k), v)
def update(self, mapping=(), **kwargs):
super(LowerDict, self).update(self._process_args(mapping, **kwargs))
def __contains__(self, k):
return super(LowerDict, self).__contains__(ensure_lower(k))
def copy(self): # don't delegate w/ super - dict.copy() -> dict :(
return type(self)(self)
@classmethod
def fromkeys(cls, keys, v=None):
return super(LowerDict, cls).fromkeys((ensure_lower(k) for k in keys), v)
def __repr__(self):
return '{0}({1})'.format(type(self).__name__, super(LowerDict, self).__repr__())
Usamos uma abordagem quase caldeira-plate para qualquer método ou método especial que faz referência a uma chave, mas caso contrário, por herança, temos métodos: len
, clear
, items
, keys
, popitem
, e values
de graça. Embora isso exigisse uma reflexão cuidadosa para acertar, é trivial ver que isso funciona.
(Observe que haskey
foi preterido no Python 2, removido no Python 3.)
Aqui está um pouco de uso:
>>> ld = LowerDict(dict(foo='bar'))
>>> ld['FOO']
'bar'
>>> ld['foo']
'bar'
>>> ld.pop('FoO')
'bar'
>>> ld.setdefault('Foo')
>>> ld
{'foo': None}
>>> ld.get('Bar')
>>> ld.setdefault('Bar')
>>> ld
{'bar': None, 'foo': None}
>>> ld.popitem()
('bar', None)
Estou impedindo que a decapagem funcione e preciso implementar
__setstate__
etc?
decapagem
E a subclasse dict pickles muito bem:
>>> import pickle
>>> pickle.dumps(ld)
b'\x80\x03c__main__\nLowerDict\nq\x00)\x81q\x01X\x03\x00\x00\x00fooq\x02Ns.'
>>> pickle.loads(pickle.dumps(ld))
{'foo': None}
>>> type(pickle.loads(pickle.dumps(ld)))
<class '__main__.LowerDict'>
__repr__
Preciso de repr, atualização e __init__
?
Definimos update
e __init__
, mas você tem uma linda __repr__
por padrão:
>>> ld # without __repr__ defined for the class, we get this
{'foo': None}
No entanto, é bom escrever um __repr__
para melhorar a depuração do seu código. O teste ideal é eval(repr(obj)) == obj
. Se for fácil fazer o seu código, recomendo vivamente:
>>> ld = LowerDict({})
>>> eval(repr(ld)) == ld
True
>>> ld = LowerDict(dict(a=1, b=2, c=3))
>>> eval(repr(ld)) == ld
True
Veja bem, é exatamente o que precisamos para recriar um objeto equivalente - isso é algo que pode aparecer em nossos logs ou nos backtraces:
>>> ld
LowerDict({'a': 1, 'c': 3, 'b': 2})
Conclusão
Devo apenas usar mutablemapping
(parece que não se deve usar UserDict
ou DictMixin
)? Se sim, como? Os documentos não são exatamente esclarecedores.
Sim, essas são mais algumas linhas de código, mas pretendem ser abrangentes. Minha primeira inclinação seria usar a resposta aceita e, se houvesse algum problema, analisaria a minha resposta - pois é um pouco mais complicada e não há ABC para me ajudar a acertar minha interface.
A otimização prematura está buscando maior complexidade na busca de desempenho.
MutableMapping
é mais simples - portanto, obtém uma vantagem imediata, sendo tudo o resto igual. No entanto, para mostrar todas as diferenças, vamos comparar e contrastar.
Devo acrescentar que houve um esforço para colocar um dicionário semelhante no collections
módulo, mas foi rejeitado . Você provavelmente deveria fazer isso:
my_dict[transform(key)]
Deve ser muito mais facilmente depurável.
Compare e contraste
Existem 6 funções de interface implementadas com a MutableMapping
(que está faltando fromkeys
) e 11 com a dict
subclasse. Eu não preciso de implementar __iter__
ou __len__
, mas em vez disso eu tenho que implementar get
, setdefault
, pop
, update
, copy
, __contains__
, efromkeys
- mas estes são bastante trivial, desde que eu posso usar a herança para a maioria dessas implementações.
Ele MutableMapping
implementa algumas coisas em Python que dict
implementam em C - então eu esperaria que uma dict
subclasse tivesse melhor desempenho em alguns casos.
Temos liberdade __eq__
nas duas abordagens - ambas assumindo igualdade apenas se outro ditado for minúsculo - mas, novamente, acho que a dict
subclasse será comparada mais rapidamente.
Resumo:
- a subclasse
MutableMapping
é mais simples, com menos oportunidades de bugs, mas mais lenta, consome mais memória (veja ditado redundante) e falhaisinstance(x, dict)
- a subclasse
dict
é mais rápida, usa menos memória e passa isinstance(x, dict)
, mas tem maior complexidade para implementar.
Qual é mais perfeito? Isso depende da sua definição de perfeito.