Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitd929652

Browse files
authored
gh-119127: functools.partial placeholders (gh-119827)
1 parent4defb58 commitd929652

File tree

8 files changed

+681
-129
lines changed

8 files changed

+681
-129
lines changed

‎Doc/library/functools.rst

Lines changed: 59 additions & 13 deletions
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`functionis 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 functoolsimport 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 functoolsimport partial, Placeholderas _
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.

‎Doc/whatsnew/3.14.rst

Lines changed: 9 additions & 0 deletions
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

‎Lib/functools.py

Lines changed: 133 additions & 59 deletions
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
fromabcimportget_cache_token
1818
fromcollectionsimportnamedtuple
1919
# import types, weakref # Deferred to single_dispatch()
20+
fromoperatorimportitemgetter
2021
fromreprlibimportrecursive_repr
2122
fromtypesimportMethodType
2223
from_threadimportRLock
@@ -274,43 +275,125 @@ def reduce(function, sequence, initial=_initial_missing):
274275
### partial() argument application
275276
################################################################################
276277

277-
# Purely functional, no descriptor behaviour
278-
classpartial:
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+
raiseTypeError(f"type '{cls.__name__}' is not an acceptable base type")
282289

283-
__slots__="func","args","keywords","__dict__","__weakref__"
290+
def__new__(cls):
291+
ifcls.__instanceisNone:
292+
cls.__instance=object.__new__(cls)
293+
returncls.__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+
ifnotargs:
305+
return0,None
306+
nargs=len(args)
307+
order= []
308+
j=nargs
309+
fori,ainenumerate(args):
310+
ifaisPlaceholder:
311+
order.append(j)
312+
j+=1
313+
else:
314+
order.append(i)
315+
phcount=j-nargs
316+
merger=itemgetter(*order)ifphcountelseNone
317+
returnphcount,merger
318+
319+
def_partial_new(cls,func,/,*args,**keywords):
320+
ifissubclass(cls,partial):
321+
base_cls=partial
286322
ifnotcallable(func):
287323
raiseTypeError("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+
ifnotcallable(func)andnothasattr(func,"__get__"):
328+
raiseTypeError(f"the first argument{func!r} must be a callable "
329+
"or a descriptor")
330+
ifargsandargs[-1]isPlaceholder:
331+
raiseTypeError("trailing Placeholders are not allowed")
332+
ifisinstance(func,base_cls):
333+
pto_phcount=func._phcount
334+
tot_args=func.args
335+
ifargs:
336+
tot_args+=args
337+
ifpto_phcount:
338+
# merge args with args of `func` which is `partial`
339+
nargs=len(args)
340+
ifnargs<pto_phcount:
341+
tot_args+= (Placeholder,)* (pto_phcount-nargs)
342+
tot_args=func._merger(tot_args)
343+
ifnargs>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+
returnself
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}"fork,vinself.keywords.items())
369+
returnf"{module}.{qualname}({', '.join(args)})"
288370

289-
ifisinstance(func,partial):
290-
args=func.args+args
291-
keywords= {**func.keywords,**keywords}
292-
func=func.func
371+
# Purely functional, no descriptor behaviour
372+
classpartial:
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-
returnself
380+
__new__=_partial_new
381+
__repr__=recursive_repr()(_partial_repr)
300382

301383
def__call__(self,/,*args,**keywords):
384+
phcount=self._phcount
385+
ifphcount:
386+
try:
387+
pto_args=self._merger(self.args+args)
388+
args=args[phcount:]
389+
exceptIndexError:
390+
raiseTypeError("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-
returnself.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)forxinself.args)
312-
args.extend(f"{k}={v!r}"for (k,v)inself.keywords.items())
313-
returnf"{module}.{qualname}({', '.join(args)})"
396+
returnself.func(*pto_args,*args,**keywords)
314397

315398
def__get__(self,obj,objtype=None):
316399
ifobjisNone:
@@ -332,6 +415,10 @@ def __setstate__(self, state):
332415
(namespaceisnotNoneandnotisinstance(namespace,dict))):
333416
raiseTypeError("invalid partial state")
334417

418+
ifargsandargs[-1]isPlaceholder:
419+
raiseTypeError("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
ifkwdsisNone:
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_functoolsimportpartial
438+
from_functoolsimportpartial,Placeholder,_PlaceholderType
350439
exceptImportError:
351440
pass
352441

353442
# Descriptor version
354-
classpartialmethod(object):
443+
classpartialmethod:
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-
ifnotcallable(func)andnothasattr(func,"__get__"):
364-
raiseTypeError("{!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-
ifisinstance(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}"fork,vinself.keywords.items())
388-
returnf"{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+
ifphcount:
457+
try:
458+
pto_args=self._merger(self.args+args)
459+
args=args[phcount:]
460+
exceptIndexError:
461+
raiseTypeError("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-
returnself.func(cls_or_self,*self.args,*args,**keywords)
467+
returnself.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)

[8]ページ先頭

©2009-2025 Movatter.jp