Blog ENI : Toute la veille numérique !
Accès illimité 24h/24 à tous nos livres & vidéos ! 
Découvrez la Bibliothèque Numérique ENI. Cliquez ici
💥 Les 22 & 23 novembre : Accès 100% GRATUIT
à la Bibliothèque Numérique ENI. Je m'inscris !

Créer et modifier des ontologies en Python

Introduction

Dans ce chapitre, nous verrons comment créer une ontologie de novo en Python, et comment modifier ou enrichir une ontologie déjà existante. La quasi-totalité des objets, attributs et listes d’Owlready vu au chapitre précédent est modifiable. Lorsque la valeur de ceux-ci est modifiée, Owlready met automatiquement à jour les triplets RDF dans le quadstore (cependant, n’oubliez pas d’enregistrer celui-ci s’il est stocké sur le disque, voir chapitre Accéder aux ontologies en Python, section Ontologie volumineuse et cache disque).

Créer une ontologie vide

La fonction get_ontology() permet de créer une ontologie vide à partir de son IRI (il est préférable d’indiquer le séparateur, « # » ou « / », à la fin de l’IRI car Owlready ne pourra pas le deviner puisque l’ontologie est vide !) :

>>> from owlready2 import *  
>>> onto = get_ontology("http://test.org/onto.owl#") 

Notez que, contrairement à ce que nous faisions au chapitre Accéder aux ontologies en Python, nous n’avons pas appelé la méthode load(). Celle-ci permettait de charger l’ontologie sans quoi, l’ontologie reste vide.

Par la suite, lorsque l’on crée des entités OWL ou des triplets RDF, il est important d’indiquer dans quelle ontologie ceux-ci sont placés. En effet, contrairement aux classes Python qui appartiennent au module dans lequel elles sont créées, les entités OWL n’appartiennent pas en propre à une ontologie : une classe peut être définie dans une ontologie A puis enrichie dans une ontologie B, par exemple avec de nouvelles classes parentes.

Owlready utilise la syntaxe ci-dessous pour indiquer l’ontologie qui reçoit les triplets RDF :

with onto:  
   <code Python> 

Tous les triplets RDF créés dans le bloc de code <code Python>...

Créer des classes

Pour créer une classe OWL, il suffit de créer une classe Python qui hérite de Thing. Par exemple, nous pouvons créer les classes Bactérie, Forme et Regroupement ainsi :

>>> with onto:  
...     class Bactérie(Thing): pass  
...     class Forme(Thing): pass  
...     class Regroupement(Thing): pass 

Notez que comme ces classes sont vides (c’est-à-dire qu’elles ne possèdent aucune méthode), nous devons utiliser le mot-clef pass (voir chapitre Le langage Python : adoptez un serpent  !, section Classes).

Afin d’observer ce qui se passe à l’intérieur du quadstore d’Owlready, nous pouvons utiliser la fonction set_log_level() qui modifie le niveau de journalisation. En mettant le niveau au maximum (9), Owlready indique les triplets RDF ajoutés, supprimés ou modifiés dans le quadstore. Voici un exemple :

>>> set_log_level(9)  
>>> with onto:  
...           class ClasseDeTest(Thing): pass  
* Owlready2 * ADD TRIPLE http://test.org/onto.owl#ClasseDeTest 
       http://www.w3.org/1999/02/22-rdf-syntax-ns#type 
       http://www.w3.org/2002/07/owl#Class  
* Owlready2 * ADD TRIPLE http://test.org/onto.owl#ClasseDeTest 
       http://www.w3.org/2000/01/rdf-schema#subClassOf ...

Créer des propriétés

Dans Owlready, les propriétés sont assimilées à des classes, car les propriétés OWL se comportent de manière similaire aux classes (avec notamment le support de l’héritage). De fait, les propriétés OWL sont en réalité des « classes de relations ». Les propriétés sont créées en définissant une classe qui hérite de DataProperty, ObjectProperty ou AnnotationProperty. De plus, les classes FunctionalProperty, InverseFunctionalProperty, TransitiveProperty, SymmetricProperty, AsymmetricProperty, ReflexiveProperty et IrreflexiveProperty peuvent être utilisées en complément (avec un héritage multiple) pour créer une propriété fonctionnelle, inverse fonctionnelle, transitive...

Les attributs de classe domain et range permettent de renseigner le domaine et le range de la propriété, sous la forme d’une liste.

L’exemple suivant crée la propriété fonctionnelle a_pour_forme :

>>> with onto:  
...        class a_pour_forme(ObjectProperty, FunctionalProperty): 
...             domain = [Bactérie]  
...             range  = [Forme] 

Pour les DataProperty, les ranges possibles figurent dans...

Créer des individus

Les individus sont créés comme n’importe quelle instance en Python, en exécutant la classe :

>>> ma_bactérie = Bactérie() 

Owlready attribue automatiquement une IRI à l’individu, en prenant celle de l’ontologie (celle du bloc with...:, ou à défaut celle associée à la classe) et en ajoutant le nom de la classe en minuscule suivi d’un numéro :

>>> ma_bactérie.iri  
'http://test.org/onto.owl#bactérie1' 

Attention à ne pas confondre le nom de la variable Python qui contient l’individu en local (ici ma_bactérie) avec le nom de l’entité (bactérie1). Lorsque l’on accède à l’individu à partir de l’ontologie, il faut utiliser le nom de l’entité et pas le nom de variable, par exemple onto.bactérie1. En revanche, lorsqu’on accède à l’individu directement en Python, il faut utiliser son nom de variable, car il s’agit d’une variable Python, par exemple ma_bactérie.

>>> ma_bactérie is onto.bactérie1  
True 

Il est possible de spécifier le nom de l’individu en le passant comme premier argument au constructeur de la classe :

>>> ma_bactérie = Bactérie("ma_bactérie") 

Il est également...

Modifier des entités : relations et restrictions existentielles

Les relations des individus et les restrictions existentielles des classes peuvent être modifiées comme n’importe quel attribut en Python, par exemple, pour redéfinir les relations d’un individu :

>>> ma_bactérie.gram_positif = True 

S’il s’agit d’une propriété de type ObjectProperty, une nouvelle instance de la classe attendue peut être créée (ici la classe Allongée) :

>>> ma_bactérie.a_pour_forme = Allongée() 

Lorsque la propriété est fonctionnelle, ou qu’une restriction de la classe fait qu’elle peut n’avoir qu’une seule valeur (restriction de cardinalité, de type « max 1 » ou « exactly 1 »), Owlready attend une valeur unique (comme dans les deux lignes ci-dessus). Dans le cas contraire, Owlready recevra une liste de valeurs, cependant, ce ne sont pas des listes Python « ordinaires », comme nous pouvons le constater en regardant la classe de ces listes et en la comparant aux listes de Python :

>>> ma_bactérie.a_pour_regroupement.__class__  
<class 'owlready2.prop.IndividualValueList'>  
 
>>> [].__class__  
<class 'list'> 

Les listes d’Owlready sont des « CallbackList »...

Créer des entités dans un espace de nommage

Par défaut, Owlready crée les entités dans l’espace de nommage de l’ontologie, c’est-à-dire que l’IRI de l’entité commence par celle de l’ontologie. Cependant, il est parfois nécessaire de créer des entités dont l’IRI ne commence pas par celle de l’ontologie. Pour effectuer cela, il faut créer un espace de nommage puis l’utiliser dans le bloc with. Contrairement à ce que nous avions fait au chapitre précédent, section Espaces de nommage, l’espace de nommage doit ici être créé à partir de l’ontologie, afin que les triplets RDF soient ajoutés dans l’ontologie correspondante. L’exemple suivant définit dans l’ontologie onto une classe avec l’IRI http://purl.obolibrary.org/obo/OBOBactérie :

>>> obo = onto.get_namespace("http://purl.obolibrary.org/obo/") 
>>> with obo:  
...     class OBOBactérie(Thing): pass  
>>> OBOBactérie.iri  
'http://purl.obolibrary.org/obo/OBOBactérie' 

La même méthode peut être utilisée pour les individus :

>>> with obo:  
...     ma_bactérie = OBOBactérie("ma_bactérie")  
>>> ma_bactérie.iri...

Renommer des entités

Les attributs name et iri des entités peuvent être modifiés pour changer l’IRI de l’entité (une opération parfois connue sous le nom de refactoring). Modifier name permet de changer le nom de l’entité tout en la gardant dans le même espace de nommage, tandis que modifier iri permet de changer à la fois l’espace de nommage et le nom.

>>> ma_bactérie.iri = "http://test.org/autre_onto.owl#bactérie1" 

Attention, renommer l’entité change son nom dans l’ontologie, mais pas le nom des variables Python ! Après exécution de la ligne ci-dessus, l’individu est toujours disponible dans la variable Python ma_bactérie, en revanche il n’est plus disponible comme onto.ma_bactérie, mais peut être récupéré en créant l’espace de nommage correspondant :

>>> get_namespace("http://test.org/autre_onto.owl").bactérie1 

Attention également : renommer une entité ne la déplace pas dans une autre ontologie.

Définitions multiples et déclarations anticipées

Lorsque plusieurs entités sont définies avec la même IRI, Owlready ne crée pas une nouvelle entité, mais retourne celle qui existe déjà. Celle-ci est mise à jour si nécessaire, par exemple avec les relations, la classe parente (pour les individus) et/ou les héritages (pour les classes). Dans l’exemple ci-dessous, un seul individu de la classe Bactérie est créé, car bactérie_a et bactérie_b ont le même IRI. Cependant, la seconde création ajoute la relation gram_positif avec pour valeur False.

>>> with onto:  
...     bactérie_a = Bactérie("la_bactérie")  
...     bactérie_b = Bactérie("la_bactérie", gram_positif = False) 
>>> bactérie_a is bactérie_b  
True 

De la sorte, il est possible de réaliser des déclarations anticipées (forward declaration) pour les classes ou les individus. Dans l’exemple suivant, la classe Bactérie est d’abord créée, puis celle-ci est utilisée dans le domaine de la propriété a_pour_forme. La définition de la classe Bactérie est ensuite reprise, pour lui ajouter la restriction existentielle « avoir au moins une forme »....

Supprimer des entités

La fonction globale destroy_entity() permet de supprimer définitivement une entité (classe, individu, propriété...).

>>> bacterie_temporaire = Bactérie()  
>>> destroy_entity(bacterie_temporaire) 

Supprimer une ontologie

La méthode destroy() permet de supprimer définitivement une ontologie. Cette méthode permet de libérer la place occupée par l’ontologie dans le quadstore.

>>> onto_temp = get_ontology("http://tmp.org/onto.owl")
>>> onto_temp.destroy() 

Enregistrer une ontologie

La méthode save() permet d’enregistrer une ontologie.

onto.save(fichier) 

où fichier peut être un nom de fichier ou un objet fichier Python. Si le fichier n’est pas précisé, Owlready enregistre l’ontologie dans le répertoire correspondant, selon la valeur de la variable onto_path (voir chapitre précédent, section Répertoire local d’ontologies).

L’attribut optionnel format permet de préciser le format de l’ontologie. Actuellement, Owlready supporte deux formats de fichier en écriture : RDF/XML (format = "rdfxml") et NTriples (format = "ntriples"). Par exemple pour enregistrer en NTriples :

>>> onto.save("fichier.owl", format = "ntriples") 

Importer des ontologies

Pour importer une ontologie, il suffit de l’ajouter dans la liste imported_ontologies de l’ontologie de destination :

>>> onto.imported_ontologies.append(une_autre_onto) 

Pour supprimer l’importation, il s’agit de retirer l’ontologie de la liste avec remove() :

 >>> onto.imported_ontologies.remove(une_autre_onto) 

Synchronisation

Lorsqu’un programme multithread utilise Owlready pour créer ou modifier des ontologies, plusieurs threads peuvent vouloir écrire dans le quadstore au même moment, ce qui peut causer une corruption de la base de données. Notez que, même si chaque thread écrit sur une ontologie différente, le problème reste le même, car les ontologies partagent en réalité le même quadstore. Dans le cas d’un programme multithread, il est donc nécessaire de synchroniser les écritures (à l’inverse, les lectures n’ont pas besoin de l’être).

En particulier, les serveurs web générés avec Flask sont généralement multithread (par défaut, Flask utilise le serveur Werkzeug qui lance le serveur dans ce mode). Lors du chapitre précédent, nous n’avions pas eu de problème de synchronisation, car le serveur ne faisait que lire l’ontologie, mais ne la modifiait pas.

Owlready gère la synchronisation, de la manière suivante : Owlready verrouille automatiquement la base de données en écriture à l’entrée d’un bloc « with ontologie:... » et la déverrouille à la sortie du bloc. De même, les fonctions et méthodes suivantes sont également synchronisées :...

Exemple: peupler une ontologie à partir d’un fichier CSV

Le peuplement d’une ontologie consiste à créer un grand nombre d’individus (ou éventuellement de classes). Ceci est souvent réalisé à partir de ressources externes, comme des fichiers tableurs (LibreOffice Calc, Excel...). Ces fichiers peuvent être enregistrés au format CSV, facilement lisibles en Python.

Le module Python csv permet de lire et d’écrire facilement des fichiers CSV en Python. Il contient deux classes, csv.reader et csv.writer, respectivement pour la lecture et l’écriture. Chacune prend comme paramètre un fichier ouvert. La fonction next() permet d’obtenir la ligne suivante d’un reader.

Dans les deux sections suivantes, nous verrons un exemple de peuplement de l’ontologie des bactéries avec des individus, puis avec des classes.

1. Peuplement par des individus

Le tableau ci-dessous montre un exemple simple de fichier CSV décrivant des individus de la classe Bactérie. Ce fichier est nommé peuplement_individus.csv, voici à quoi il ressemble :

images/p65.png

Lorsqu’une bactérie a plusieurs regroupements, elle peut être décrite sur plusieurs lignes (par exemple la bactérie bact3 sur la figure précédente). Le programme suivant permet de peupler l’ontologie avec des individus créés à partir des données du fichier CSV :

# Fichier peuplement_individus.py  
from owlready2 import *  
import csv  
 
onto = get_ontology("bacterie.owl").load()  
 
onto_individus = get_ontology("http://lesfleursdunormal.fr/static/ \ 
                              _downloads/bacterie_individus.owl") 
onto_individus.imported_ontologies.append(onto)  
 
f = open("peuplement_individus.csv")  
reader = csv.reader(f)  
next(reader)  
 
with onto_individus:  
 for ligne in reader:  
   identifiant, gram_positif, forme, regroupement, nb_colonies = ligne 
   individu = onto.Bactérie(identifiant)  
 
   if gram_positif:  
       if gram_positif == "True": gram_positif = True  ...