¿Existe una forma inteligente de pasar la clave a default_factory de defaultdict?

92

Una clase tiene un constructor que toma un parámetro:

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

En algún lugar del código, es útil que los valores de un dictado conozcan sus claves.
Quiero usar un defaultdict con la clave pasada a los valores predeterminados recién nacidos:

d = defaultdict(lambda : C(here_i_wish_the_key_to_be))

¿Alguna sugerencia?

Benjamin Nitlehoo
fuente

Respuestas:

127

Difícilmente califica como inteligente , pero las subclases son tus amigos:

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
fuente
16
Esa es exactamente la fealdad que estoy tratando de evitar ... Incluso usar un simple dictado y verificar la existencia de la clave es mucho más limpio.
Benjamin Nitlehoo
1
@Paul: y sin embargo, esta es tu respuesta. ¿Fealdad? ¡Venga!
tzot
4
Creo que solo tomaré ese fragmento de código y lo pondré en mi módulo personalizado de utilidades generales para poder usarlo cuando quiera. No tan feo de esa manera ...
weronika
24
+1 Aborda directamente la pregunta del OP y no me parece "feo". También es una buena respuesta porque muchos no parecen darse cuenta de que defaultdictel __missing__()método de se puede anular (como puede ser en cualquier subclase de la dictclase incorporada desde la versión 2.5).
Martineau
7
+1 El propósito de __missing__ es personalizar el comportamiento de las claves faltantes. El enfoque dict.setdefault () mencionado por @silentghost también funcionaría (en el lado positivo, setdefault () es corto y ya existe; en el lado negativo, sufre problemas de eficiencia y a nadie realmente le gusta el nombre "setdefault") .
Raymond Hettinger
26

No no hay.

La defaultdictimplementación no se puede configurar para pasar la falta keya la lista default_factoryde fábrica. Tu única opción es implementar tu propiadefaultdict subclase, como lo sugiere @JochenRitzel, arriba.

Pero eso no es "inteligente" ni tan limpio como lo sería una solución de biblioteca estándar (si existiera). Por lo tanto, la respuesta a su sucinta pregunta sí / no es claramente "No".

Es una lástima que a la biblioteca estándar le falte una herramienta tan necesaria.

Stuart Berg
fuente
Sí, hubiera sido una mejor elección de diseño dejar que la fábrica tomara la llave (función unaria en lugar de nulary). Es fácil descartar un argumento cuando queremos devolver una constante.
YvesgereY
6

No creo que necesites estar defaultdictaquí en absoluto. ¿Por qué no usar el dict.setdefaultmétodo?

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

Eso, por supuesto, crearía muchos casos de C. En caso de que sea un problema, creo que el enfoque más simple servirá:

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

Sería más rápido que el defaultdicto cualquier otra alternativa por lo que puedo ver.

ETA con respecto a la velocidad de la inprueba frente al uso de la 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
fuente
7
Esto es un gran desperdicio en los casos en los que se accede a d muchas veces, y rara vez se pierde una clave: C (clave) creará toneladas de objetos innecesarios para que el GC los recopile. Además, en mi caso hay un problema adicional, ya que la creación de nuevos objetos C es lenta.
Benjamin Nitlehoo
@Paul: eso es correcto. Sugeriría entonces un método aún más simple, vea mi edición.
SilentGhost
No estoy seguro de que sea más rápido que defaultdict, pero esto es lo que suelo hacer (vea mi comentario a la respuesta de THC4k). Esperaba que hubiera una forma sencilla de solucionar el hecho de que default_factory no toma argumentos, para mantener el código un poco más elegante.
Benjamin Nitlehoo
5
@SilentGhost: No entiendo, ¿cómo resuelve esto el problema del OP? Pensé que OP quería que cualquier intento de lectura d[key]regresara d[key] = C(key)si key not in d. ¿Pero su solución requiere que él vaya y preestablezca de d[key]antemano? ¿Cómo sabría cuál keynecesitaría?
máximo
2
Porque setdefault es feo como el infierno y el defaultdict de la colección DEBE soportar una función de fábrica que reciba la clave. ¡Qué oportunidad perdida por parte de los diseñadores de Python!
jgomo3
0

Este es un ejemplo funcional de un diccionario que agrega un valor automáticamente. La tarea de demostración para encontrar archivos duplicados en / usr / include. Tenga en cuenta que el diccionario de personalización PathDict solo requiere cuatro líneas:

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
fuente