|
8 | 8 | .. moduleauthor:: Raymond Hettinger <[email protected]>
|
9 | 9 | .. moduleauthor:: Nick Coghlan <[email protected]>
|
10 | 10 | .. moduleauthor:: Łukasz Langa <[email protected]>
|
| 11 | +.. moduleauthor:: Pablo Galindo <[email protected]> |
11 | 12 | .. sectionauthor:: Peter Harris <[email protected]>
|
12 | 13 |
|
13 | 14 | **Source code:** :source:`Lib/functools.py`
|
14 | 15 |
|
| 16 | +.. testsetup:: default |
| 17 | + |
| 18 | + import functools |
| 19 | + from functools import * |
| 20 | + |
15 | 21 | --------------
|
16 | 22 |
|
17 | 23 | The :mod:`functools` module is for higher-order functions: functions that act on
|
@@ -512,6 +518,192 @@ The :mod:`functools` module defines the following functions:
|
512 | 518 | .. versionadded:: 3.8
|
513 | 519 |
|
514 | 520 |
|
| 521 | +.. class:: TopologicalSorter(graph=None) |
| 522 | + |
| 523 | + Provides functionality to topologically sort a graph of hashable nodes. |
| 524 | + |
| 525 | + A topological order is a linear ordering of the vertices in a graph such that for |
| 526 | + every directed edge u -> v from vertex u to vertex v, vertex u comes before vertex |
| 527 | + v in the ordering. For instance, the vertices of the graph may represent tasks to |
| 528 | + be performed, and the edges may represent constraints that one task must be |
| 529 | + performed before another; in this example, a topological ordering is just a valid |
| 530 | + sequence for the tasks. A complete topological ordering is possible if and only if |
| 531 | + the graph has no directed cycles, that is, if it is a directed acyclic graph. |
| 532 | + |
| 533 | + If the optional *graph* argument is provided it must be a dictionary representing |
| 534 | + a directed acyclic graph where the keys are nodes and the values are iterables of |
| 535 | + all predecessors of that node in the graph (the nodes that have edges that point |
| 536 | + to the value in the key). Additional nodes can be added to the graph using the |
| 537 | + :meth:`~TopologicalSorter.add` method. |
| 538 | + |
| 539 | + In the general case, the steps required to perform the sorting of a given graph |
| 540 | + are as follows: |
| 541 | + |
| 542 | + * Create an instance of the :class:`TopologicalSorter` with an optional initial graph. |
| 543 | + * Add additional nodes to the graph. |
| 544 | + * Call :meth:`~TopologicalSorter.prepare` on the graph. |
| 545 | + * While :meth:`~TopologicalSorter.is_active` is ``True``, iterate over the |
| 546 | + nodes returned by :meth:`~TopologicalSorter.get_ready` and process them. |
| 547 | + Call :meth:`~TopologicalSorter.done` on each node as it finishes processing. |
| 548 | + |
| 549 | + In case just an immediate sorting of the nodes in the graph is required and |
| 550 | + no parallelism is involved, the convenience method :meth:`TopologicalSorter.static_order` |
| 551 | + can be used directly. For example, this method can be used to implement a simple |
| 552 | + version of the C3 linearization algorithm used by Python to calculate the Method |
| 553 | + Resolution Order (MRO) of a derived class: |
| 554 | + |
| 555 | + .. doctest:: |
| 556 | + |
| 557 | + >>> class A: pass |
| 558 | + >>> class B(A): pass |
| 559 | + >>> class C(A): pass |
| 560 | + >>> class D(B, C): pass |
| 561 | + |
| 562 | + >>> D.__mro__ |
| 563 | + (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>) |
| 564 | + |
| 565 | + >>> graph = {D: {B, C}, C: {A}, B: {A}, A:{object}} |
| 566 | + >>> ts = TopologicalSorter(graph) |
| 567 | + >>> topological_order = tuple(ts.static_order()) |
| 568 | + >>> tuple(reversed(topological_order)) |
| 569 | + (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>) |
| 570 | + |
| 571 | + The class is designed to easily support parallel processing of the nodes as they |
| 572 | + become ready. For instance:: |
| 573 | + |
| 574 | + topological_sorter = TopologicalSorter() |
| 575 | + |
| 576 | + # Add nodes to 'topological_sorter'... |
| 577 | + |
| 578 | + topological_sorter.prepare() |
| 579 | + while topological_sorter.is_active(): |
| 580 | + for node in topological_sorter.get_ready(): |
| 581 | + # Worker threads or processes take nodes to work on off the |
| 582 | + # 'task_queue' queue. |
| 583 | + task_queue.put(node) |
| 584 | + |
| 585 | + # When the work for a node is done, workers put the node in |
| 586 | + # 'finalized_tasks_queue' so we can get more nodes to work on. |
| 587 | + # The definition of 'is_active()' guarantees that, at this point, at |
| 588 | + # least one node has been placed on 'task_queue' that hasn't yet |
| 589 | + # been passed to 'done()', so this blocking 'get()' must (eventually) |
| 590 | + # succeed. After calling 'done()', we loop back to call 'get_ready()' |
| 591 | + # again, so put newly freed nodes on 'task_queue' as soon as |
| 592 | + # logically possible. |
| 593 | + node = finalized_tasks_queue.get() |
| 594 | + topological_sorter.done(node) |
| 595 | + |
| 596 | + .. method:: add(node, *predecessors) |
| 597 | + |
| 598 | + Add a new node and its predecessors to the graph. Both the *node* and |
| 599 | + all elements in *predecessors* must be hashable. |
| 600 | + |
| 601 | + If called multiple times with the same node argument, the set of dependencies |
| 602 | + will be the union of all dependencies passed in. |
| 603 | + |
| 604 | + It is possible to add a node with no dependencies (*predecessors* is not |
| 605 | + provided) or to provide a dependency twice. If a node that has not been |
| 606 | + provided before is included among *predecessors* it will be automatically added |
| 607 | + to the graph with no predecessors of its own. |
| 608 | + |
| 609 | + Raises :exc:`ValueError` if called after :meth:`~TopologicalSorter.prepare`. |
| 610 | + |
| 611 | + .. method:: prepare() |
| 612 | + |
| 613 | + Mark the graph as finished and check for cycles in the graph. If any cycle is |
| 614 | + detected, :exc:`CycleError` will be raised, but |
| 615 | + :meth:`~TopologicalSorter.get_ready` can still be used to obtain as many nodes |
| 616 | + as possible until cycles block more progress. After a call to this function, |
| 617 | + the graph cannot be modified, and therefore no more nodes can be added using |
| 618 | + :meth:`~TopologicalSorter.add`. |
| 619 | + |
| 620 | + .. method:: is_active() |
| 621 | + |
| 622 | + Returns ``True`` if more progress can be made and ``False`` otherwise. Progress |
| 623 | + can be made if cycles do not block the resolution and either there are still |
| 624 | + nodes ready that haven't yet been returned by |
| 625 | + :meth:`TopologicalSorter.get_ready` or the number of nodes marked |
| 626 | + :meth:`TopologicalSorter.done` is less than the number that have been returned |
| 627 | + by :meth:`TopologicalSorter.get_ready`. |
| 628 | + |
| 629 | + The :meth:`~TopologicalSorter.__bool__` method of this class defers to this |
| 630 | + function, so instead of:: |
| 631 | + |
| 632 | + if ts.is_active(): |
| 633 | + ... |
| 634 | + |
| 635 | + if possible to simply do:: |
| 636 | + |
| 637 | + if ts: |
| 638 | + ... |
| 639 | + |
| 640 | + Raises :exc:`ValueError` if called without calling :meth:`~TopologicalSorter.prepare` |
| 641 | + previously. |
| 642 | + |
| 643 | + .. method:: done(*nodes) |
| 644 | + |
| 645 | + Marks a set of nodes returned by :meth:`TopologicalSorter.get_ready` as |
| 646 | + processed, unblocking any successor of each node in *nodes* for being returned |
| 647 | + in the future by a call to :meth:`TopologicalSorter.get_ready`. |
| 648 | + |
| 649 | + Raises :exc:`ValueError` if any node in *nodes* has already been marked as |
| 650 | + processed by a previous call to this method or if a node was not added to the |
| 651 | + graph by using :meth:`TopologicalSorter.add`, if called without calling |
| 652 | + :meth:`~TopologicalSorter.prepare` or if node has not yet been returned by |
| 653 | + :meth:`~TopologicalSorter.get_ready`. |
| 654 | + |
| 655 | + .. method:: get_ready() |
| 656 | + |
| 657 | + Returns a ``tuple`` with all the nodes that are ready. Initially it returns all |
| 658 | + nodes with no predecessors, and once those are marked as processed by calling |
| 659 | + :meth:`TopologicalSorter.done`, further calls will return all new nodes that |
| 660 | + have all their predecessors already processed. Once no more progress can be |
| 661 | + made, empty tuples are returned. |
| 662 | + made. |
| 663 | + |
| 664 | + Raises :exc:`ValueError` if called without calling |
| 665 | + :meth:`~TopologicalSorter.prepare` previously. |
| 666 | + |
| 667 | + .. method:: static_order() |
| 668 | + |
| 669 | + Returns an iterable of nodes in a topological order. Using this method |
| 670 | + does not require to call :meth:`TopologicalSorter.prepare` or |
| 671 | + :meth:`TopologicalSorter.done`. This method is equivalent to:: |
| 672 | + |
| 673 | + def static_order(self): |
| 674 | + self.prepare() |
| 675 | + while self.is_active(): |
| 676 | + node_group = self.get_ready() |
| 677 | + yield from node_group |
| 678 | + self.done(*node_group) |
| 679 | + |
| 680 | + The particular order that is returned may depend on the specific order in |
| 681 | + which the items were inserted in the graph. For example: |
| 682 | + |
| 683 | + .. doctest:: |
| 684 | + |
| 685 | + >>> ts = TopologicalSorter() |
| 686 | + >>> ts.add(3, 2, 1) |
| 687 | + >>> ts.add(1, 0) |
| 688 | + >>> print([*ts.static_order()]) |
| 689 | + [2, 0, 1, 3] |
| 690 | + |
| 691 | + >>> ts2 = TopologicalSorter() |
| 692 | + >>> ts2.add(1, 0) |
| 693 | + >>> ts2.add(3, 2, 1) |
| 694 | + >>> print([*ts2.static_order()]) |
| 695 | + [0, 2, 1, 3] |
| 696 | + |
| 697 | + This is due to the fact that "0" and "2" are in the same level in the graph (they |
| 698 | + would have been returned in the same call to :meth:`~TopologicalSorter.get_ready`) |
| 699 | + and the order between them is determined by the order of insertion. |
| 700 | + |
| 701 | + |
| 702 | + If any cycle is detected, :exc:`CycleError` will be raised. |
| 703 | + |
| 704 | + .. versionadded:: 3.9 |
| 705 | + |
| 706 | + |
515 | 707 | .. function:: update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)
|
516 | 708 |
|
517 | 709 | Update a *wrapper* function to look like the *wrapped* function. The optional
|
@@ -621,3 +813,19 @@ differences. For instance, the :attr:`~definition.__name__` and :attr:`__doc__`
|
621 | 813 | are not created automatically. Also, :class:`partial` objects defined in
|
622 | 814 | classes behave like static methods and do not transform into bound methods
|
623 | 815 | during instance attribute look-up.
|
| 816 | + |
| 817 | + |
| 818 | +Exceptions |
| 819 | +---------- |
| 820 | +The :mod:`functools` module defines the following exception classes: |
| 821 | + |
| 822 | +.. exception:: CycleError |
| 823 | + |
| 824 | + Subclass of :exc:`ValueError` raised by :meth:`TopologicalSorter.prepare` if cycles exist |
| 825 | + in the working graph. If multiple cycles exist, only one undefined choice among them will |
| 826 | + be reported and included in the exception. |
| 827 | + |
| 828 | + The detected cycle can be accessed via the second element in the :attr:`~CycleError.args` |
| 829 | + attribute of the exception instance and consists in a list of nodes, such that each node is, |
| 830 | + in the graph, an immediate predecessor of the next node in the list. In the reported list, |
| 831 | + the first and the last node will be the same, to make it clear that it is cyclic. |
0 commit comments