graphlib — Fonctionnalités pour travailler avec des structures de type graphe

Code source: Lib/graphlib.py


class graphlib.TopologicalSorter(graph=None)

Fournit les fonctionnalités pour trier topologiquement un graphe de nœuds hachables.

L'ordre topologique est un ordre linéaire des sommets d'un graphe afin que pour chaque arête u → v d'un sommet u à un sommet v, cet ordre va placer le sommet u avant le sommet v. Par exemple, les sommets d'un graphe peuvent représenter une tâche à faire et une arête peut représenter la contrainte comme quoi telle tâche doit être réalisée avant telle autre. Dans cet exemple, un ordre topologique est simplement une séquence valide pour ces tâches. Cet ordre n'est possible que si le graphe n'a pas de circuit, c'est-à-dire si c'est un graphe orienté acyclique.

Si l'argument optionnel graph est fourni, cela doit être un dictionnaire représentant un graphe acyclique avec comme clés les nœuds et comme valeurs des itérables sur les prédécesseurs de ces nœuds dans le graphe (les nœuds qui ont des arêtes qui pointent vers la valeur de la clé). Les nœuds s'ajoutent en utilisant la méthode add()

De manière générale, les étapes nécessaires pour trier un graphe donné sont les suivantes :

  • créer une instance de la classe TopologicalSorter avec éventuellement un graphe initial ;

  • ajouter d'autres nœuds au graphe ;

  • appeler prepare() sur le graphe ;

  • tant que is_active() est à True, itérer sur les nœuds renvoyés par get_ready() pour les traiter. Appeler done() sur chaque nœud une fois le traitement terminé.

Si vous souhaitez simplement trier des nœuds du graphe sans parallélisme, la méthode TopologicalSorter.static_order() peut être utilisée directement :

>>> graph = {"D": {"B", "C"}, "C": {"A"}, "B": {"A"}}
>>> ts = TopologicalSorter(graph)
>>> tuple(ts.static_order())
('A', 'C', 'B', 'D')

La classe est conçue pour prendre facilement en charge le traitement en parallèle des nœuds quand ils deviennent disponibles. Par exemple :

topological_sorter = TopologicalSorter()

# Add nodes to 'topological_sorter'...

topological_sorter.prepare()
while topological_sorter.is_active():
    for node in topological_sorter.get_ready():
        # Worker threads or processes take nodes to work on off the
        # 'task_queue' queue.
        task_queue.put(node)

    # When the work for a node is done, workers put the node in
    # 'finalized_tasks_queue' so we can get more nodes to work on.
    # The definition of 'is_active()' guarantees that, at this point, at
    # least one node has been placed on 'task_queue' that hasn't yet
    # been passed to 'done()', so this blocking 'get()' must (eventually)
    # succeed.  After calling 'done()', we loop back to call 'get_ready()'
    # again, so put newly freed nodes on 'task_queue' as soon as
    # logically possible.
    node = finalized_tasks_queue.get()
    topological_sorter.done(node)
add(node, *predecessors)

Ajoute un nouveau nœud et son prédécesseur dans le graphe. Le node ainsi que tous les éléments dans predecessors doivent être hachables.

S'il est appelé plusieurs fois avec le même nœud en tant qu'argument, l'ensemble des dépendances sera l'union de toutes les dépendances qui auront été transmises.

Il est possible d'ajouter un nœud sans dépendance (predecessors n'est pas fourni) ou de fournir une dépendance deux fois. Si un nœud qui n'a jamais été fourni auparavant est inclus dans predecessors il sera automatiquement ajouté au graphe sans prédécesseur lui-même.

Lève une ValueError si appelée après prepare().

prepare()

Indique que le graphe est terminé et vérifie les circuits du graphe. Si un circuit est détecté, une CycleError est levée mais get_ready() peut encore être utilisée pour obtenir autant de nœuds que possible avant que les circuits ne bloquent la progression. Après un appel de cette fonction, le graphe ne peut pas être modifié, et donc aucun nœud ne peut être ajouté avec add().

is_active()

Renvoie True si une progression peut être faite et False dans le cas contraire. La progression est possible si des circuits ne bloquent pas la résolution ou qu'il reste des nœuds prêts qui n'ont pas encore été renvoyés par TopologicalSorter.get_ready() ou que le nombre de nœuds marqués TopologicalSorter.done() est inférieur au nombre qui a été renvoyé par TopologicalSorter.get_ready().

La méthode __bool__() de cette classe renvoie à cette fonction donc au lieu de :

if ts.is_active():
    ...

il est plus simple de faire :

if ts:
    ...

Lève une ValueError si l'appel à prepare() n'a pas été fait au préalable.

done(*nodes)

Marque un ensemble de nœuds renvoyé par TopologicalSorter.get_ready() comme traités, permettant aux successeurs de chaque nœud de nodes d'être renvoyés lors d'un prochain appel à get_ready().

Lève une ValueError si n'importe quel nœud dans nodes a déjà été marqué comme traité par un précédent appel à cette méthode ou si un nœud n'a pas été ajouté au graphe en utilisant TopologicalSorter.add(), si l'appel est fait sans appel à prepare() ou si le nœud n'a pas encore été renvoyé par get_ready().

get_ready()

Renvoie un n-uplet avec tous les nœuds prêts. Renvoie d'abord tous les nœuds sans prédécesseurs, et une fois marqués comme traités avec un appel de TopologicalSorter.done(), les autres appels renvoient tous les nouveaux nœuds dont tous les prédécesseurs sont traités. Une fois que la progression n'est plus possible, des tuples vides sont renvoyés.

Lève une ValueError si l'appel à prepare() n'a pas été fait au préalable.

static_order()

Returns an iterator object which will iterate over nodes in a topological order. When using this method, prepare() and done() should not be called. This method is equivalent to:

def static_order(self):
    self.prepare()
    while self.is_active():
        node_group = self.get_ready()
        yield from node_group
        self.done(*node_group)

Le tri obtenu peut dépendre de l'ordre dans lequel les éléments ont été ajoutés dans le graphe. Par exemple :

>>> ts = TopologicalSorter()
>>> ts.add(3, 2, 1)
>>> ts.add(1, 0)
>>> print([*ts.static_order()])
[2, 0, 1, 3]

>>> ts2 = TopologicalSorter()
>>> ts2.add(1, 0)
>>> ts2.add(3, 2, 1)
>>> print([*ts2.static_order()])
[0, 2, 1, 3]

Ceci est dû au fait que "0" et "2" sont au même niveau dans le graphe (ils auraient été renvoyés dans le même appel à get_ready()) et l'ordre entre eux est déterminé par l'ordre lors de l'insertion.

Si un circuit est détecté alors une CycleError est levée.

Nouveau dans la version 3.9.

Exceptions

Le module graphlib définit les classes d'exceptions suivantes :

exception graphlib.CycleError

Une classe héritant de ValueError levée par TopologicalSorter.prepare() si un circuit existe dans le graphe courant. Si plusieurs circuits existent, un seul est inclus dans l'exception.

On accède au circuit détecté via le second élément de l'attribut args de l'instance de l'exception. Cet attribut est une liste de nœuds où chaque nœud est, dans le graphe, un prédécesseur immédiat du nœud suivant de la liste. Dans la liste renvoyée, le premier et le dernier nœud sont les mêmes afin de bien indiquer que c'est un circuit.