Existe uma maneira inteligente de passar a chave para default_factory do defaultdict?

93

Uma classe tem um construtor que recebe um parâmetro:

class C(object):
    def __init__(self, v):
        self.v = v
        ...

Em algum lugar do código, é útil para valores em um dicionário conhecer suas chaves.
Quero usar um defaultdict com a chave passada para os valores padrão recém-nascidos:

d = defaultdict(lambda : C(here_i_wish_the_key_to_be))

Alguma sugestão?

Benjamin Nitlehoo
fonte

Respostas:

127

Isso dificilmente se qualifica como inteligente - mas a subclasse é sua amiga:

class keydefaultdict(defaultdict):
    def __missing__(self, key):
        if self.default_factory is None:
            raise KeyError( key )
        else:
            ret = self[key] = self.default_factory(key)
            return ret

d = keydefaultdict(C)
d[x] # returns C(x)
Jochen Ritzel
fonte
16
Essa é exatamente a feiura que estou tentando evitar ... Até mesmo usar um ditado simples e verificar a existência da chave é muito mais limpo.
Benjamin Nitlehoo
1
@Paul: e ainda assim esta é a sua resposta. Feiúra? Vamos!
tzot
4
Acho que vou pegar esse pedaço de código e colocá-lo no meu módulo de utilitários gerais personalizado para que possa usá-lo quando quiser. Não tão feio assim ...
weronika
24
O +1 responde diretamente à pergunta do OP e não me parece "feio". Também é uma boa resposta porque muitos parecem não perceber que defaultdicto __missing__()método pode ser sobrescrito (como pode em qualquer subclasse da classe embutida dictdesde a versão 2.5).
martineau de
7
+1 Todo o propósito de __missing__ é personalizar o comportamento para chaves ausentes. A abordagem dict.setdefault () mencionada por @silentghost também funcionaria (no lado positivo, setdefault () é curto e já existe; no lado negativo, ele sofre de problemas de eficiência e ninguém realmente gosta do nome "setdefault") .
Raymond Hettinger de
26

Não, não há.

A defaultdictimplementação não pode ser configurada para passar em falta keypara o default_factoryout-of-the-box. Sua única opção é implementar sua própria defaultdictsubclasse, conforme sugerido por @JochenRitzel, acima.

Mas isso não é "inteligente" ou quase tão limpo quanto uma solução de biblioteca padrão seria (se existisse). Portanto, a resposta à sua pergunta sucinta sim / não é claramente "Não".

É uma pena que a biblioteca padrão não tenha uma ferramenta tão necessária.

Stuart Berg
fonte
Sim, teria sido uma escolha de design melhor deixar a fábrica assumir a chave (função unária em vez de nula). É fácil descartar um argumento quando queremos retornar uma constante.
YvesgereY
6

Eu não acho que você precise defaultdictaqui. Por que não apenas usar o dict.setdefaultmétodo?

>>> d = {}
>>> d.setdefault('p', C('p')).v
'p'

Isso, é claro, criaria muitas instâncias de C. Caso seja um problema, acho que a abordagem mais simples servirá:

>>> d = {}
>>> if 'e' not in d: d['e'] = C('e')

Seria mais rápido do que o defaultdictou qualquer outra alternativa, pelo que posso ver.

ETA em relação à velocidade do inteste versus o uso da cláusula try-except:

>>> def g():
    d = {}
    if 'a' in d:
        return d['a']


>>> timeit.timeit(g)
0.19638929363557622
>>> def f():
    d = {}
    try:
        return d['a']
    except KeyError:
        return


>>> timeit.timeit(f)
0.6167065411074759
>>> def k():
    d = {'a': 2}
    if 'a' in d:
        return d['a']


>>> timeit.timeit(k)
0.30074866358404506
>>> def p():
    d = {'a': 2}
    try:
        return d['a']
    except KeyError:
        return


>>> timeit.timeit(p)
0.28588609450770264
SilentGhost
fonte
7
Isso é um grande desperdício nos casos em que d é acessado muitas vezes e raramente falta uma chave: C (chave) criará toneladas de objetos desnecessários para o GC coletar. Além disso, no meu caso, há uma dor adicional, pois a criação de novos objetos C é lenta.
Benjamin Nitlehoo
@Paul: isso mesmo. Eu sugeriria então um método ainda mais simples, veja minha edição.
SilentGhost
Não tenho certeza se é mais rápido do que defaultdict, mas é o que eu costumo fazer (veja meu comentário à resposta do THC4k). Eu esperava que houvesse uma maneira simples de contornar o fato de que default_factory não leva args, para manter o código um pouco mais elegante.
Benjamin Nitlehoo
5
@SilentGhost: Não entendo - como isso resolve o problema do OP? Achei que OP queria qualquer tentativa de ler d[key]para retornar d[key] = C(key)se key not in d. Mas a sua solução requer que ele vá e pré-configure d[key]com antecedência? Como ele saberia o que keyele precisava?
máx.
2
Porque setdefault é feio como o inferno e o defaultdict da coleção DEVE suportar uma função de fábrica que recebe a chave. Que oportunidade desperdiçada pelos designers do Python!
jgomo3
0

Aqui está um exemplo prático de um dicionário que adiciona um valor automaticamente. A tarefa de demonstração para localizar arquivos duplicados em / usr / include. Observe que o PathDict do dicionário personalizado requer apenas quatro linhas:

class FullPaths:

    def __init__(self,filename):
        self.filename = filename
        self.paths = set()

    def record_path(self,path):
        self.paths.add(path)

class PathDict(dict):

    def __missing__(self, key):
        ret = self[key] = FullPaths(key)
        return ret

if __name__ == "__main__":
    pathdict = PathDict()
    for root, _, files in os.walk('/usr/include'):
        for f in files:
            path = os.path.join(root,f)
            pathdict[f].record_path(path)
    for fullpath in pathdict.values():
        if len(fullpath.paths) > 1:
            print("{} located in {}".format(fullpath.filename,','.join(fullpath.paths)))
Gerardw
fonte