I couldn’t come to final decision on gh-125028: Prohibit placeholders in partial keywords by dg-pb · Pull Request #126062 · python/cpython · GitHub
Thus, I explored possibilities and finally convinced myself that it is best to prohibit keyword Placeholders
as there is a real possibility for 1 more extension.
I am not planning on doing it any time soon, but thought it would be worth sharing findings for future reference and potential feedback.
1. Let’s define current functools.partial
as V1.
It allows Placeholder
s in positional arguments.
2. Version V2 - kwpartial
. Allowing Placeholder
in keyword arguments.
Motivation.
Say there is an external function, which either has:
a) keyword-only
argument of interest
b) positional-or-keyword
, but very far down, say 10th argument. In this case it is impossible not to overwrite defaults in order to bring it to the front.
from itertools import starmap
_ = functools.Placeholder
def foo(a=1, b=1, c=1, *, d=1):
return a + b + c - d
p = kwpartial(foo, _, d=_)
list_of_args = [(1, 2), (2, 3), (3, 4)]
print(list(starmap(p, list_of_args))) # ???
# [1, 1, 1, 1] # V2 variation 1. signature(p) == foo(a, d, b=1, c=1)
# [3, 3, 3, 3] # V2 variation 2. signature(p) == foo(d, a, b=1, c=1)
So here is the first dilemma.
“Variation 1” is more natural.
However, with variation 1 it is impossible to retain coherent *args/**kwds
internal representation when doing optimizations when using partial
recursivley.
Thus, “variation 1” has a drastic complexity increase as it requires additional containers to store the order.
This would lead to 30-40% complexity increase (if not more).
While “variation 2” is trivial in comparison and does not require drastic changes.
It would lead to 10-15% complexity increase, which is much more sensible.
Here is a simple Python wrapper, which makes use of current partial
to achieve this:
CPython issue comment
3. Version 3. - impartial
(Indexed mode partial).
class IPlaceholder:
__slots__ = ('idx',)
....
def foo(a=1, b=1, c=1, d=1, e=1):
return a + b + c - d - e
_0, _1, _2 = map(IPlaceholder, range(2))
p = impartial(foo, _2, _0, _0, e=_1)
# signature(p) # foo(b_and_c, e, a, d=1)
print(p(1, 2, 3)) # 4
This one is pretty complicated.
Overhead on top of “V2 variation 1” is smaller than it might seem.
However, it involves some inconveniences, such as merging arguments in signature, when 1 positional input fills more than one argument.
It becomes even more ambiguous with negative indices.
However, the good new is that it is possible to have a performant version of this for those rare cases where this can be useful.
Given kwpartial
exists:
p = kwpartial(foo, _, _, _, e=_)
IG = opr.itemgetter(2, 0, 0, 1)
args = (1, 2, 3)
new_args = IG(*args)
print(p(*new_args)) # 4 (same as above)
This also allows negative indices.
Summary and Current conclusion
So far I think that the effort required for “V2 variation 2” might be justified.
It is fairly useful extension which does not add much complexity while providing convenient path to ad-hoc V3 - impartial
.
Making kwpartial
from current partial
using pure python is expensive.
It results in > 100%
performance overhead.
While making impartial
from kwpartial
is a much better deal, given performant itemgetter
.
It is only ~50%
(my best guess) overhead on top of kwpartial
implemented in CPython
, which is quite good given flexibility gain.
So my current take is that despite awkwardness of V2 variation 2
, where keyword Placeholders
get pushed in front of positional Placeholders
, it could be a good compromise.
If say, previous partial
covered 65% of cases.
V1 - positional Placeholders
added 15% coverage.
V2 - keyword Placeholders
would add extra 10%.
Extra 5% then has a convenient trick to get V3 - Indexed mode
with attrgetter
.
Which results in reasonably performant coverage for say 95% of cases.
Of course, this is speculation.
Percentage of remaining cases is hard to know.
But at least proportions of relative coverage should be somewhat sensible.
So this is mostly to document for the future.
However, if PRs on the back of current partial
are merged and this is deemed worthwhile (not only by me), I might implement this.
I have worked out the logic in the process (apart from signature
logic) anyways.