Support for using partial with positional only argument functions

I’d like to propose adding a way for functions that take positional only arguments to be used with functools.partial.

As an example, say you want to create a function that parses datetimes for a specific format.

Due to positional only arguments, the following will not work:

>>> functools.partial(datetime.datetime.strptime, format="%d %B, %Y")
>>> p("3 June, 2021")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: strptime() takes no keyword arguments

I’d like to add a PLACEHOLDER (similar to c++ bind) sentinel in partial that allows to use the functools.partial for positional only argument functions. Example:

from functools import partial
p = partial(
    datetime.datetime.strptime,
    partial.PLACEHOLDER,
    "%d %B, %Y"
)
p("3 June, 2019")  # works as expected

With the introduction of PEP570, there are now even more functions that cannot be used with partiality due to the enforcement of positional arguments, making this even more useful IMHO.

I have a PoC here if you want to play with it for the Python part. If this is of interest I’ll create a bpo, add this to the extension as well and send a PR.

1 Like

You can use lambda:

p = lambda x: datetime.datetime.strptime(x, "%d %B, %Y")
1 Like

That is indeed what I am doing right now :blush:, I guess it is also that I generally prefer partials over lambdas for this kind of case as I have seen people fall into cases like:

from functools import partial
import datetime
format_ = "%d %B, %Y"
p = lambda datestr: datetime.datetime.strptime(datestr, format_)
format_ = None
print(p("3 June, 2019"))   # Fails

Also, 100% subjective, it’s easier for me to read partials, as I know it is just the same functionality just binding arguments.
If lambdas were a perfect solution for binding arguments there would not be partials at all, right?

2 Likes

Rather than adding a magical “PLACEHOLDER” sentinel value, another
common solution is to define a version of partial that binds from the
right rather than the left.

Here’s an untested implementation:

def partial_right(func, /, *args, **keywords):
    def newfunc(*fargs, **fkeywords):
        newkeywords = {**keywords, **fkeywords}
        return func(*fargs, *args, **newkeywords)
    newfunc.func = func
    newfunc.args = args
    newfunc.keywords = keywords
    return newfunc

It is a one line change from the code given in the docs:

https://docs.python.org/3/library/functools.html#functools.partial

I don’t think this is a very common need. As far as I can tell, none of
the major functional-programming languages that support partial
application provide the functionality. Although that might be because
their partial application are built on currying?

In any case, you are not the only one to request this sort of feature,
at least in the Javascript world:

3 Likes

Functions — funcy 2.0 documentation - is available in third-party tool
How to fill specific positional arguments with partial in python? - Stack Overflow - another conversation with workarounds

Another approach how to implement such behavior:

Let’s assume partial_positional:

def partial_positional(func: Callable, *pos_args: Union[Tuple[int, Any], Tuple[slice, Iterable[Any]]])->Callable:
    ...

For POSITIONAL_ONLY or POSITIONAL_OR_KEYWORD it applies default values for according parameters
For VAR_POSITIONAL values are bound, but can’t be rebounded later

pos_args: Tuple[int, Any] - index of positional arg and its value
pos_args: Tuple[slice, Iterable[Any]] - slice of pos_args indexes and their values.
pos_args indexes must not be overlapped during one call of partial

Looks great! It can be a very helpfull feature for rare cases then u need to use partial with unknown function. I have a production example in my OSS project
Also, PLACEHOLDER can be easely added and it costs nothing for the regular partial usage

from functools import partial as p

class Placeholder:
    pass

class partial(p):
    def __call__(self, /, *args, **keywords):
        i = 0
        final_args = []
        for partial_arg in self.args:
            if partial_arg == Placeholder:
                try:
                    final_args.append(args[i])
                except IndexError:
                    raise RuntimeError("not enough positional arguments to unpack")

                i += 1
            else:
                final_args.append(partial_arg)
        final_args.extend(args[i:])

        keywords = {**self.keywords, **keywords}
        return self.func(*final_args, **keywords)

It is just an example, I have an implementation, resolving in the __init__ method, not in runtime, but it still works in these cases:

def func(a, b, c):
    return a + b + c

partial(func, 1, Placeholder, 3)(2) == 6
partial(func, Placeholder, 2)(1, 3) == 6
partial(func, Placeholder, Placeholder, 3)(1, b=2) # raises TypeError

Well, ellepsis usage looks great. But why it is not the language feature?