Skip to content

Commit d929652

Browse files
authored
pythongh-119127: functools.partial placeholders (pythongh-119827)
1 parent 4defb58 commit d929652

File tree

8 files changed

+681
-129
lines changed

8 files changed

+681
-129
lines changed

Diff for: Doc/library/functools.rst

+59-13
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,14 @@ The :mod:`functools` module defines the following functions:
328328
Returning ``NotImplemented`` from the underlying comparison function for
329329
unrecognised types is now supported.
330330

331+
.. data:: Placeholder
332+
333+
A singleton object used as a sentinel to reserve a place
334+
for positional arguments when calling :func:`partial`
335+
and :func:`partialmethod`.
336+
337+
.. versionadded:: 3.14
338+
331339
.. function:: partial(func, /, *args, **keywords)
332340

333341
Return a new :ref:`partial object<partial-objects>` which when called
@@ -338,26 +346,67 @@ The :mod:`functools` module defines the following functions:
338346
Roughly equivalent to::
339347

340348
def partial(func, /, *args, **keywords):
341-
def newfunc(*fargs, **fkeywords):
342-
newkeywords = {**keywords, **fkeywords}
343-
return func(*args, *fargs, **newkeywords)
349+
def newfunc(*more_args, **more_keywords):
350+
keywords_union = {**keywords, **more_keywords}
351+
return func(*args, *more_args, **keywords_union)
344352
newfunc.func = func
345353
newfunc.args = args
346354
newfunc.keywords = keywords
347355
return newfunc
348356

349-
The :func:`partial` is used for partial function application which "freezes"
357+
The :func:`partial` function is used for partial function application which "freezes"
350358
some portion of a function's arguments and/or keywords resulting in a new object
351359
with a simplified signature. For example, :func:`partial` can be used to create
352360
a callable that behaves like the :func:`int` function where the *base* argument
353-
defaults to two:
361+
defaults to ``2``:
362+
363+
.. doctest::
354364

355-
>>> from functools import partial
356365
>>> basetwo = partial(int, base=2)
357366
>>> basetwo.__doc__ = 'Convert base 2 string to an int.'
358367
>>> basetwo('10010')
359368
18
360369

370+
If :data:`Placeholder` sentinels are present in *args*, they will be filled first
371+
when :func:`partial` is called. This allows custom selection of positional arguments
372+
to be pre-filled when constructing a :ref:`partial object <partial-objects>`.
373+
374+
If :data:`!Placeholder` sentinels are present, all of them must be filled at call time:
375+
376+
.. doctest::
377+
378+
>>> say_to_world = partial(print, Placeholder, Placeholder, "world!")
379+
>>> say_to_world('Hello', 'dear')
380+
Hello dear world!
381+
382+
Calling ``say_to_world('Hello')`` would raise a :exc:`TypeError`, because
383+
only one positional argument is provided, while there are two placeholders
384+
in :ref:`partial object <partial-objects>`.
385+
386+
Successive :func:`partial` applications fill :data:`!Placeholder` sentinels
387+
of the input :func:`partial` objects with new positional arguments.
388+
A place for positional argument can be retained by inserting new
389+
:data:`!Placeholder` sentinel to the place held by previous :data:`!Placeholder`:
390+
391+
.. doctest::
392+
393+
>>> from functools import partial, Placeholder as _
394+
>>> remove = partial(str.replace, _, _, '')
395+
>>> message = 'Hello, dear dear world!'
396+
>>> remove(message, ' dear')
397+
'Hello, world!'
398+
>>> remove_dear = partial(remove, _, ' dear')
399+
>>> remove_dear(message)
400+
'Hello, world!'
401+
>>> remove_first_dear = partial(remove_dear, _, 1)
402+
>>> remove_first_dear(message)
403+
'Hello, dear world!'
404+
405+
Note, :data:`!Placeholder` has no special treatment when used for keyword
406+
argument of :data:`!Placeholder`.
407+
408+
.. versionchanged:: 3.14
409+
Added support for :data:`Placeholder` in positional arguments.
361410

362411
.. class:: partialmethod(func, /, *args, **keywords)
363412

@@ -742,10 +791,7 @@ have three read-only attributes:
742791
The keyword arguments that will be supplied when the :class:`partial` object is
743792
called.
744793

745-
:class:`partial` objects are like :ref:`function objects <user-defined-funcs>`
746-
in that they are callable, weak referenceable, and can have attributes.
747-
There are some important differences. For instance, the
748-
:attr:`~function.__name__` and :attr:`function.__doc__` attributes
749-
are not created automatically. Also, :class:`partial` objects defined in
750-
classes behave like static methods and do not transform into bound methods
751-
during instance attribute look-up.
794+
:class:`partial` objects are like :class:`function` objects in that they are
795+
callable, weak referenceable, and can have attributes. There are some important
796+
differences. For instance, the :attr:`~definition.__name__` and :attr:`__doc__` attributes
797+
are not created automatically.

Diff for: Doc/whatsnew/3.14.rst

+9
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,15 @@ Added support for converting any objects that have the
255255
(Contributed by Serhiy Storchaka in :gh:`82017`.)
256256

