You are on page 1of 2

Better Unbound Python Descriptors

In this post, a developer goes over the basics of using descriptors in your Python
code.

Welcome back from another hiatus! This post is a facepalm post because I recently
realized that I've been an idiot for so long. I have a tendency to make things more
complicated than they need to be, as can be seen in my articles about instance
properties.

I've briefly mentioned unbound attributes ( Class.attr returns a function that you
pass an instance into to look up the the value of that instance's version of the
attribute) with descriptors a time or two and they always ended up using a whole
new object to represent the unbound attribute. In the example given, I returned a
local function to use as the unbound attribute; in the descriptor-tools library
that goes along with the book, I implemented it with an UnboundAttribute type,
which allowed it to easily carry extra data (such as the descriptor object
reference); then I discovered attrgetter in the operator module, so I substituted
that in instead. But there was one big obvious solution I was missing.

Some Background
When implementing the __get__() method of a descriptor, the convention was to
always return the descriptor itself if an instance was not given. When I started
espousing using unbound attributes instead, I always had one caveat: since the
convention for so long has been to return the descriptor, it can go against the
Principle of Least Astonishment to return something else. So, I always advised
using it with a grain of salt.

But I myself didn't care; I had no real use for returning the descriptor. Really,
the only thing that bugged me was that we had to create an object that had barely
any use. I really love the style of having lots of small classes doing their things
and letting the runtime largely deal with the repercussions, but it always hurt a
little bit inside, since this object seemed like a waste.

Good News!
Well, after all these years, I've finally realized how much of an idiot I am and
that none of these issues have to be issues at all!

The solution? Return the descriptor and give it a __call__() method that takes in
the instance and delegates to the __get__() method, as shown:

class MyDescriptor:
def __init__(self, �):
...
def __call__(self, instance):
return self.__get__(instance)
def __get__(self, instance, owner=None):
if instance is None:
return self
else:
...
This also finally gave me a good excuse for using the default value of None for the
owner parameter!

But...
There is still one caveat to this version of an unbound attribute. If the
descriptor has another use for __call__(), then using it for this either requires
changing the unbound attribute to return some other implementation (one that allows
the user to get access to the descriptor, or else having the __call__() method is
useless) or you'll have to use a normal method instead of __call__() for that use
case.

Outro
As always, the KISS principle (Keep It Simple, Stupid) prevails. Not only is it
simpler in almost every way, but it even makes it so you can ignore all the likely
problems.

You might also like