Python, manipuler les dictionnaires avec la notation dot

Introduction

Un simple fichier JSON :

resources.json
{
	"id": 307
	,"date": "April 14, 2020"
	,"thumbnail": "python"
	,"langurls":[
		{"lang":"fr" , "file":"python-parsing-des-arguments-argparse-et-getopt.html"}
		,{"lang":"en", "file":"python-parsing-arguments-packages-argparse-getopt.html"}
	]
  ,"sitemap" : {  "title" : "Conception - Python", "url" : "/conception/python" }
}

Avec Javascript, après avoir récupéré le fichier JSON, les propriétés ou attributs sont habituellement directement manipulées avec la notation point (dot notation) :

props = {};

load_prop = async function() {
	const res = await fetch("https://www.sqlpac.ger/referentiel/docs/resources.json");
	if (res.status==200)  response = await res.json(); 
	props = response;
};

show_prop = function() {
	console.log(props.date);
};

load_prop().then(show_prop);
console> April 14, 2020

En basculant en Python, mauvaises nouvelles : la notation dot n’est pas directement possible avec le dictionnaire résultant.

import json

def main():
  with open('resources.json','rb') as f:
    props = json.load(f)
    print(props.date)

if (__name__=="__main__"):
  main()
    print(props.date)
AttributeError: 'dict' object has no attribute 'date'

On doit utiliser la syntaxe usuelle : props["attribute1"]["attribute2"]…

import json

def main():
  with open('resources.json','rb') as f:
    props = json.load(f)
    print(props["date"])

if (__name__=="__main__"):
  main()
April 14, 2020

Python n’est pas Javascript. Habitudes, habitudes, difficile de les éradiquer. Cet exemple traite du chargement de données JSON, le même contexte se produit lors du chargement de données YAML…

Comment utiliser la notation dot avec Python sur un dictionnaire ? Cela peut être réalisé nativement mais quand les dictionnaires sont imbriqués, cela devient fastidieux sans avoir à développer sa propre bibliothèque. Des packages maintenus de la communauté Python existent pour faire le job : Prodict, python-box.

Les méthodes natives

Remplir le dictionnaire d’une classe

Le dictionnaire d’une classe est alimenté par les données :

import json

class clsprops:
	def __init__(self, data):
		self.__dict__ = data
	
def main():
  with open('resources.json','rb') as f:
    props = clsprops(json.load(f))
    print(props.date)

if (__name__=="__main__"):
  main()
April 14, 2020

Mais qu’en est-il des attributs des dictionnaires imbriqués ( { … ,"sitemap": { "url":"…", "title":"…" } … } ) ? Prévisible, les attributs des dictionnaires imbriqués ne peuvent pas être utilisés avec la notation dot.

def main():
  with open('resources.json','rb') as f:
    props = clsprops(json.load(f))
    print(props.sitemap.url)
    print(props.sitemap.url)
AttributeError: 'dict' object has no attribute 'url'

props.sitemap["url"] doit être utilisé.

En utilisant SimpleNamespace

Un code plus élégant en utilisant SimpleNamespace sans avoir à déclarer une classe personnalisée :

import json
from types import SimpleNamespace

def main():
	with open('resources.json','rb') as f: 
		props = SimpleNamespace(** json.load(f))
		print(props.date)

if (__name__=="__main__"):
	main()
April 14, 2020

Même problème que lors de l’utilisation d’une classe personnalisée, les attributs des dictionnaires imbriqués ne sont pas accessibles avec la notation dot :


def main():
	with open('resources.json','rb') as f: 
	props = SimpleNamespace(** json.load(f))
	print(props.sitemap.url)
AttributeError: 'dict' object has no attribute 'url'

Les packages "Dot notation dictionary"

Heureusement, il y a tellement de packages dans la communauté Python qu’il doit bien y en avoir quelques uns qui couvrent ce besoin. Le plus difficile est de trouver ceux qui offrent les meilleures fonctionnalités, avec le moins de dépendances et par dessus tout un package maintenu (regarder les dates des dernières releases, révélateur sur la pérennité et le support du package).

