Hello,
currently, regarding positional arguments, `partial` gives us the option to
partialize functions from the left. There's been some interest about
partializing functions from the right instead (e.g. [SO post, 9k views, 39
upvotes](https://stackoverflow.com/q/7811247/3767239)), especially w.r.t. the
various `str` methods.
I propose adding a function to `functools` that works with placeholders and
thus offers even greater flexibility. The Ellipsis literal `...` seems a
intuitive choice for that task. When eventually calling such a "partial
placeholder" object, it would fill in placeholders from the left and add
remaining `args` to the right. In terms of implementation this can be realized
as a subclass of `partial` itself.
## Implementation
from functools import partial
from reprlib import recursive_repr
class partial_placehold(partial):
placeholder = Ellipsis
def __call__(self, /, *args, **keywords):
args = iter(args)
try:
old_args = [x if x is not self.placeholder else next(args) for
x in self.args]
except StopIteration:
raise TypeError('too few arguments were supplied') from None
keywords = {**self.keywords, **keywords}
return self.func(*old_args, *args, **keywords)
@recursive_repr()
def __repr__(self):
qualname = type(self).__qualname__
args = [repr(self.func)]
args.extend(repr(x) if x is not self.placeholder else '...' for x
in self.args) # Only this line deviates from `partial.__repr__`; could also
factor that out into a separate method.
args.extend(f"{k}={v!r}" for (k, v) in self.keywords.items())
if type(self).__module__ == "functools":
return f"functools.{qualname}({', '.join(args)})"
return f"{qualname}({', '.join(args)})"
# Would need to add something for compatibility with `partial`, i.e. for
partializing a placeholder function.
## Example
This allows for example the following usage:
replace_dots_with_underscore = partial_placehold(str.replace, ..., '.', '_')
replace_dots_with_underscore('foo.bar.baz')
## Relevance
Sure we could also use a `lambda` instead ([as discussed
here](https://mail.python.org/archives/list/[email protected]/message/YD5OQEPXRL6LIK3DRVIZR6IIMHATCMVC/))
but there was a reason `partial` was introduced and I think the same arguments
apply here too. Though most functions allow partializing via keyword arguments
and this is undoubtedly a cleaner way, some might not and for example
built-ins' methods won't allow it. Especially Python 3.8's introduction of
positional-only parameters (PEP 570) might give rise to cases where `partial`
is not sufficient.
In case inspection is desired a `lambda` does not provide much information
(sure you could always dig deeper with `inspect` for example but that's not the
point). Consider the following example of a pre-defined sequence of default
postprocessing steps and the user might add their own or remove existing ones,
as appropriate:
postprocessing_steps = [
lambda s: s.replace('foo', 'bar'),
]
print(postprocessing_steps[0]) # <function <lambda> at 0x7f94a850dd30>
This doesn't give a lot of information about what the lambda actually does (and
thus whether the user should remove it or not). Using the `partial_placehold`
instead, it's clear what is happening:
postprocessing_steps = [
partial_placehold(str.replace, ..., 'foo', 'bar'),
]
print(postprocessing_steps[0]) # partial_placehold(<method 'replace' of
'str' objects>, ..., 'foo', 'bar')
## Compatibility
The proposed solution works with the current syntax and the usage of Ellipsis
as a placeholder object is likely not to collide with actually used values (in
any case the user might still reassign the `.placeholder` attribute).
Because the direction of partializing is unchanged (still left to right) this
doesn't introduce ambiguities which might come with a "right partial" function.
Creating a placeholder function from a `partial` object is possible without any
changes, the opposite way requires an additional check to result in a
placeholder object again.
## Possible confusion
Regarding the usage of Ellipsis right now, in `numpy` or `typing` for example,
it always represents a placeholder for multiple "things", not a single one:
array[..., None] # All the dimensions of `array` plus a new one.
typing.Tuple[str, ...] # Any number of str objects.
So the expectations might be biased in that sense. For example:
def foo(a, b, c, d):
pass
p_foo = partial_placehold(foo, ..., 1, 2)
p_foo(3, 4)
Someone else reviewing the code might now assume that the `...` means to act as
a placeholder for all arguments except the last two (and hence `p_foo(3, 4)`
would be equivalent to `foo(3, 4, 1, 2)` while it actually is equivalent to
`foo(3, 1, 2, 4)`). But this would be again some kind of "right partial"
function and also the function name implies something else; documentation might
clarify as well, of course.
## Conclusion
Adding a "partial with placeholders" function to `functools` allows for
covering use cases where the standard `partial` is not sufficient. No new
syntax is required and the implementation is fairly straightforward given the
inheritance from `partial`. Ellipsis `...` seems an intuitive choice for acting
as a placeholder (concerning both, conflicts with actual partial values and
code readability). There are uses cases where such a function would provide a
clean solution and there is an interest in the community
(https://stackoverflow.com/q/7811247/3767239,
https://stackoverflow.com/q/19701775/3767239 for example). Especially with the
introduction of positional-only parameters new use cases are likely to arise.
-----
**Related threads:**
*
https://mail.python.org/archives/list/[email protected]/message/TVNCM7XWIP33Q3435PXOHWIFTPMJ6PCX/
- Mentions essentially a similar idea.
The original [PEP 309 -- Partial Function
Application](https://www.python.org/dev/peps/pep-0309/) also mentions:
> Partially applying arguments from the right, or inserting arguments at
> arbitrary positions creates its own problems, but pending discovery of a good
> implementation and non-confusing semantics, I don't think it should be ruled
> out.
_______________________________________________
Python-ideas mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at
https://mail.python.org/archives/list/[email protected]/message/RLM7XILUVIGSVLLBCB7NY5NJ4PNSRSFJ/
Code of Conduct: http://python.org/psf/codeofconduct/