-
-
Notifications
You must be signed in to change notification settings - Fork 31.4k
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
Add functools.partialmethod #48581
Comments
Calling a function created by _functools.partial as a method raises an "TypeError: method_new() takes exactly n non-keyword arguments (0 given)" Where method_new is the function passed into partial() and n is the This does not happen when using a python version of partial(). Strangely, in the circumstance that I originally encountered the bug, Tested on 2.6 and 2.5.2 |
A short update, I believe that the reason that it was working in one |
I don't think this is any kind of bug, it is simply a product of only |
Reclassifying as a feature request. |
I followed the advice of Raymond and implement a descriptor on partial. |
Christophe, It looks like your patch goes out of its way to avoid creating nested partials. This is a worthwhile goal and I think it should be done in partial_new so that partial(partial(f, x), y) returns partial(f, x, y). If fact, I was surprised to learn that current partial implementation does not behave this way: >>> partial(partial(f, 1), 2).func
<functools.partial object at 0x100435af8> Does anyone know the reason for the current behavior? It is possible that I am missing some subtlety related to keyword arguments. |
Please see bpo-7830 for a related patch. |
I'm having some trouble wrapping my head around this one. It isn't obvious to me that should print (<A object at 0x1234>, 'argA', 'argB') and not The patch seems to prefer the first form but if you are using a partial shouldn't you expect 'argA' to always be the first argument to the partial-ized function? |
I would expect the second and would view the first as a bug. |
We talked about it at sprints and the semantics are ambiguous and there are alternatives. Ambiguous: Alternatives: partial is a convenience function not an optimization (it doesn't offer a speedup. So you can write a lambda or named function that has the exact semantics you want without suffering a speed penalty. So unless there are a lot of good use cases with obvious behavior, we should refuse the temptation to guess and leave partial as-is. |
correction: |
I've attached a patch that implements the descriptor protocol for functools.partial with minimum changes. |
Just for the record, the behaviour is documented, unfortunately in the very last line of the functools documentation: "Also, partial objects defined in classes behave like static methods and do not transform into bound methods during instance attribute look-up." Concerning how exactly they should behave during that lookup, I'd use the least surprising variant, namely that they are not treated differently from other functions: The first parameter is implicitly "self". |
What's preventing this from being committed and closed? |
There is at least one thing that is missing in the patch, it lacks the necessary tests. The partialbug.py demonstrates the issue, it could be used as a base. However, even then, there is still one thing that is problematic: The fact that partial() returns something that behaves like a static method is documented and changing that is not backward compatible. I still think that something like this should become part of Python though. Jack Diederich argues that you can use lambda to achieve the same, but that is not always true. If you want to bind an argument to the current value of a variable instead of a constant, lambda fails. You need the closure created by a function call to bind those variables inside a local function. Having a dedicated function for that is IMHO preferable to people copying the Python-only equivalent of partial() to achieve the same effect or even inventing their own. |
See also bpo-11470. |
I don't believe it is reasonable to change the behaviour of partial at this late stage of the game. It's documented as behaving like staticmethod (albeit by not implementing the descriptor protocol at all), so that's no longer something we can change. If bpo-11470 is added, then we'll just implement the staticmethod-like behaviour explicitly rather than leaving it as implicit. More importantly, the acceptance of PEP-443's functools.singledispatch makes it more desirable than ever to support partial binding for method implementations *even when the descriptor protocol is not involved*. Accordingly, I suggest converting this proposal to a separate functools.partialmethod API that:
functools.partial will then continue to behave as it always has (thus posing no backwards compatibility risks), while the new partialmethod implementation should work with both class descriptor protocol based dispatch (through point 3) *and* the new functools.singledispatch mechanism (through point 1). |
Any associated tests may also want check that wrapping classmethod around a partialmethod generates a well behaved class method, and ditto for property. If singledispath, classmethod, partialmethod and class and instance attribute access all work correctly, then we can be absolutely certain the results is behaving just like an ordinary function does :) |
This sounds excellent Nick. |
To clarify the current state of this:
The following prototype should work as a starting point to be elaborated into a full patch with docs and tests: class partialmethod(functools.partial):
def __get__(self, obj, cls):
if obj is None:
return self
return functools.partial(self.func,
*((obj,) + self.args),
**self.keywords)
def __call__(*args, **kwds):
self, *args = args
call_kwds = {}
call_kwds.update(self.keywords)
call_kwds.update(kwds)
return self.func(self,
*(self.args + args),
**call_kwds)
class C:
def example(self, *args, **kwds):
print(self, args, kwds)
fails = functools.partial(example, 1, 2, 3, x=1)
works = partialmethod(example, 1, 2, 3, x=1) >>> C().fails()
1 (2, 3) {'x': 1}
>>> C().works()
<__main__.C object at 0x7f91cefeea90> (1, 2, 3) {'x': 1}
>>> C().fails(4, 5, 6)
1 (2, 3, 4, 5, 6) {'x': 1}
>>> C().works(4, 5, 6)
<__main__.C object at 0x7f91cefeea10> (1, 2, 3, 4, 5, 6) {'x': 1} |
I just want to make sure I understand the semantics concerning class methods, the following example demonstrates a usage similar to regular methods as much as possible: class A(object):
def add(self, x, y):
print(self)
return x + y
add10 = partialmethod(add, 10)
add10class = classmethod(partialmethod(add, 10)) assert A().add10(5) == 15 # prints <main.A object at 0x1097e1390> Another option would be to return a class-bound partial from the __get__ method. It's not as consistent as the first example but perhaps nicer: class A(object):
def add(self, x, y):
print(self)
return x + y
add10 = partialmethod(add, 10) assert A().add10(5) == 15 # prints <main.A object at 0x1097e1390> Is the first option what you had in mind? |
On 26 Oct 2013 05:28, "alon horev" <report@bugs.python.org> wrote:
That's actually an interesting question. I was going to say yes, but then I That view means we should be delegating __get__ to the underlying We also need to make sure the descriptor does the right thing when |
Here's another attempt at a consistent api with regular methods. I also think staticmethod in this context is useless but agree consistency is a thing (you could just use partial instead). If consistency hadn't held us back, the first proposal of partialmethod, working both for instances and classes, would have been most elegant. from functools import partial
class partialmethod(object):
def __init__(self, func, *args, **keywords):
self.func = func
self.args = args
self.keywords = keywords
def __call__(self, *args, **keywords):
call_keywords = {}
call_keywords.update(self.keywords)
call_keywords.update(keywords)
return self.func(*(self.args + args), **call_keywords)
def __get__(self, obj, cls):
return partial(self.func.__get__(obj, cls), *self.args, **self.keywords)
class A(object):
def add(self, x, y):
print(self)
return x + y
add10 = partialmethod(add, 10)
add10class = partialmethod(classmethod(add), 10)
add10static = partialmethod(staticmethod(add), 'self', 10) assert A().add10(5) == 15 # prints <main.A object at 0x1097e1390> Once we approve of the API I'll provide a full fledged patch. |
I like your suggestion of not providing __call__(), as I don't see a way to make it work with arbitrary underlying descriptors, and neither classmethod nor staticmethod is callable. In terms of usage, I think this approach will be OK, as in practice I expect @classmethod, etc, will be applied to the initial method definition as appropriate, so they won't need to be inline except in test cases like these. Additional test cases needed:
All three should produce 15 as the result, and print the same thing as the following: A.add10class(5)
A.add10static(5)
A().add10(5) A test method that uses a partial instance as the callable would also be helpful in assuring the edge cases are covered. I believe it may be necessary to have a __get__ method something like: def _make_unbound_method(self, cls):
def _unbound_method(*args, **kwds):
call_keywords = self.keywords.copy()
call_keywords.update(keywords)
return self.func(*(self.args + args), **call_keywords)
_unbound_method.__objclass__ = cls
return _unbound_method
def __get__(self, obj, cls):
get = getattr(self.func, "__get__")
if get is None:
if obj is None:
return self._make_unbound_method(cls)
callable = self.func
else:
callable = get(obj, cls)
if callable is self.func:
return self._make_unbound_method(cls)
return partial(callable, *self.args, **self.keywords) |
I think the following test demonstrates the API we're looking for.
from functools import partial
class partialmethod(object):
def __init__(self, func, *args, **keywords):
# 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 = {}
self.keywords.update(func.keywords)
self.keywords.update(keywords)
else:
self.func = func
self.args = args
self.keywords = keywords
def _make_unbound_method(self):
def _method(*args, **keywords):
keywords.update(self.keywords)
cls_or_self, *rest = args
call_args = (cls_or_self,) + self.args + tuple(rest)
return self.func(*call_args, **keywords)
return _method
def __get__(self, obj, cls):
get = getattr(self.func, "__get__", None)
if get is None:
if obj is None:
return self._make_unbound_method()
return partial(self._make_unbound_method(), obj) # returns a bound method
else:
callable = get(obj, cls)
if callable is self.func:
return self._make_unbound_method()
return partial(callable, *self.args, **self.keywords)
class A(object):
def add(self, x, y):
return self, x + y
add10 = partialmethod(add, 10)
add10class = partialmethod(classmethod(add), 10)
add10static = partialmethod(staticmethod(add), 'self', 10)
return15 = partialmethod(add10, 5) # nested partialmethod
add2partial = partial(add, x=2)
return12 = partialmethod(add2partial, y=10) # partialmethod over partial
def test():
cls = A
instance = cls()
test() |
On 27 Oct 2013 22:17, "alon horev" <report@bugs.python.org> wrote:
The keyword arg handling is backwards for unbound methods (the call time Otherwise, looks good to me.
Do you mean @AbstractMethod? We need a __isabstractmethod__ property implementation that delegates the See http://docs.python.org/dev/library/abc#abc.abstractmethod for details. |
Adding a patch with tests and documentation. Please feel free to comment on anything: my English, coding/testing style. I'm pretty sure the documentation can be better but it turns out I speak better Python than English. Two decisions I've made and unsure of:
|
I've changed the test according to the code review. Thanks |
Updated patch based on Alon's last patch. The major functional change is to ensure __self__ is set appropriately on any bound methods returned by the descriptor. I also updated the docs and docstring, and added a What's New entry (as well as rewording the existing entry for functools.singledispatch) There were a few other cosmetic changes, with the most noticeable being moving the partialmethod implementation and tests adjacent to the existing ones for functools.partial. Assuming nobody pokes any significant holes in this idea and implementation in the meantime, I'll commit this before beta 1. |
New changeset 46d3c5539981 by Nick Coghlan in branch 'default': |
Should we add partialmethod to __all__ for consistency? |
Indeed, added to __all__ in http://hg.python.org/cpython/rev/ac1685661b07 |
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
bugs.python.org fields:
The text was updated successfully, but these errors were encountered: