Open
Labels
extension-modulesC modules in the Modules dirC modules in the Modules dirperformancePerformance or resource usagePerformance or resource usagestdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytype-featureA feature request or enhancementA feature request or enhancement
Description
Feature or enhancement
Proposal:
Currently, if an object on a heap is modified, it is necessary to run heapq.heapify() to restore the heap property.
As an example, take a priority queue with objects that have a priority propety.
# removing an object from the heap
del heap[heap.index(myobj)]
heapq.heapify(heap)
# changing an object's priority
myobj.priority = new_priority
heapq.heapify(heap)Instead, it would be useful, if an object's index is known, to be able to remove, modify, or replace it in O(log n) time:
# removing an object
heapq.heapremove(heap, heap.index(myobject))
# updating its priority:
myobject.priority = new_priority
i = heap.index(myobject)
heapq.heapremove(heap, i, replacement=myobject) While finding an object in the heap is a an O(n) operation, it need not invoke the all important __lt__ operator
and reordering a heap, when a single object changes, can be done with O(logN) comparisons.
Has this already been discussed elsewhere?
No response given
Links to previous discussion of this feature:
No response
Linked PRs
Metadata
Metadata
Assignees
Labels
extension-modulesC modules in the Modules dirC modules in the Modules dirperformancePerformance or resource usagePerformance or resource usagestdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytype-featureA feature request or enhancementA feature request or enhancement
Activity
rhettinger commentedon Nov 29, 2023
I'm just starting to work through this proposal by looking at other APIs and implementations:
https://docs.oracle.com/javase/8/docs/api/java/util/PriorityQueue.html
https://en.cppreference.com/w/cpp/container/priority_queue
https://pkg.go.dev/container/heap
https://docs.rs/priority-queue/latest/priority_queue/priority_queue/struct.PriorityQueue.html
kristjanvalur commentedon Nov 29, 2023
Interesting. In the PR I mention that im open to suggestions about naming, etc.
I see that golang has
FixandRemove, which are separate functions for doing what my proposed heapremove can do with an optional arg. I guess that is pretty clean.But maybe we can move the discussion to the PR?
rhettinger commentedon Jan 2, 2024
I've worked through a bunch of use cases and looked at several API variants. The most Pythonic API I've found is to accept a value to be removed rather than an index:
The key function is provided to support record types in a priority queue. Consider for example a queue of Task objects defined by:
Then a pending task can be removed with any of these:
A heapremove() implementation has three steps: search, remove, and heapify.
The search step is necessarily linear but you may be able to cut the number of comparisons in half. See: https://stackoverflow.com/questions/2372994/search-an-element-in-a-heap
The remove step can be made O(1) by swapping with the last element and then running a
list.pop()The heapify step is the one than can be significantly optimized by using _siftup() and _siftdown() to replace the linear time heapify() call.
That gives us:
For now, we can skip heap_fix(). Removing a task and pushing its replacement will suffice:
This approach gives the user control over the what gets updated and it doesn't make assumptions about whether a task entry is mutable.
Overall, I think a heapremove() is a good idea. On the one hand, it is just an optimization of what we can already do with the existing API and for small heaps it likely won't be much of an improvement in speed. On the other hand, it presents a new high level way of thinking about the problem, and it abstracts away a lot of thought about the best way to implement it.
kristjanvalur commentedon Jan 2, 2024
The
heapremoveimplementation you make is essentially the same I have in #112550 except:IMHO, it is up to the user to do that. The user may have other means of deriving the index, or may decide at he doesn't want to remove it after finding it. heaps work on lists and there are plenty of ways of finding a list member. We should use decomposable operations.
Interestingly,
list.indexdoesn't have akeyargument. Perhaps this is an opportunity to add alist.findhere?? Then we could simply doheapremove(mylist, mylist.find(key=mykey))Which IMO is a cleaner way to achieve the combined effect you are looking for.siftdownreturn the final index, to be passed tosiftup. It results in the same total amount of comparisons, but it might be considered cleaner to have all the comparisons in the siftup/siftdown methods rather than scattered about in higher level functions. OTOH, my usage might invalidate some of the preconditions in the comments to those functions so it is probably best to do it your way.I also don't quite get your objection to having a
heapfix, since it is faster still thanheapremove()+heappush()and trivially implemented as a subset ofheapremove()Please see #112550 for a fully tested implementation
kristjanvalur commentedon Jan 2, 2024
I've modified #112550 to use the same "if repl_item < found_item" logic as your code.
however,
heapfix()does not have the previous value to work with so it continues to use:the
i > 0comparison is formally not needed but skips a comparison in that case.rhettinger commentedon Jan 2, 2024
Please make an update to incorporate the find logic. A user has no a priiori way of knowing where the index is (it moves as the heap updates) and the existing API is value oriented and not index oriented.
For now, we'll omit headfix. It does save a few cycles but most of the win is already in heapremove. We want as small of an API expansion as possible and doesn't want to make assumptions about an item being mutable (the norm is to use tuples) that drive a user toward a different implementation.
kristjanvalur commentedon Jan 2, 2024
Final appeal, before I change things:
Adding the "find" complicates things:
ValueErroror something.It really feels like finding an index in a list should not be part of this API since it is a completely independent problem
and easily decomposable into more primitive actions without any increased algorithmic complexity. I'd be happier adding
a separate
heapindex()function with the signature you describe, which could have a python-only implementation.If you don't agree, I'll do it the way you like. And I'll drop the
heapfix. Seeing how jealously you protect this module, I won't be trying some of my other PRs, I'll leave them for myheapq_pluspackage :)p.s.
indexOfandcountOfdon't appear to conform to naming conventions.rhettinger commentedon Jan 3, 2024
That comment feels abusive, personal, and unprofessional. It feels like an attempt to bully a reviewer into accepting an API expansion that isn’t in harmony with the module. AFAICT, I’m the only one to have paid any attention to this feature request, to have devoted volunteer research time during the holiday, or made an effort to move this forward.
@gvanrossum
Do you have a preference for the high level value oriented API, a lower level index oriented API, or dropping the proposal altogether? I'm +0 on the first option, -1 on the second, and fine with the third.
In general, the heap structure isn't conducive to removing or updating arbitrary entries in a priority queue. In a module that is all about O(log n) updates, removing or updating entries both stand out as O(n) operations. Whether it is a good idea or not, people do occasionally want to do this. Right now, the best way to remove a task is a
list.remove(task)followed by aheapify()call. This is further complicated because real-world tasks are often objects with multiple fields, so task removal entails searching for a particular field such as a specific callback or task id.By adding a new function, the work load can be reduced a bit. The search step would still be O(n) but the heapify step can be focused to a single siftup or siftdown step.
With a high level value oriented API, removing a task would look like this:
With a low level index oriented API, removing a task would look like this:
With the current API, removing a task looks like this:
gvanrossum commentedon Jan 3, 2024
Raymond. Stop calling me out whenever there's an issue. If you need to appeal to a higher authority, it's the SC, not me.
kristjanvalur commentedon Jan 3, 2024
Sorry that was not my intent, it was intended in good humour, seeing that we know each other from old times. On the contrary I quite admire careful shepherding of this piece of code. My apologies.
Since the entire heap*() api is a low level one, operating explicitly on lists, where even
list[0]is the recommended way of getting a peek at the highest priority object, aremoveoperation which doesn't use the list index operator seems just a bit out of place, and unusually high-level. If the search is combined with it, it forces the operator into adopting a single pattern. He can no longer, for example, do:Since internally, the removal does use the index, having a built-in search operation which just is a preamble to
produce that index seems like an overdesign of the api.
Just to clarify, in case I managed to somehow muddle the point I'm trying to make.
The high level api just sticks a convenient search in front. but without the low level api, the user is forced into
using the particular search pattern built into the high level api.
Anyway, sorry for sounding offensive, still awaiting your decision.
kristjanvalur commentedon Jan 3, 2024
Oh, and since you mentioned that there is no way to know the index of an object,
I do in fact have versions of the heap*() function which accept a callable which can update an index map of the items in the queue for each operation, or even, update an attribute within each entry, roughly:
For cases where you expect a lot of removals/updates, this will remove the O(n) factor from the equation entirely.
But that's a more radical change and one I'm not pushing into the stdlib.
kristjanvalur commentedon Jan 3, 2024
Just occurred to me that I can just provide two functions, heapremove_index being a low-level one easily implementable in C and heapremove() with the high level api with a .py implementation on top. I'll update the pr presently, also removing heapfix
rhettinger commentedon Jan 3, 2024
@gvanrossum
It wasn’t authority I was looking for. It was your instinct for Pythonic API design and a good user experience. If you’re not interested in giving your insights and guidance, sorry to have bugged you.
rhettinger commentedon Jan 3, 2024
No worries. We're good.
I'm going to have to ghost you for week though. Right now, I'm on a cruise ship with bad connectivity when not in port.
kristjanvalur commentedon Jan 4, 2024
I've updated the PR, with a
heapremove_index()implemented in .py and .c. I'm also okay with making it unofficial, adding an underscore, if that is what you like. the search itself, though, does not, IMO, benefit from being implemented in C.Cheers, and enjoy your cruise.
Fix some grammar mistakes in comments in _pylong.py