-
-
Notifications
You must be signed in to change notification settings - Fork 32.3k
bpo-32380 Fix singledispatch interaction with methods #4987
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
Conversation
Singledispatch used the first argument to dispatch on. This made it incomatible with methods. This change adds support for methods by dispatching on the second argument if the passed function is a method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! I think this should be implemented more carefully, see comments.
Lib/functools.py
Outdated
return dispatch(args[0].__class__)(*args, **kw) | ||
# If our decorated function is in the first arg's attributes, | ||
# we are using a method. | ||
if getattr(args[0], func.__name__, False): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like a fragile hack. Some arguments may have the same attribute just by coincidence.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, yes, I wasn't sure if this was the best solution or not. The main issue is that the decorated function is not an instance of types.MethodType
when the decorator reaches it. I also looked at frame stack hacks but that would cause conflicts if it was decorating closures. At this point it seems to me a new methodsingledispatch
is in order, with dispatching on args[1]
. e.g.:
def methodsingledispatch(func):
wrapped = singledispatch(func)
def wrapper(*args, **kwargs):
wrapped.dispatch(args[1].__class__)(*args, **kw)
wrapper.register = wrapped.register
update_wrapper(wrapper, func)
return wrapper
Alternatively, I think allowing the user to specify which arg index for singledispatch to dispatch on would also be a possible solution (and a nice feature to add in general). Then it would be as simple as methodsingledispatch = partial(singledispatch, index=1)
or something like that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an API change, so this probably should go through python-ideas/python-dev.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I will post to python-dev.
Lib/functools.py
Outdated
@@ -816,8 +815,15 @@ def register(cls, func=None): | |||
dispatch_cache.clear() | |||
return func | |||
|
|||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you change the whitespace formatting?
Doc/library/functools.rst
Outdated
@@ -332,7 +332,7 @@ The :mod:`functools` module defines the following functions: | |||
False | |||
|
|||
When called, the generic function dispatches on the type of the first | |||
argument:: | |||
argument, unless it is used on class:: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be more clear. For example, "Note: if it is used with instance or class methods, then self
/cls
is skipped, and the next argument is used." Also add an example illustrating this.
Lib/test/test_functools.py
Outdated
self.assertEqual(a.t(0), "int") | ||
self.assertEqual(a.t(''), "str") | ||
self.assertEqual(a.t(0.0), "base") | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add more tests. At least cover these cases:
@classmethod
and@staticmethod
- method with zero args (it should fail gracefully)
- calling via instance vs calling via class for all of the above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@classmethod
and @staticmethod
return descriptors, and singledispatch requires a callable, thus these are not compatible with singledispatch (or would require more significant changes than I'm intending). Generally, you put these around the decorator, but for singledispatch that won't work, as that would break register()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be at least one way to do it. Allowing to use singledispatch
only with instance methods looks like an incomplete solution.
@@ -0,0 +1,2 @@ | |||
``functools.singledispatch`` now supports decorating and registering with | |||
class methods. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just "methods."
A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated. Once you have made the requested changes, please leave a comment on this pull request containing the phrase |
try: | ||
impl = registry[cls] | ||
impl = dispatch_cache[cls] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the diff being weird, I did not change this. The block and main part of singledispatch was indented.
Doc/library/functools.rst.bak
Outdated
@@ -0,0 +1,511 @@ | |||
:mod:`functools` --- Higher-order functions and operations on callable objects |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This shouldn't have been included in the commit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. I don't know why this isn't ignored by git. I will remove it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR, but unfortunately, I don't believe this particular class-definition-time-only approach is ever going to work right given the semantics of the descriptor protocol.
Instead, something more like the functools.partialmethod
will be needed, which defers some key tasks to its __get__
method implementation.
@@ -357,6 +357,23 @@ The :mod:`functools` module defines the following functions: | |||
for the base ``object`` type, which means it is used if no better | |||
implementation is found. | |||
|
|||
You can also tell ``@singeldispatch`` to dispatch on another argument index |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/singeldispatch/singledispatch/ (typo fix)
@@ -753,76 +753,78 @@ def singledispatch(func): | |||
implementations can be registered using the register() attribute of the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Docstring needs to be updated for the new parameter
@@ -744,7 +744,7 @@ def _find_impl(cls, registry): | |||
match = t | |||
return registry.get(match) | |||
|
|||
def singledispatch(func): | |||
def singledispatch(func=None, arg=0): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name arg
doesn't clear convey that this is the arg index the will be used to dispatch on. arg_index
would be clearer.
(Although this will become a moot point if you switch to a separate singledispatchmethod
as proposed below)
return "int" | ||
@t.register(str) | ||
def _(self, arg): | ||
return "str" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As @ilevkivskyi noted, additional tests are needed to cover the interaction with other method decorators, especially classmethod
, staticmethod
, abstractmethod
, and partialmethod
.
I think you will find that adding a parameter to the existing singledispatch
function isn't going to be sufficient to solve the problem in the general case (including supporting post-registration of new overloads after the class has been defined). Instead, you'll need something more along the lines of partialmethod
which is aware of the descriptor protocol, and uses that to delegate to the underlying descriptor's __get__
method as necessary. While it's a bit long, singledispatchmethod
would follow the existing convention for descriptor protocol aware variants of previously function-only APIs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @ncoghlan and @ethanhs, I'd been lurking the cpython PRs and had some time today, so I made a quick gist of a possible descriptor-aware implementation of singlemethoddispatch
. It's far from complete, but it does work for instance, abstract and class methods. (I haven't tried static methods or partial methods yet). I thought I'd link to it here in case it can provide you with some direction / inspiration.
Nice to haves (for my implementation, at least), would be a mechanism which wraps up any methods register
ed by the decorated method in the same decorators in which that method is wrapped; e.g., if Foo.bar
is a classmethod
, then Foo.bar.register(int, baz)
should make baz
a class method too. On the other hand, that's complicated to implement and possibly not even the desired behavior, so I'm putting it on the back-burner for now.
Anyway, here's the gist. Heavily inspired by the partialmethod
implementation, as you'll see: https://gist.github.com/ianhoffman/e25899e8fca3c1ef8772f331b14031d0
I'm afraid I don't have the bandwidth for this at the moment. @ianhoffman if you want to take over be my guest. I'll come back to it another time if not. |
It is sad to see this closed, I think it would be a useful addition, please keep me posted if you will resume your work. |
I want to focus on PEP 561 and a personal project, but I might get back to this after a month or two. |
Singledispatch dispatches on the first argument, making it incompatible with classes. This change fixes that.
https://bugs.python.org/issue32380