Pablo Galindo | 2f172d8 | 2020-06-01 00:41:14 +0100 | [diff] [blame] | 1 | :mod:`graphlib` --- Functionality to operate with graph-like structures |
| 2 | ========================================================================= |
| 3 | |
| 4 | .. module:: graphlib |
| 5 | :synopsis: Functionality to operate with graph-like structures |
| 6 | |
| 7 | |
| 8 | **Source code:** :source:`Lib/graphlib.py` |
| 9 | |
| 10 | .. testsetup:: default |
| 11 | |
| 12 | import graphlib |
| 13 | from graphlib import * |
| 14 | |
| 15 | -------------- |
| 16 | |
| 17 | |
| 18 | .. class:: TopologicalSorter(graph=None) |
| 19 | |
| 20 | Provides functionality to topologically sort a graph of hashable nodes. |
| 21 | |
| 22 | A topological order is a linear ordering of the vertices in a graph such that |
| 23 | for every directed edge u -> v from vertex u to vertex v, vertex u comes |
| 24 | before vertex v in the ordering. For instance, the vertices of the graph may |
| 25 | represent tasks to be performed, and the edges may represent constraints that |
| 26 | one task must be performed before another; in this example, a topological |
| 27 | ordering is just a valid sequence for the tasks. A complete topological |
| 28 | ordering is possible if and only if the graph has no directed cycles, that |
| 29 | is, if it is a directed acyclic graph. |
| 30 | |
| 31 | If the optional *graph* argument is provided it must be a dictionary |
| 32 | representing a directed acyclic graph where the keys are nodes and the values |
| 33 | are iterables of all predecessors of that node in the graph (the nodes that |
| 34 | have edges that point to the value in the key). Additional nodes can be added |
| 35 | to the graph using the :meth:`~TopologicalSorter.add` method. |
| 36 | |
| 37 | In the general case, the steps required to perform the sorting of a given |
| 38 | graph are as follows: |
| 39 | |
| 40 | * Create an instance of the :class:`TopologicalSorter` with an optional |
| 41 | initial graph. |
| 42 | * Add additional nodes to the graph. |
| 43 | * Call :meth:`~TopologicalSorter.prepare` on the graph. |
| 44 | * While :meth:`~TopologicalSorter.is_active` is ``True``, iterate over |
| 45 | the nodes returned by :meth:`~TopologicalSorter.get_ready` and |
| 46 | process them. Call :meth:`~TopologicalSorter.done` on each node as it |
| 47 | finishes processing. |
| 48 | |
| 49 | In case just an immediate sorting of the nodes in the graph is required and |
| 50 | no parallelism is involved, the convenience method |
| 51 | :meth:`TopologicalSorter.static_order` can be used directly: |
| 52 | |
| 53 | .. doctest:: |
| 54 | |
| 55 | >>> graph = {"D": {"B", "C"}, "C": {"A"}, "B": {"A"}} |
| 56 | >>> ts = TopologicalSorter(graph) |
| 57 | >>> tuple(ts.static_order()) |
| 58 | ('A', 'C', 'B', 'D') |
| 59 | |
| 60 | The class is designed to easily support parallel processing of the nodes as |
| 61 | they become ready. For instance:: |
| 62 | |
| 63 | topological_sorter = TopologicalSorter() |
| 64 | |
| 65 | # Add nodes to 'topological_sorter'... |
| 66 | |
| 67 | topological_sorter.prepare() |
| 68 | while topological_sorter.is_active(): |
| 69 | for node in topological_sorter.get_ready(): |
| 70 | # Worker threads or processes take nodes to work on off the |
| 71 | # 'task_queue' queue. |
| 72 | task_queue.put(node) |
| 73 | |
| 74 | # When the work for a node is done, workers put the node in |
| 75 | # 'finalized_tasks_queue' so we can get more nodes to work on. |
| 76 | # The definition of 'is_active()' guarantees that, at this point, at |
| 77 | # least one node has been placed on 'task_queue' that hasn't yet |
| 78 | # been passed to 'done()', so this blocking 'get()' must (eventually) |
| 79 | # succeed. After calling 'done()', we loop back to call 'get_ready()' |
| 80 | # again, so put newly freed nodes on 'task_queue' as soon as |
| 81 | # logically possible. |
| 82 | node = finalized_tasks_queue.get() |
| 83 | topological_sorter.done(node) |
| 84 | |
| 85 | .. method:: add(node, *predecessors) |
| 86 | |
| 87 | Add a new node and its predecessors to the graph. Both the *node* and all |
| 88 | elements in *predecessors* must be hashable. |
| 89 | |
| 90 | If called multiple times with the same node argument, the set of |
| 91 | dependencies will be the union of all dependencies passed in. |
| 92 | |
| 93 | It is possible to add a node with no dependencies (*predecessors* is not |
| 94 | provided) or to provide a dependency twice. If a node that has not been |
| 95 | provided before is included among *predecessors* it will be automatically |
| 96 | added to the graph with no predecessors of its own. |
| 97 | |
| 98 | Raises :exc:`ValueError` if called after :meth:`~TopologicalSorter.prepare`. |
| 99 | |
| 100 | .. method:: prepare() |
| 101 | |
| 102 | Mark the graph as finished and check for cycles in the graph. If any cycle |
| 103 | is detected, :exc:`CycleError` will be raised, but |
| 104 | :meth:`~TopologicalSorter.get_ready` can still be used to obtain as many |
| 105 | nodes as possible until cycles block more progress. After a call to this |
| 106 | function, the graph cannot be modified, and therefore no more nodes can be |
| 107 | added using :meth:`~TopologicalSorter.add`. |
| 108 | |
| 109 | .. method:: is_active() |
| 110 | |
| 111 | Returns ``True`` if more progress can be made and ``False`` otherwise. |
| 112 | Progress can be made if cycles do not block the resolution and either |
| 113 | there are still nodes ready that haven't yet been returned by |
| 114 | :meth:`TopologicalSorter.get_ready` or the number of nodes marked |
| 115 | :meth:`TopologicalSorter.done` is less than the number that have been |
| 116 | returned by :meth:`TopologicalSorter.get_ready`. |
| 117 | |
| 118 | The :meth:`~TopologicalSorter.__bool__` method of this class defers to |
| 119 | this function, so instead of:: |
| 120 | |
| 121 | if ts.is_active(): |
| 122 | ... |
| 123 | |
Mario Šaško | 8516060 | 2020-09-03 12:00:10 +0200 | [diff] [blame] | 124 | it is possible to simply do:: |
Pablo Galindo | 2f172d8 | 2020-06-01 00:41:14 +0100 | [diff] [blame] | 125 | |
| 126 | if ts: |
| 127 | ... |
| 128 | |
| 129 | Raises :exc:`ValueError` if called without calling |
| 130 | :meth:`~TopologicalSorter.prepare` previously. |
| 131 | |
| 132 | .. method:: done(*nodes) |
| 133 | |
| 134 | Marks a set of nodes returned by :meth:`TopologicalSorter.get_ready` as |
| 135 | processed, unblocking any successor of each node in *nodes* for being |
| 136 | returned in the future by a call to :meth:`TopologicalSorter.get_ready`. |
| 137 | |
| 138 | Raises :exc:`ValueError` if any node in *nodes* has already been marked as |
| 139 | processed by a previous call to this method or if a node was not added to |
| 140 | the graph by using :meth:`TopologicalSorter.add`, if called without |
| 141 | calling :meth:`~TopologicalSorter.prepare` or if node has not yet been |
| 142 | returned by :meth:`~TopologicalSorter.get_ready`. |
| 143 | |
| 144 | .. method:: get_ready() |
| 145 | |
| 146 | Returns a ``tuple`` with all the nodes that are ready. Initially it |
| 147 | returns all nodes with no predecessors, and once those are marked as |
| 148 | processed by calling :meth:`TopologicalSorter.done`, further calls will |
| 149 | return all new nodes that have all their predecessors already processed. |
| 150 | Once no more progress can be made, empty tuples are returned. |
| 151 | |
| 152 | Raises :exc:`ValueError` if called without calling |
| 153 | :meth:`~TopologicalSorter.prepare` previously. |
| 154 | |
| 155 | .. method:: static_order() |
| 156 | |
| 157 | Returns an iterable of nodes in a topological order. Using this method |
| 158 | does not require to call :meth:`TopologicalSorter.prepare` or |
| 159 | :meth:`TopologicalSorter.done`. This method is equivalent to:: |
| 160 | |
| 161 | def static_order(self): |
| 162 | self.prepare() |
| 163 | while self.is_active(): |
| 164 | node_group = self.get_ready() |
| 165 | yield from node_group |
| 166 | self.done(*node_group) |
| 167 | |
| 168 | The particular order that is returned may depend on the specific order in |
| 169 | which the items were inserted in the graph. For example: |
| 170 | |
| 171 | .. doctest:: |
| 172 | |
| 173 | >>> ts = TopologicalSorter() |
| 174 | >>> ts.add(3, 2, 1) |
| 175 | >>> ts.add(1, 0) |
| 176 | >>> print([*ts.static_order()]) |
| 177 | [2, 0, 1, 3] |
| 178 | |
| 179 | >>> ts2 = TopologicalSorter() |
| 180 | >>> ts2.add(1, 0) |
| 181 | >>> ts2.add(3, 2, 1) |
| 182 | >>> print([*ts2.static_order()]) |
| 183 | [0, 2, 1, 3] |
| 184 | |
| 185 | This is due to the fact that "0" and "2" are in the same level in the |
| 186 | graph (they would have been returned in the same call to |
| 187 | :meth:`~TopologicalSorter.get_ready`) and the order between them is |
| 188 | determined by the order of insertion. |
| 189 | |
| 190 | |
| 191 | If any cycle is detected, :exc:`CycleError` will be raised. |
| 192 | |
| 193 | .. versionadded:: 3.9 |
| 194 | |
| 195 | |
| 196 | Exceptions |
| 197 | ---------- |
| 198 | The :mod:`graphlib` module defines the following exception classes: |
| 199 | |
| 200 | .. exception:: CycleError |
| 201 | |
| 202 | Subclass of :exc:`ValueError` raised by :meth:`TopologicalSorter.prepare` if cycles exist |
| 203 | in the working graph. If multiple cycles exist, only one undefined choice among them will |
| 204 | be reported and included in the exception. |
| 205 | |
| 206 | The detected cycle can be accessed via the second element in the :attr:`~CycleError.args` |
| 207 | attribute of the exception instance and consists in a list of nodes, such that each node is, |
| 208 | in the graph, an immediate predecessor of the next node in the list. In the reported list, |
| 209 | the first and the last node will be the same, to make it clear that it is cyclic. |