-
-
Notifications
You must be signed in to change notification settings - Fork 32.3k
gh-119127: functools.partial placeholders #119827
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 68 commits
ee7333c
8bcc462
c67c9b4
680d900
9591ff5
067e938
8af20b3
607a0b1
f55801e
5894145
3722e07
a79c2af
12aaa72
92c767b
496a9d2
38d9c11
707b957
14b38ca
32bca19
8576493
a3fd2d6
0852993
6fea348
caec6e8
115b8c5
3f5f00b
202c929
2c16d38
400ff55
8ccc38f
e7c82c7
c9b7ef3
e59d711
7bfc591
7957a97
8aaee6a
fe8e0ad
00dd80e
d352cfa
9038ed5
49b8c71
bc1fdbd
3067221
1185510
266b4fa
dd58a12
5971fbb
9033650
d31e5d1
a3d39b0
9e4c5df
16f12f8
82dd600
f9cb653
d255524
404044e
800217b
38ee450
11f47db
3c872bd
fd16189
a6c6ef2
1c8d73e
a8bd3ae
70e47ed
2eacf5e
f78d8d3
0a8640e
6e3d282
66c305d
14bf68c
ee642d5
8d6c28e
8744bcb
b896470
4881ae6
c3ad7d9
5e5d484
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,7 @@ | |
from abc import get_cache_token | ||
from collections import namedtuple | ||
# import types, weakref # Deferred to single_dispatch() | ||
from operator import itemgetter | ||
dg-pb marked this conversation as resolved.
Show resolved
Hide resolved
|
||
from reprlib import recursive_repr | ||
from types import MethodType | ||
from _thread import RLock | ||
|
@@ -274,43 +275,125 @@ def reduce(function, sequence, initial=_initial_missing): | |
### partial() argument application | ||
################################################################################ | ||
|
||
# Purely functional, no descriptor behaviour | ||
class partial: | ||
"""New function with partial application of the given arguments | ||
and keywords. | ||
|
||
class _PlaceholderType: | ||
"""The type of the Placeholder singleton. | ||
|
||
Used as a placeholder for partial arguments. | ||
""" | ||
__instance = None | ||
dg-pb marked this conversation as resolved.
Show resolved
Hide resolved
|
||
__slots__ = () | ||
|
||
def __init_subclass__(cls, *args, **kwargs): | ||
raise TypeError(f"type '{cls.__name__}' is not an acceptable base type") | ||
|
||
__slots__ = "func", "args", "keywords", "__dict__", "__weakref__" | ||
def __new__(cls): | ||
if cls.__instance is None: | ||
cls.__instance = object.__new__(cls) | ||
return cls.__instance | ||
Comment on lines
+290
to
+293
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is overcomplication. The user has no reasons to create an instance of private class If you want to add some guards here, just make There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was to mimic C implementation, which has the behaviour of I think the question is: "Is it a good practice for a non-trivial sentinel to be singleton, i.e. If yes and this sentinel is considered non-trivial, then this is as good as it can get for now and protection issues can be sorted out together with further developments in this area. If no, then this needs to be changed for both C and Python. @rhettinger has suggested this initially and I like this behaviour (and adapted it to my own sentinels). It would be good if you together could come to agreement before I make any further changes here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not required for its main function, and this complicates both implementations. It is better to implement only necessary parts. If later we will find a need of this feature, it will be easier to add it than to remove it. Strictly speaking, making the Placeholder class non-inheritable and non-instantiable is not required. But it is easy to implement. I hope Raymond will change his opinion on this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What you are saying makes sense, but at the same time I like current behaviour and if sentinels were standardised and their creation was made more convenient I think this elegant behaviour would be nice to get by default. I am neutral by now on this specific case. Well, slightly negative just because I put thought and effort into this and simply like it. |
||
|
||
def __repr__(self): | ||
return 'Placeholder' | ||
|
||
def __new__(cls, func, /, *args, **keywords): | ||
def __reduce__(self): | ||
return 'Placeholder' | ||
|
||
Placeholder = _PlaceholderType() | ||
|
||
def _partial_prepare_merger(args): | ||
if not args: | ||
return 0, None | ||
nargs = len(args) | ||
order = [] | ||
j = nargs | ||
for i, a in enumerate(args): | ||
if a is Placeholder: | ||
order.append(j) | ||
j += 1 | ||
else: | ||
order.append(i) | ||
phcount = j - nargs | ||
merger = itemgetter(*order) if phcount else None | ||
return phcount, merger | ||
|
||
def _partial_new(cls, func, /, *args, **keywords): | ||
if issubclass(cls, partial): | ||
base_cls = partial | ||
if not callable(func): | ||
raise TypeError("the first argument must be callable") | ||
else: | ||
base_cls = partialmethod | ||
# func could be a descriptor like classmethod which isn't callable | ||
if not callable(func) and not hasattr(func, "__get__"): | ||
raise TypeError(f"the first argument {func!r} must be a callable " | ||
"or a descriptor") | ||
if args and args[-1] is Placeholder: | ||
raise TypeError("trailing Placeholders are not allowed") | ||
dg-pb marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if isinstance(func, base_cls): | ||
pto_phcount = func._phcount | ||
tot_args = func.args | ||
if args: | ||
tot_args += args | ||
if pto_phcount: | ||
# merge args with args of `func` which is `partial` | ||
nargs = len(args) | ||
if nargs < pto_phcount: | ||
tot_args += (Placeholder,) * (pto_phcount - nargs) | ||
tot_args = func._merger(tot_args) | ||
if nargs > pto_phcount: | ||
tot_args += args[pto_phcount:] | ||
phcount, merger = _partial_prepare_merger(tot_args) | ||
else: # works for both pto_phcount == 0 and != 0 | ||
phcount, merger = pto_phcount, func._merger | ||
keywords = {**func.keywords, **keywords} | ||
func = func.func | ||
else: | ||
tot_args = args | ||
phcount, merger = _partial_prepare_merger(tot_args) | ||
|
||
self = object.__new__(cls) | ||
self.func = func | ||
self.args = tot_args | ||
self.keywords = keywords | ||
self._phcount = phcount | ||
self._merger = merger | ||
return self | ||
|
||
def _partial_repr(self): | ||
cls = type(self) | ||
module = cls.__module__ | ||
qualname = cls.__qualname__ | ||
args = [repr(self.func)] | ||
args.extend(map(repr, self.args)) | ||
args.extend(f"{k}={v!r}" for k, v in self.keywords.items()) | ||
return f"{module}.{qualname}({', '.join(args)})" | ||
|
||
if isinstance(func, partial): | ||
args = func.args + args | ||
keywords = {**func.keywords, **keywords} | ||
func = func.func | ||
# Purely functional, no descriptor behaviour | ||
class partial: | ||
"""New function with partial application of the given arguments | ||
and keywords. | ||
""" | ||
|
||
self = super(partial, cls).__new__(cls) | ||
__slots__ = ("func", "args", "keywords", "_phcount", "_merger", | ||
"__dict__", "__weakref__") | ||
|
||
self.func = func | ||
self.args = args | ||
self.keywords = keywords | ||
return self | ||
__new__ = _partial_new | ||
__repr__ = recursive_repr()(_partial_repr) | ||
|
||
def __call__(self, /, *args, **keywords): | ||
phcount = self._phcount | ||
if phcount: | ||
try: | ||
pto_args = self._merger(self.args + args) | ||
args = args[phcount:] | ||
except IndexError: | ||
raise TypeError("missing positional arguments " | ||
"in 'partial' call; expected " | ||
f"at least {phcount}, got {len(args)}") | ||
else: | ||
pto_args = self.args | ||
keywords = {**self.keywords, **keywords} | ||
return self.func(*self.args, *args, **keywords) | ||
|
||
@recursive_repr() | ||
def __repr__(self): | ||
cls = type(self) | ||
qualname = cls.__qualname__ | ||
module = cls.__module__ | ||
args = [repr(self.func)] | ||
args.extend(repr(x) for x in self.args) | ||
args.extend(f"{k}={v!r}" for (k, v) in self.keywords.items()) | ||
return f"{module}.{qualname}({', '.join(args)})" | ||
return self.func(*pto_args, *args, **keywords) | ||
|
||
def __get__(self, obj, objtype=None): | ||
if obj is None: | ||
|
@@ -332,6 +415,10 @@ def __setstate__(self, state): | |
(namespace is not None and not isinstance(namespace, dict))): | ||
raise TypeError("invalid partial state") | ||
|
||
if args and args[-1] is Placeholder: | ||
raise TypeError("trailing Placeholders are not allowed") | ||
phcount, merger = _partial_prepare_merger(args) | ||
|
||
args = tuple(args) # just in case it's a subclass | ||
if kwds is None: | ||
kwds = {} | ||
|
@@ -344,53 +431,40 @@ def __setstate__(self, state): | |
self.func = func | ||
self.args = args | ||
self.keywords = kwds | ||
self._phcount = phcount | ||
self._merger = merger | ||
|
||
try: | ||
from _functools import partial | ||
from _functools import partial, Placeholder, _PlaceholderType | ||
except ImportError: | ||
pass | ||
|
||
# Descriptor version | ||
class partialmethod(object): | ||
class partialmethod: | ||
"""Method descriptor with partial application of the given arguments | ||
and keywords. | ||
|
||
Supports wrapping existing descriptors and handles non-descriptor | ||
callables as instance methods. | ||
""" | ||
|
||
def __init__(self, func, /, *args, **keywords): | ||
if not callable(func) and not hasattr(func, "__get__"): | ||
raise TypeError("{!r} is not callable or a descriptor" | ||
.format(func)) | ||
|
||
# func could be a descriptor like classmethod which isn't callable, | ||
# so we can't inherit from partial (it verifies func is callable) | ||
if isinstance(func, partialmethod): | ||
# flattening is mandatory in order to place cls/self before all | ||
# other arguments | ||
# it's also more efficient since only one function will be called | ||
self.func = func.func | ||
self.args = func.args + args | ||
self.keywords = {**func.keywords, **keywords} | ||
else: | ||
self.func = func | ||
self.args = args | ||
self.keywords = keywords | ||
|
||
def __repr__(self): | ||
cls = type(self) | ||
module = cls.__module__ | ||
qualname = cls.__qualname__ | ||
args = [repr(self.func)] | ||
args.extend(map(repr, self.args)) | ||
args.extend(f"{k}={v!r}" for k, v in self.keywords.items()) | ||
return f"{module}.{qualname}({', '.join(args)})" | ||
__new__ = _partial_new | ||
__repr__ = _partial_repr | ||
|
||
def _make_unbound_method(self): | ||
def _method(cls_or_self, /, *args, **keywords): | ||
phcount = self._phcount | ||
if phcount: | ||
try: | ||
pto_args = self._merger(self.args + args) | ||
args = args[phcount:] | ||
except IndexError: | ||
raise TypeError("missing positional arguments " | ||
"in 'partialmethod' call; expected " | ||
f"at least {phcount}, got {len(args)}") | ||
else: | ||
pto_args = self.args | ||
keywords = {**self.keywords, **keywords} | ||
return self.func(cls_or_self, *self.args, *args, **keywords) | ||
return self.func(cls_or_self, *pto_args, *args, **keywords) | ||
_method.__isabstractmethod__ = self.__isabstractmethod__ | ||
_method.__partialmethod__ = self | ||
return _method | ||
|
Uh oh!
There was an error while loading. Please reload this page.