Please fix your Python decorators
hynek.me
hynek.me
Solution: Use a slow, third party library.
I'll (personally) stick with the inspection "problem". Changing program behavior at runtime using inspection against function signatures, unless done ridiculously well, is a recipe for creating an unmaintainable disaster.
Granted, I've seen some pretty cool things done with it (for example, injecting a database connection into a handler function if it has a "db" keyword in its signature), but I've also seen the edge cases where it blows up painfully; explicit vs implicit and all that.
The missing intellisense can be a pain, but the docstring is more reliable in most cases (such as the plethora of stdlib functions with "keyword" arguments which aren't actually keyword arguments since they're implemented in C).
The last time I wrote a bunch of decorators, I was writing python bindings to a set of C++ classes that each had an internal error state that had to be checked after each time a method was called to see if the method failed. If the error state was set, then subsequent method calls for the same object would fail silently, so it was crucial that the error state be checked. There were dozens of methods per class.
I ended up writing a class decorator that would decorate each method with the appropriate method decorator. The method decorator would call the GetErrorCode method, check if nonzero, reset the error state of the object, and then raise an appropriate python exception if necessary.
Major surgery to change the error handling in the called C++ code was not an option.
Can you suggest a better approach?
def doStuff():
# do stuff
return checkError(value)
@checkError
def doStuff():
# do stuff
return value
EDIT: In fact, I think the above succinctly hits on lotsoflumens' point. The latter clearly obfuscates the fact that an error check is performed behind the decorator. The former makes it very clear that something is happening to the return value before it leaves.Just because you can introspect a thing does not mean you must introspect that thing.
from django.utils.decorators import method_decorator
from django.views.decorators.http import last_modified
from rest_framework import viewsets
class SomeClassBasedView(viewsets.ViewSet):
def last_modified_time(self, request):
return cache.get_or_set('key', some_date)
@method_decorator(last_modified(last_modified_time))
def get_some_object(self):
# expensive DB operation
Of course it blew up, even when I decorated the last_modified_time with @staticmethod or @classmethod so I had two choices1. The misleading simple function definition inside the class body, like:
class SomeClassBasedView(viewsets.ViewSet):
def last_modified_time(request):
return cache.get_or_set('key', some_date)
@method_decorator(last_modified(last_modified_time))
def get_some_object(self):
# expensive DB operation
The biggest problem with this that it can be confusing for our junior developers (might be even strange for seasoned Pythonistas).
Also an IDE (e.g. PyCharm) marks the request argument with a different color making it even more confusing.2. Define the function outside of the class body (where it doesn't belong...):
def last_modified_time(request):
return cache.get_or_set('key', some_date)
class SomeClassBasedView(viewsets.ViewSet):
@method_decorator(last_modified(last_modified_time))
def get_some_object(self):
# expensive DB operation In [1]: def my_decorator(f):
...: def wrapped(*args, **kwargs):
...: print("wrapping %s" % f.__name__)
...: return f(*args, **kwargs)
...: return wrapped
...:
In [2]: class Foo:
...: @classmethod
...: @my_decorator
...: def meth(cls, x, y):
...: return x + y
...:
In [3]: Foo.meth(1, 2)
wrapping meth
Out[3]: 3
This works because the user-defined decorator gets applied to the underlying function before it gets wrapped in a classmethod object (which doesn't supply the desired __name__ attribute).The signature introspection issue is real, and dealing with it in full generality requires a fair amount of work, especially if you want to be compatible with python 2 and python 3.
Why it must not change signatures? Everyone says that it shouldn't, but why? Maybe because named the same as Java/.Net decorator pattern? Decorators in Python is almost just a function composition and it can change signature.