Chercher dans plusieurs dicts à la fois avec ChainMap 14


Depuis Python 3.3 existe un nouvel outil pour travailler avec les dicos et j’étais complètement passé à côté : ChainMap.

Il permet de créer un objet qui se comporte comme un dict, mais qui va chercher dans plusieurs dictionnaires.

Un exemple sera plus clair.

Imaginez que vous ayez un système de configuration avec des valeurs par défaut :

default_config = {'DEBUG': False, 'HOST': 'localhost', 'PORT': 8080}

Puis votre utilisateur peut fournir un fichier de configuration settings.py qui contient :

DEBUG = True
PORT = 8000

Et avec un peu de parsing, vous le récupérez sous forme de dico :

import settings
user_config = {k: v for k, v in vars(settings).items() if k.isupper()}
## {'DEBUG': True, 'PORT': 8000}

Puis l’utilisateur peut passer la config via la ligne de commande, et une fois il fait :

--host 0.0.0.0

Et vous récupérez la config :

cmd_config = {"HOST": "0.0.0.0"}

Maintenant il faut prendre tout ça en compte. La ligne de commande écrase le fichier de config qui écrase les valeurs par défaut :

conf = {}
conf.update(default_config)
conf.update(user_config)
conf.update(cmd_config)
print(conf) # configuration finale
## {'DEBUG': True, 'HOST': '0.0.0.0', 'PORT': 8000}

Ça va marcher, mais ça a plusieurs défauts :

  • Si vos dicos sont très grands, vous prenez encore plus de place en mémoire.
  • Si vous modifiez conf, impossible de savoir quelle était sa valeur initiale.
  • Si vous modifiez user_config, il faut tout refusionner. Mais si vous avez modifié conf entre temps, comment vous assurer que vous n’allez pas écraser ces modifications ?
  • Si vous voulez temporairement faire une modification à conf, il faut de nouveau créer un dico en plus avec tout dedans.

ChainMap résout ce problème en cherchant une clé dans une liste de dicos sous-jacent, mais en appliquant les modifications uniquement sur le premier dico.

>>> from collections import ChainMap
 
>>> conf = ChainMap({}, # <- ce mapping sera le seul modifié
                    # les clés seront cherchées dans cet ordre :
                    cmd_config, 
                    user_config, 
                    default_config)
 
>>> conf['HOST']
>>> '0.0.0.0'
>>> conf['DEBUG']
>>> True
>>> conf['PORT']
>>> 8000

Les dicos sont ici stockés par référence, ça ne prend pas de mémoire en plus, et si on modifie un dico :

user_config['DEBUG'] = False

Alors c’est reflété par ChainMap:

>>> conf['DEBUG']
False

Si on fait une modification, seul le dico le plus haut dans la chaine (ici notre dico vide), est modifié :

>>> conf["PORT"] = 7777
>>> conf
ChainMap({'PORT': 7777}, {'HOST': '0.0.0.0'}, {'DEBUG': False, 'PORT': 8000}, {'DEBUG': False, 'HOST': 'localhost', 'PORT': 8080})

Et si on a besoin d’un contexte temporaire, on peut créer un enfant :

>>> sub_conf = conf.new_child()
>>> sub_conf
ChainMap({}, {'PORT': 7777}, {'HOST': '0.0.0.0'}, {'DEBUG': False, 'PORT': 8000}, {'DEBUG': False, 'HOST': 'localhost', 'PORT': 8080})

Cela crée un nouveau ChainMap, avec un dico vide en haut de la chaîne, qui permet donc de travailler temporairement avec de nouvelles valeurs, sans toucher au ChainMap d’origine.

14 thoughts on “Chercher dans plusieurs dicts à la fois avec ChainMap

  • Joshua

    Ah mais c’est très cool ça. Je vois une autre utilisation pour les gens qui comme moi font des simulations avec des paramètres passés sous forme de dicos. Comme en général on a toujours besoin des paramètres initiaux à un moment donné, mais qu’en meme temps à chaque simu on change la valeur d’un paramètre, un ChainMap c’est cool.

    Mais question: il y a moyen simple d’accéder au dico d’origine quand meme ? (aux port 8000 ou 8080 par exemple dans ton exemple).

    En passant, il y a un faute dans l’avant-dernier exemple à la dernières valeur du dernier dico.

  • Zanguu

    J’ai pas de 3.3 sous la main, mais si on suis tes commandes tu devrais avoir ‘DEBUG’: False dans le user_config des deux derniers blocs de code, non ?

    En tout cas, comme betepoilue avant_hier, je doit faire parti de votre réseau télépathique.
    Je pensais justement à un système avec la conf de base du site que chaque membre peut surcharger.
    Par exemple, un site de vente de livres avec les infos de base par article et un user peut ajouter le champ “nb pages” sur chaque article si il trouve ça pertinent.
    (Oui osef de ma life mais je suis loin d’implanter un truc comme ça donc si ça peut donner des idées à d’autres je préfère l’écrire)

  • Sam Post author

    Putain mais vous voyez tout c’est hallucinant. Je peux pas glisser une petit faute, discrètement, comme ça, et espérer que personne ne capte. Corrigé.

    @Joshua: oui on peut accéder à tous les dicos précédents via l’attributs maps qui est la listes des mappings que référence le ChainMap.

  • Zanguu

    @Sam, la faute soulignée par Joshua est que tu as : 'PORT 8080}
    au lieu de : 'PORT': 8080})
    Dans l’avant dernier block.

    Par rapport à l’article, tu souligne ceci :

    Si vos dicos sont très grands, vous prenez encore plus de place en mémoire.

    Mais ChainMap garde encore les 3 dicos en mémoire, je ne vois pas le gain sur ce point du coup.

  • Sam Post author

    Ouai mais la soution 1 garde les 3 dicos (pour lire les anciennes valeurs, notamment les valeurs par defaut) + le dico mergé.

    Sur des settings énormes (typiquement Django), c’est quelques ko de gagné.

  • Zanguu

    Ah oui j’ai oublié le mergé, effectivement ça fait gagner de la place.

    Ça marche seulement avec des dict à un niveaux ou on peut lui donner des objet conf et des données plus complexes du genre 'HOST': {'IP': {'0.0.0.0', '127.0.0.1'}, 'PORT':8080}

    ps: il manque encore la parenthèse fermante dans la correction ;)

  • JeromeJ

    Toute petite erreur qui m’a fait gratter mon cuir chevelu pendant 2 sec:

    >>> conf["PORT"] = 7777
    >>> conf
    >>> ChainMap({'PORT': 7777}, {'HOST': '0.0.0.0'}, {'DEBUG': False, 'PORT': 8000}, {'DEBUG': False, 'HOST': 'localhost', 'PORT': 8080})

    Faut retirer les >>> sur la dernière ligne. Voilou.

    Génial sinon au fait ! J’ai partagé sur mon shaarli et utilise l’astuce en ce moment même pour mon nouveau projet.

Leave a comment

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.