Skip to content

Commit 886086a

Browse files
committed
bpo-17005: Add a topological sort algorithm
1 parent d586ccb commit 886086a

File tree

4 files changed

+172
-1
lines changed

4 files changed

+172
-1
lines changed

Doc/library/functools.rst

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,55 @@ The :mod:`functools` module defines the following functions:
208208
Returning NotImplemented from the underlying comparison function for
209209
unrecognised types is now supported.
210210

211+
.. function:: topsort(pairs)
212+
Topologically sort a list of (parent, child) pairs. Return a sequence of
213+
the elements in dependency order (parent to child order).
214+
215+
If a dependency cycle is found it raises :exc:`CycleError`.
216+
217+
This function can be used directly with a graph given by 2-tuples
218+
specifiying the relationships between elements::
219+
220+
>>> topsort([(1,2), (3,4), (5,6), (1,3), (1,5), (1,6), (2,5)])
221+
[1, 2, 3, 5, 4, 6]
222+
>>> topsort([(1,2), (1,3), (2,4), (3,4), (5,6), (4,5)])
223+
[1, 2, 3, 4, 5, 6]
224+
225+
If we provide a cyclic graph, :exc:`CycleError` will be raised::
226+
227+
>>> topsort([(1,2), (2,3), (3,2)])
228+
Traceback (most recent call last):
229+
CycleError: ([1], {2: 1, 3: 1}, {2: [3], 3: [2]})
230+
231+
This function can be used to implement a simple version of the C3
232+
linearization algorithm used by Python to calculate the Method Resolution
233+
Order (MRO) of a derived class::
234+
235+
from itertools import tee
236+
def c3_linearization(inheritance_seqs):
237+
graph = set()
238+
for seq in inheritance_seqs:
239+
a, b = tee(seq)
240+
next(b, None)
241+
graph.update([*zip(a,b)])
242+
return topsort(graph)
243+
244+
As a test, we can compare with the MRO of a simple diamond inheritance::
245+
246+
>> class A: pass
247+
>> class B(A): pass
248+
>> class C(A): pass
249+
>> class D(B, C): pass
250+
251+
>>D.__mro__
252+
(__main__.D, __main__.B, __main__.C, __main__.A, object)
253+
254+
>>c3_linearization([(D, B, A, object), (D, C, A, object)])
255+
['__main__.D', '__main__.B', '__main__.C', '__main__.A', 'object']
256+
257+
.. versionadded:: 3.8
258+
259+
211260
.. function:: partial(func, *args, **keywords)
212261

213262
Return a new :ref:`partial object<partial-objects>` which when called

Lib/functools.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
'partialmethod', 'singledispatch', 'singledispatchmethod']
1515

1616
from abc import get_cache_token
17-
from collections import namedtuple
17+
from collections import namedtuple, defaultdict
1818
# import types, weakref # Deferred to single_dispatch()
1919
from reprlib import recursive_repr
2020
from _thread import RLock
@@ -193,6 +193,65 @@ def total_ordering(cls):
193193
return cls
194194

195195

196+
################################################################################
197+
### topological sort
198+
################################################################################
199+
200+
class CycleError(Exception):
201+
"""Cycle Error"""
202+
pass
203+
204+
205+
def topsort(pairlist):
206+
"""Topologically sort a list of (parent, child) pairs.
207+
Return a list of the elements in dependency order (parent to child order).
208+
>>> topsort([(1,2), (3,4), (5,6), (1,3), (1,5), (1,6), (2,5)])
209+
[1, 2, 3, 5, 4, 6]
210+
>>> topsort([(1,2), (1,3), (2,4), (3,4), (5,6), (4,5)])
211+
[1, 2, 3, 4, 5, 6]
212+
>>> topsort([(1,2), (2,3), (3,2)])
213+
Traceback (most recent call last):
214+
CycleError: ([1], {2: 1, 3: 1}, {2: [3], 3: [2]})
215+
"""
216+
# This implementation is based on Kahn's algorithm.
217+
218+
# Each element is the number of predecessors
219+
num_parents = defaultdict(int)
220+
# Each element is the number of successors
221+
children = defaultdict(dict)
222+
for parent, child in pairlist:
223+
# Make sure every element is a key in num_parents.
224+
if parent not in num_parents:
225+
num_parents[parent] = 0
226+
# Increment the number of parents of this child
227+
num_parents[child] += 1
228+
229+
# Add the parent to the collection of parents
230+
# relaying of the ordered nature of dictionaries
231+
# to preserve stability.
232+
children[parent][child] = None
233+
234+
# Start with all elements that do not have a parent
235+
result = [elem for (elem, parents) in num_parents.items() if not parents]
236+
237+
# For everything in result, reduce the order of the parent count of its children.
238+
for parent in result:
239+
del num_parents[parent]
240+
if parent not in children:
241+
continue
242+
for child in children[parent]:
243+
num_parents[child] -= 1
244+
if num_parents[child] == 0:
245+
result.append(child)
246+
# Remove parent to better error reporting if a cycle is found.
247+
del children[parent]
248+
249+
if num_parents:
250+
# If we still have elements with childs, there is a cycle.
251+
raise CycleError(result, dict(num_parents), dict(children))
252+
return result
253+
254+
196255
################################################################################
197256
### cmp_to_key() function converter
198257
################################################################################