257257

258+
functools
259+
---------
260+
261+
* Added support to :func:`functools.partial` and
262+
:func:`functools.partialmethod` for :data:`functools.Placeholder` sentinels
263+
to reserve a place for positional arguments.
264+
(Contributed by Dominykas Grigonis in :gh:`119127`.)
265+
266+
258267
http
259268
----
260269

Diff for: Lib/functools.py

+133-59
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,18 @@
66
# Written by Nick Coghlan <ncoghlan at gmail.com>,
77
# Raymond Hettinger <python at rcn.com>,
88
# and Łukasz Langa <lukasz at langa.pl>.
9-
# Copyright (C) 2006-2013 Python Software Foundation.
9+
# Copyright (C) 2006-2024 Python Software Foundation.
1010
# See C source code for _functools credits/copyright
1111

1212
__all__ = ['update_wrapper', 'wraps', 'WRAPPER_ASSIGNMENTS', 'WRAPPER_UPDATES',
1313
'total_ordering', 'cache', 'cmp_to_key', 'lru_cache', 'reduce',
1414
'partial', 'partialmethod', 'singledispatch', 'singledispatchmethod',
15-
'cached_property']
15+
'cached_property', 'Placeholder']
1616

1717
from abc import get_cache_token
1818
from collections import namedtuple
1919
# import types, weakref # Deferred to single_dispatch()
20+
from operator import itemgetter
2021
from reprlib import recursive_repr
2122
from types import MethodType
2223
from _thread import RLock
@@ -274,43 +275,125 @@ def reduce(function, sequence, initial=_initial_missing):
274275
### partial() argument application
275276
################################################################################
276277

