Suppose I am to implement a simple decorator @notifyme
that prints a message when the decorated function is invoked. I would like the decorator to accept one argument to print a customized message; the argument (along with the parentheses surrounding the argument) may be omitted, in which case the default message is printed:
@notifyme('Foo is invoked!')
def foo():
pass
@notifyme # instead of @notifyme()
def bar():
pass
To allow the parentheses to be omitted, I have to provide two implementations of @notifyme
:
The first implementation allows the user to customize the message, so it accepts a string as argument and returns a decorator:
def notifyme_customized(message: str) -> Callable[[Callable], Callable]:
def decorator(func: Callable) -> Callable:
def decorated_func(*args, **kwargs):
print(str)
return func(*args, **kwargs)
return decorated_func
return decorator
The second implementation is a decorator itself and uses the first implementation to print a default message:
def notifyme_default(func: Callable) -> Callable:
return notifyme_customized('The function is invoked.')(func)
To make the two implementations above use the same name notifyme
, I used functools.singledispatch
to dynamically dispatch the call to notifyme
to one of the two implementations:
# This is a complete minimal reproducible example
from functools import singledispatch
from typing import Callable
@singledispatch
def notifyme(arg):
return NotImplemented
@notifyme.register
def notifyme_customized(message: str) -> Callable[[Callable], Callable]:
def decorator(func: Callable) -> Callable:
def decorated_func(*args, **kwargs):
print(str)
return func(*args, **kwargs)
return decorated_func
return decorator
@notifyme.register
def notifyme_default(func: Callable) -> Callable:
return notifyme_customized('The function is invoked.')(func)
However, as the code is interpreted by the Python interpreter, it complains that typing.Callable
is an invalid type:
Traceback (most recent call last):
File "demo.py", line 20, in <module>
def notifyme_default(func: Callable) -> Callable:
File "C:\Program Files\Python38\lib\functools.py", line 860, in register
raise TypeError(
TypeError: Invalid annotation for 'func'. typing.Callable is not a class.
I have found this issue on Python bug tracker, according to which it seems to be expected behavior since Python 3.7. Does a solution or workaround exist in Python 3.8 I use currently (or Python 3.9 that has been released recently)?
Thanks in advance.
The singledispatch function is useful, but it is not without limitations. Its main limitation is also apparent in its name: it can only dispatch based on a single function parameter, and then only the first. If you require multiple dispatching you will need a third-party library as Python does not come with that built in.
They did this by adding a neat little decorator to the functools module called singledispatch. This decorator will transform your regular function into a single dispatch generic function. Note however that singledispatch only happens based on the first argument's type. Let's take a look at an example to see how this works!
You still have to register your Vector implementation for the single dispatch after the class has been created, because only then can you register a dispatch for the class:
The functools.singledispatchmethod () option that Python 3.8 adds uses a class as the decorator which implements the descriptor protocol, just like methods do. This lets it then handle dispatch before binding (so before self would be prepended to the argument list) and then bind the registered function that the singledispatch dispatcher returns.
https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable
from collections import abc
@notifyme.register
def notifyme_default(func: abc.Callable) -> Callable:
return notifyme_customized('The function is invoked.')(func)
I was unable to use typing.Callable
with functools.singledispatch
, but I did find a workaround by using a function
class reference instead:
from functools import singledispatch
from typing import Callable
function = type(lambda: ())
@singledispatch
def notifyme(arg):
return NotImplemented
@notifyme.register
def notifyme_customized(message: str) -> Callable[[Callable], Callable]:
def decorator(func: Callable) -> Callable:
def decorated_func(*args, **kwargs):
print(str)
return func(*args, **kwargs)
return decorated_func
return decorator
@notifyme.register
def notifyme_default(func: function) -> Callable:
return notifyme_customized('The function is invoked.')(func)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With