2 packages sélectionnés :

prodict

L’installation de Prodict n’a pas de dépendances :

pip3 search prodict
prodict (0.8.3)  - Prodict = Pro Dictionary with IDE friendly(auto code completion), dot-accessible attributes and more.
pip3 install product
Successfully installed prodict-0.8.3

La mise en œuvre et l’utilisation de la notation dot est alors très facile, les clés non définies retournent None au lieu de l’erreur par défaut dict AttributeError et des clés peuvent être ajoutées dynamiquement :

import json
from prodict import Prodict

def main():
	with open('resources.json','rb') as f: 
		props = Prodict.from_dict(json.load(f))
		print(props.sitemap.url)
    
    if (props.sitemap.url2==None):
      props.sitemep.url2="/conception/python-3.8"
      print(props.sitemep.url2)

if (__name__=="__main__"):
	main()
/conception/python
/conception/python-3.8

Juste un inconvénient, quand les dictionnaires imbriqués sont définis dans une liste et uniquement dans ce cas :

"langurls":[
		{"lang":"fr" , "file":"python-parsing-des-arguments-argparse-et-getopt.html"}
		,{"lang":"en", "file":"python-parsing-arguments-packages-argparse-getopt.html"}
]

un nouvel objet Prodict est nécessaire pour utiliser la notation dot, sinon l’erreur AttributeError est levée :

import json
from prodict import Prodict

def main():
	with open('resources.json','rb') as f: 
    props = Prodict.from_dict(json.load(f))
		
    # print(props.langurls[0].lang)  Not possible with Prodict, Attribute Error raised
    print(Prodict.from_dict(props.langurls[0]).lang)

if (__name__=="__main__"):
	main()
fr

L’objet Prodict est un dictionnaire, aussi il peut être utilisé comme un dictionnaire et comparé à des dictionnaires classiques, aucune translation nécessaire :

		with open('results.json','w') as r:
			json.dump(props, r, indent=4, ensure_ascii=False)
…, 
"sitemap": {
        "title": "Conception - Python",
        "url": "/conception/python",
        "url2": "/conception/python-3.8"
}

python-box

Python-box a des dépendances (les packages des parsers ruamel.yaml et toml) :

pip3 search python-box
python-box (4.2.2)                       - Advanced Python dictionaries with dot notation access
pip3 install python-box
Successfully installed python-box-4.2.2 ruamel.yaml-0.16.10 ruamel.yaml.clib-0.2.0 toml-0.10.0

Pour utiliser python-box

import json

from box import Box

def main():
	with open('resources.json','rb') as f: 
		props = Box(json.load(f))
		print(props.sitemap.url)

if (__name__=="__main__"):
	main()
/conception/python

À propos de l’inconvénient noté avec Prodict, les dictionnaires définis dans une liste sont bien gérés par python-box :

import json

from box import Box

def main():
	with open('resources.json','rb') as f: 
		props = Box(json.load(f))
		print(props.sitemap.url)
		print(props.langurls[0].lang)

if (__name__=="__main__"):
	main()

/conception/python
fr

Les objets Box peuvent être comparés aux dictionnaires natifs. La méthode to_dict traduit un objet Box en dictionnaire natif, mais les quelques tests réalisés pour des actions usuelles (json.dump par exemple) n’ont pas nécessité d’utiliser la méthode to_dict.

L’inconvénient :

  • L’interrogation de clés non définis lève une erreur(BoxKeyError)

Conclusion

De l’opinion de l’auteur de cet article, le package Prodict est le plus adapté : pas de dépendances, très léger en code et surtout, on peut gérer les clés non existantes comme on le ferait en Javascript sans qu’une exception soit levée, c’est exactement la fonctionnalité supplémentaire que l’on cherchait en plus de la notation dot :

if (props.sitemap.url2 == undefined) { // Javascript code }
if (props.sitemap.url2 == None) :
  # Python code

Dans le cas où des dictionnaire sont définis dans une liste, des contournements sont possibles.