277-
# Purely functional, no descriptor behaviour
278-
class partial:
279-
"""New function with partial application of the given arguments
280-
and keywords.
278+
279+
class _PlaceholderType:
280+
"""The type of the Placeholder singleton.
281+
282+
Used as a placeholder for partial arguments.
281283
"""
284+
__instance = None
285+
__slots__ = ()
286+
287+
def __init_subclass__(cls, *args, **kwargs):
288+
raise TypeError(f"type '{cls.__name__}' is not an acceptable base type")
282289

283-
__slots__ = "func", "args", "keywords", "__dict__", "__weakref__"
290+
def __new__(cls):
291+
if cls.__instance is None:
292+
cls.__instance = object.__new__(cls)
293+
return cls.__instance
294+
295+
def __repr__(self):
296+
return 'Placeholder'
284297

285-
def __new__(cls, func, /, *args, **keywords):
298+
def __reduce__(self):
299+
return 'Placeholder'
300+
301+
Placeholder = _PlaceholderType()
302+
303+
def _partial_prepare_merger(args):
304+
if not args:
305+
return 0, None
306+
nargs = len(args)
307+
order = []
308+
j = nargs
309+
for i, a in enumerate(args):
310+
if a is Placeholder:
311+
order.append(j)
312+
j += 1
313+
else:
314+
order.append(i)
315+
phcount = j - nargs
316+
merger = itemgetter(*order) if phcount else None
317+
return phcount, merger
318+
319+
def _partial_new(cls, func, /, *args, **keywords):
320+
if issubclass(cls, partial):
321+
base_cls = partial
286322
if not callable(func):
287323
raise TypeError("the first argument must be callable")
324+
else:
325+
base_cls = partialmethod
326+
# func could be a descriptor like classmethod which isn't callable
327+
if not callable(func) and not hasattr(func, "__get__"):
328+
raise TypeError(f"the first argument {func!r} must be a callable "
329+
"or a descriptor")
330+
if args and args[-1] is Placeholder:
331+
raise TypeError("trailing Placeholders are not allowed")
332+
if isinstance(func, base_cls):
333+
pto_phcount = func._phcount
334+
tot_args = func.args
335+
if args:
336+
tot_args += args
337+
if pto_phcount:
338+
# merge args with args of `func` which is `partial`
339+
nargs = len(args)
340+
if nargs < pto_phcount:
341+
tot_args += (Placeholder,) * (pto_phcount - nargs)
342+
tot_args = func._merger(tot_args)
343+
if nargs > pto_phcount:
344+
tot_args += args[pto_phcount:]
345+
phcount, merger = _partial_prepare_merger(tot_args)
346+
else: # works for both pto_phcount == 0 and != 0
347+
phcount, merger = pto_phcount, func._merger
348+
keywords = {**func.keywords, **keywords}
349+
func = func.func
350+
else:
351+
tot_args = args
352+
phcount, merger = _partial_prepare_merger(tot_args)
353+
354+
self = object.__new__(cls)
355+
self.func = func
356+
self.args = tot_args
357+
self.keywords = keywords
358+
self._phcount = phcount
359+
self._merger = merger
360+
return self
361+
362+
def _partial_repr(self):
363+
cls = type(self)
364+
module = cls.__module__
365+
qualname = cls.__qualname__
366+
args = [repr(self.func)]
367+
args.extend(map(repr, self.args))
368+
args.extend(f"{k}={v!r}" for k, v in self.keywords.items())
369+
return f"{module}.{qualname}({', '.join(args)})"
288370

289-
if isinstance(func, partial):
290-
args = func.args + args
291-
keywords = {**func.keywords, **keywords}
292-
func = func.func
371+
# Purely functional, no descriptor behaviour
372+
class partial:
373+
"""New function with partial application of the given arguments
374+
and keywords.
375+
"""
293376

294-
self = super(partial, cls).__new__(cls)
377+
__slots__ = ("func", "args", "keywords", "_phcount", "_merger",
378+
"__dict__", "__weakref__")
295379

296-
self.func = func
297-
self.args = args
298-
self.keywords = keywords
299-
return self
380+
__new__ = _partial_new
381+
__repr__ = recursive_repr()(_partial_repr)
300382

301383
def __call__(self, /, *args, **keywords):
384+
phcount = self._phcount
385+
if phcount:
386+
try:
387+
pto_args = self._merger(self.args + args)
388+
args = args[phcount:]
389+
except IndexError:
390+
raise TypeError("missing positional arguments "
391+
"in 'partial' call; expected "
392+
f"at least {phcount}, got {len(args)}")
393+
else:
394+
pto_args = self.args
302395
keywords = {**self.keywords, **keywords}
303-
return self.func(*self.args, *args, **keywords)
304-
305-
@recursive_repr()
306-
def __repr__(self):
307-
cls = type(self)
308-
qualname = cls.__qualname__
309-
module = cls.__module__
310-
args = [repr(self.func)]
311-
args.extend(repr(x) for x in self.args)
312-
args.extend(f"{k}={v!r}" for (k, v) in self.keywords.items())
313-
return f"{module}.{qualname}({', '.join(args)})"
396+
return self.func(*pto_args, *args, **keywords)
314397

315398
def __get__(self, obj, objtype=None):
316399
if obj is None:
@@ -332,6 +415,10 @@ def __setstate__(self, state):
332415
(namespace is not None and not isinstance(namespace, dict))):
333416
raise TypeError("invalid partial state")
334417

418+
if args and args[-1] is Placeholder:
419+
raise TypeError("trailing Placeholders are not allowed")
420+
phcount, merger = _partial_prepare_merger(args)
421+
335422
args = tuple(args) # just in case it's a subclass
336423
if kwds is None:
337424
kwds = {}
@@ -344,53 +431,40 @@ def __setstate__(self, state):
344431
self.func = func
345432
self.args = args
346433
self.keywords = kwds
434+
self._phcount = phcount
435+
self._merger = merger
347436

348437
try:
349-
from _functools import partial
438+
from _functools import partial, Placeholder, _PlaceholderType
350439
except ImportError:
351440
pass
352441

353442
# Descriptor version
354-
class partialmethod(object):
443+
class partialmethod:
355444
"""Method descriptor with partial application of the given arguments
356445
and keywords.
357446
358447
Supports wrapping existing descriptors and handles non-descriptor
359448
callables as instance methods.
360449
"""
361-
362-
def __init__(self, func, /, *args, **keywords):
363-
if not callable(func) and not hasattr(func, "__get__"):
364-
raise TypeError("{!r} is not callable or a descriptor"
365-
.format(func))
366-
367-
# func could be a descriptor like classmethod which isn't callable,
368-
# so we can't inherit from partial (it verifies func is callable)
369-
if isinstance(func, partialmethod):
370-
# flattening is mandatory in order to place cls/self before all
371-
# other arguments
372-
# it's also more efficient since only one function will be called
373-
self.func = func.func
374-
self.args = func.args + args
375-
self.keywords = {**func.keywords, **keywords}
376-
else:
377-
self.func = func
378-
self.args = args
379-
self.keywords = keywords
380-
381-
def __repr__(self):
382-
cls = type(self)
383-
module = cls.__module__
384-
qualname = cls.__qualname__
385-
args = [repr(self.func)]
386-
args.extend(map(repr, self.args))
387-
args.extend(f"{k}={v!r}" for k, v in self.keywords.items())
388-
return f"{module}.{qualname}({', '.join(args)})"
450+
__new__ = _partial_new
451+
__repr__ = _partial_repr
389452

390453
def _make_unbound_method(self):
391454
def _method(cls_or_self, /, *args, **keywords):
455+
phcount = self._phcount
456+
if phcount:
457+
try:
458+
pto_args = self._merger(self.args + args)
459+
args = args[phcount:]
460+
except IndexError:
461+
raise TypeError("missing positional arguments "
462+
"in 'partialmethod' call; expected "
463+
f"at least {phcount}, got {len(args)}")
464+
else:
465+
pto_args = self.args
392466
keywords = {**self.keywords, **keywords}
393-
return self.func(cls_or_self, *self.args, *args, **keywords)
467+
return self.func(cls_or_self, *pto_args, *args, **keywords)
394468
_method.__isabstractmethod__ = self.__isabstractmethod__
395469
_method.__partialmethod__ = self
396470
return _method

0 commit comments

Comments
 (0)