The best thing about this job is moments like these. When the problem at hand allows you to delve even further into the recesses of the language you are currently using.
According to the official documentation, the definition of a decorator is:
A function returning another function, usually applied as a function transformation using the @wrapper syntax. Common examples for decorators are
Which often precedes code examples like this one (to increase the readability I have added some naïve implementation of static method below):
def staticmethod(original_func): def wrapper(*args, **kwargs): # pre func code result = original_func() # post func code return result return wrapper def f(...): ... f = staticmethod(f) @staticmethod def f(...): ...
Up to this point, there’s nothing unusual or hard to follow here. Decorators are just high-level functions.
But what if you want to use a decorator on an instance method? And what if you decide to use a class as a decorator? Consider: you have a certain set of classes, used for access filtering that you want to use for specific methods exclusively.
Our class in an original form could look more or less like this:
class AccountActive(): def apply(self): # For the sake of simplicity I omit the body of this method # Treat this method as some kind of before_filter # for checking if account is active pass
So, what happens when you use our class as a decorator then? Let’s start with defining our example method. For the purposes of this post, I will use a straightforward case which is the resource endpoint in Flask:
from flask.views import MethodView class Vehicles(MethodView): def get(self): # Find a specific vehicle and return it pass
With the decorator applied, it will not look very different:
class Vehicles(API): @AccountActive def get(self): # Find a specific vehicle and return it pass
Keeping in mind what we have read in the definition of decorator presented above, the class
AccountActive will be used in this way:
get = AccountActive(get)
So, we see that our original
get function will be used as an argument of a constructor. We should update our class then:
class AccountActive(): def __init__(self, func): self.func = func def apply(self): pass
What next? Obviously, our new
get method is an instance of the
AccountActive class and should be callable as such. However, there still is nothing that could surprise us here:
class AccountActive(): def __init__(self, func): self.func = func def __call__(self, *args, **kwargs): # This is the place where we apply our before_filter logic self.apply() return self.func(*args, **kwargs) def apply(self): pass
This should do the job, right? Job’s done? Well, not exactly. Let’s have a look at what REPL tells us when we use this code:
>>> resource = Vehicles() >>> resource.get() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 6, in __call__ TypeError: get() missing 1 required positional argument: 'self'
Why is that so, you ask. Haven’t we transferred all arguments inside our wrapper? Yes, we have, but let’s step back and check the definition of decorator. It says that a decorator is applied to the function object, not the bound method (you can read more about the differences between these two concepts on my colleague’s blog post A valid method signature?). Which means we lost our instance argument here, and should restore it in order to be able to use our filters.
There are several solutions, but we will focus on the one which justifies the presence of the descriptor in the post’s title.
First things first – let’s see what a descriptor is:
Any object which defines the methods get(), set(), or delete(). When a class attribute is a descriptor, its special binding behavior is triggered upon attribute lookup. Normally, using a.b to get, set or delete an attribute looks up the object named b in the class dictionary for a, but if b is a descriptor, the respective descriptor method gets called. Understanding descriptors is a key to a deep understanding of Python because they are the basis for many features including functions, methods, properties, class methods, static methods, and reference to super classes.
get() method looks promising, let’s dive a little bit deeper into the documentation:
Called to get the attribute of the owner class (class attribute access) or of an instance of that class (instance attribute access). The optional owner argument is the owner class, while instance is the instance that the attribute was accessed through, or None when the attribute is accessed through the owner.
This method should return the computed attribute value or raise an AttributeError exception.
PEP 252 specifies that get() is callable with one or two arguments. Python’s own built-in descriptors support this specification; however, it is likely that some third-party tools have descriptors that require both arguments. Python’s own getattribute() implementation always passes in both arguments whether they are required or not.
So, after applying a decorator we no longer have a bound method, but a simple function. Let’s now try to restore it, using the descriptor concept.
Given the above, we can expect that – after we transform our
AccountActive filter into a descriptor – during the attribute lookup on the
Vehicles object level we will hit a get method with some handy arguments.
First, let’s make our
AccountActive a descriptor (more specifically a non-data descriptor, because we will implement the get method only):
class AccountActive(): def __init__(self, func): self.func = func def __call__(self, *args, **kwargs): self.apply() return self.func(*args, **kwargs) def __get__(self, instance, owner=None): # self is instance of our AccountActive class # instance is the instance of our Vehicles class (the place the lookup starts from) # Now we return a new function with restored # instance argument at the first position using a partial helper return partial(self.__call__, instance) def apply(self): pass
This time REPL keeps silent, and our tests are all green.
We used the descriptor concept along with a partial helper to create a bound method. This way we have our missing positional argument back. Nevertheless, the descriptor has way more to offer, than you could initially think. I encourage you to explore its abilities on the Descriptor HowTo Guide.