Existe-t-il un moyen intelligent de transmettre la clé à defaultdict's default_factory?


93

Une classe a un constructeur qui prend un paramètre:

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

Quelque part dans le code, il est utile pour les valeurs d'un dict de connaître leurs clés.
Je souhaite utiliser un defaultdict avec la clé transmise aux valeurs par défaut du nouveau-né:

d = defaultdict(lambda : C(here_i_wish_the_key_to_be))

Aucune suggestion?

Réponses:


127

Cela n'est guère considéré comme intelligent - mais le sous- classement est votre ami:

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)

16
C'est exactement la laideur que j'essaie d'éviter ... Même utiliser un simple dict et vérifier l'existence d'une clé est beaucoup plus propre.
Benjamin Nitlehoo

1
@Paul: et pourtant c'est votre réponse. La laideur? Allons!
tzot

4
Je pense que je vais juste prendre ce morceau de code et le mettre dans mon module d'utilitaires généraux personnalisé afin que je puisse l'utiliser quand je veux. Pas trop moche comme ça ...
weronika

24
+1 Répond directement à la question du PO et ne me paraît pas "moche". Aussi une bonne réponse car beaucoup ne semblent pas se rendre compte que defaultdictla __missing__()méthode de s peut être surchargée (comme c'est le cas dans n'importe quelle sous-classe de la classe intégrée dictdepuis la version 2.5).
martineau

7
+1 Le but de __missing__ est de personnaliser le comportement des clés manquantes. L'approche dict.setdefault () mentionnée par @silentghost fonctionnerait également (du côté positif, setdefault () est court et existe déjà; du côté négatif, il souffre de problèmes d'efficacité et personne n'aime vraiment le nom "setdefault") .
Raymond Hettinger

26

Non, il n'y en a pas.

L' defaultdictimplémentation ne peut pas être configurée pour transmettre les données manquantes keyau prêt à l' default_factoryemploi. Votre seule option est d'implémenter votre propre defaultdictsous-classe, comme suggéré par @JochenRitzel, ci-dessus.

Mais ce n'est pas «intelligent» ou presque aussi propre qu'une solution de bibliothèque standard le serait (si elle existait). Ainsi, la réponse à votre question succincte oui / non est clairement «non».

C'est dommage que la bibliothèque standard ne dispose pas d'un outil aussi souvent nécessaire.


Oui, cela aurait été un meilleur choix de conception de laisser l'usine prendre la clé (fonction unaire plutôt que nullaire). Il est facile de rejeter un argument lorsque nous voulons renvoyer une constante.
YvesgereY

6

Je ne pense pas que vous ayez besoin defaultdictici du tout. Pourquoi ne pas simplement utiliser la dict.setdefaultméthode?

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

Cela créerait bien sûr de nombreux exemples de C. Au cas où c'est un problème, je pense que l'approche la plus simple fera l'affaire:

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

Ce serait plus rapide que la defaultdictou toute autre alternative pour autant que je sache.

ETA concernant la vitesse du intest par rapport à l'utilisation de la clause 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

7
C'est un gaspillage important dans les cas où d est accédé plusieurs fois, et il ne manque que rarement une clé: C (clé) créera ainsi des tonnes d'objets inutiles que le GC devra collecter. De plus, dans mon cas, il y a une douleur supplémentaire, car la création de nouveaux objets C est lente.
Benjamin Nitlehoo

@Paul: c'est vrai. Je suggérerais alors une méthode encore plus simple, voir ma modification.
SilentGhost

Je ne suis pas sûr que ce soit plus rapide que defaultdict, mais c'est ce que je fais habituellement (voir mon commentaire sur la réponse de THC4k). J'espérais qu'il existe un moyen simple de contourner le fait que default_factory ne prend aucun argument, pour garder le code un peu plus élégant.
Benjamin Nitlehoo

5
@SilentGhost: Je ne comprends pas - comment cela résout-il le problème du PO? Je pensais que OP voulait toute tentative de lecture d[key]pour revenir d[key] = C(key)si key not in d. Mais votre solution l'oblige à y aller et à pré-régler d[key]à l'avance? Comment saurait-il ce dont keyil a besoin?
max

2
Parce que setdefault est moche comme l'enfer et que le defaultdict de la collection DEVRAIT supporter une fonction d'usine qui reçoit la clé. Quelle opportunité gâchée des concepteurs de Python!
jgomo3

0

Voici un exemple fonctionnel d'un dictionnaire qui ajoute automatiquement une valeur. La tâche de démonstration pour trouver des fichiers en double dans / usr / include. Notez que le dictionnaire personnalisé PathDict ne nécessite que quatre lignes:

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)))
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.