Lib/test/test_functools.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,6 +1142,67 @@ def __eq__(self, other):
11421142
return self.value == other.value
11431143

11441144

1145+
class TestTopologicalStort(unittest.TestCase):
1146+
1147+
def test_simple_linear_graph(self):
1148+
graph = [("A", "B"), ("B", "C")]
1149+
expected = ["A", "B", "C"]
1150+
self.assertEqual(functools.topsort(graph), expected)
1151+
1152+
def test_simple_graph_with_fork(self):
1153+
graph = [("A", "C"), ("A", "B"), ("B", "C")]
1154+
expected = ["A", "B", "C"]
1155+
self.assertEqual(functools.topsort(graph), expected)
1156+
1157+
def test_graph_with_merge(self):
1158+
graph = [("A", "B"), ("A", "D"), ("D", "C"), ("C", "B")]
1159+
expected = ["A", "D", "C", "B"]
1160+
self.assertEqual(functools.topsort(graph), expected)
1161+
1162+
def test_graph_order(self):
1163+
graph = [("A", "B"), ("C", "D"), ("E", "F")]
1164+
expected = ["A", "C", "E", "B", "D", "F"]
1165+
self.assertEqual(functools.topsort(graph), expected)
1166+
1167+
def test_complex_cases(self):
1168+
test_cases= [
1169+
[('B', 'C'), ('M', 'N'), ('B', 'J'), ('L', 'N'), ('E', 'L'), ('F', 'G')],
1170+
[('E', 'K'), ('A', 'M'), ('A', 'D'), ('J', 'N'), ('I', 'L'), ('N', 'O')],
1171+
[('G', 'N'), ('C', 'L'), ('D', 'J'), ('C', 'J'), ('D', 'H'), ('E', 'L')],
1172+
[('B', 'K'), ('D', 'M'), ('E', 'J'), ('C', 'H'), ('F', 'G'), ('A', 'D')],
1173+
[('D', 'N'), ('K', 'O'), ('D', 'I'), ('D', 'H'), ('A', 'C'), ('F', 'L')],
1174+
[('I', 'J'), ('D', 'H'), ('A', 'G'), ('A', 'D'), ('A', 'I'), ('C', 'E')],
1175+
[('C', 'N'), ('A', 'H'), ('F', 'I'), ('K', 'N'), ('D', 'G'), ('A', 'F')],
1176+
[('I', 'N'), ('A', 'C'), ('B', 'O'), ('G', 'J'), ('F', 'K'), ('C', 'L')],
1177+
]
1178+
1179+
expected_results = [
1180+
['B', 'M', 'E', 'F', 'C', 'J', 'L', 'G', 'N'],
1181+
['E', 'A', 'J', 'I', 'K', 'M', 'D', 'N', 'L', 'O'],
1182+
['G', 'C', 'D', 'E', 'N', 'J', 'H', 'L'],
1183+
['B', 'E', 'C', 'F', 'A', 'K', 'J', 'H', 'G', 'D', 'M'],
1184+
['D', 'K', 'A', 'F', 'N', 'I', 'H', 'O', 'C', 'L'],
1185+
['A', 'C', 'G', 'D', 'I', 'E', 'H', 'J'],
1186+
['C', 'A', 'K', 'D', 'H', 'F', 'N', 'G', 'I'],
1187+
['I', 'A', 'B', 'G', 'F', 'N', 'C', 'O', 'J', 'K', 'L'],
1188+
]
1189+
1190+
for graph, expected in zip(test_cases, expected_results):
1191+
self.assertEqual(functools.topsort(graph), expected)
1192+
1193+
def test_simple_circle(self):
1194+
with self.assertRaises(functools.CycleError):
1195+
functools.topsort([("A", "B"), ("B", "A")])
1196+
1197+
def test_circle(self):
1198+
with self.assertRaises(functools.CycleError):
1199+
functools.topsort([("A", "B"), ("B", "C"), ("C", "A")])
1200+
1201+
def test_complex_cycle(self):
1202+
with self.assertRaises(functools.CycleError):
1203+
functools.topsort([("A",'B'), ("B","C"), ("Z","B"), ("E","Z"), ("C","E")])
1204+
1205+
11451206
class TestLRU:
11461207

11471208
def test_lru(self):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add a topological sort algorithm to the standard library in the
2+
:mod:`functools` module. Patch by Pablo Galindo.

0 commit comments

Comments
 (0)