Des­crip­tors are one of the most po­wer­ful fea­tu­res of Py­tho­n. The rea­son wh­y
­the­y’­re so po­wer­ful is be­cau­se they ena­ble us to con­trol the co­re ope­ra­tion­s
(­ge­t, se­t, de­le­te) 1, of an attri­bu­te in a gi­ven ob­jec­t, so that we can hook
a par­ti­cu­lar co­de, con­tro­lled by us, in or­der to mo­di­fy, chan­ge, or ex­tend the
o­ri­gi­nal ope­ra­tio­n.

A descriptor is an object that implements either __get__,
__set__, or __delete__.

We’­ll un­ders­tand be­tter what the pa­ra­me­ters mean, on­ce we’­ve seen so­me exam­ple­s
of des­crip­tors and how the­y’­re us­e­d.

How to use them

In or­der to use des­crip­tors we need at least two cla­sses: one for the
­des­crip­tor itsel­f, and the cla­ss that is going to use the des­crip­tor ob­jec­ts
(o­ften re­fe­rred to as the ma­na­ged cla­ss).

Getting Data

Con­si­der this ba­sic exam­ple on whi­ch I ha­ve a fic­tio­nal ma­na­ger for vi­deo­
ou­tpu­t, that can hand­le mul­ti­ple de­vi­ce­s. Ea­ch de­vi­ce is set wi­th a par­ti­cu­la­r
­re­so­lu­tio­n, pro­vi­ded by a use­r. Ho­we­ve­r, if for so­me rea­son one of the de­vi­ce­s
­does not ha­ve a ren­de­ring re­so­lu­tion se­t, we want to use a de­fault one,
s­pe­ci­fied on the cla­ss de­fi­ni­tio­n.

In this case resolution is a descriptor that implements only
__get__(). If an instance of the display manager, has a resolution
set, it will retrieve just that one. On the other hand, if it does not, then
when we access one of the class attributes like media.tv, what actually
happens is that Python calls:

VideoDriver.tv.__get__(media, VideoDriver)

Which executes the code in the __get__() method of the descriptor,
which in this case returns the default value, previously passed.

When the des­crip­tor is ca­lled from the cla­ss, and not the ins­tan­ce, the va­lue
of the pa­ra­me­ter “ins­tan­ce” is No­ne, but the “o­w­ne­r” is sti­ll a re­fe­ren­ce to­
­the cla­ss being in­vo­ked (tha­t’s pro­ba­bly one of the rea­sons why the­se are two­
se­pa­ra­te pa­ra­me­ter­s, ins­tead of just let the user de­ri­ve the cla­ss from the
ins­tan­ce, it allo­ws even mo­re fle­xi­bi­li­ty).

For this rea­so­n, is co­m­mon to hand­le this ca­se, and re­turn the des­crip­to­r
i­tsel­f, whi­ch is the ra­tio­na­le be­hind the li­ne:

ifinstanceisNone:returnself

That is why when you de­fi­ne a pro­per­ty in a cla­ss, and ca­ll it from an ins­tan­ce
ob­jec­t, you’­ll get the re­sult of the com­pu­ta­tion of the me­tho­d. Ho­we­ve­r, if
­you ca­ll the pro­per­ty from the cla­ss, you get the pro­per­ty ob­jec­t.

Setting Data

Example: imagine we want to have some attributes in an object that are going to
be traced, by other attributes that keep track, of how many times their values
changed. So, for example, for every attribute <x> on the object, there would
be a corresponding count_<x> one, that will keep count of how many times x
changed its value. For simplicity let’s assume attributes starting with
count_<name>, cannot be modified, and those only correspond to the count of
attribute <name>.

There may be several ways to address this problem. One way could be overriding
__setattr__(). Another option, could be by the means of properties
(getters and setters) for each attribute we want to track. Or, we can use descriptors.

Both the properties, and __setattr__() approaches, might be subject to
code repetition. Their logic should be repeated for several different
properties, unless a property function builder is created (in order to reuse
the logic of the property across several variables). As per the
__setattr__() strategy, if we need to use this logic in multiple
classes we would have to come up with some sort of mixin class, in order to
achieve it, and if one of the classes already overrides this method, things
might get overcomplicated.

The docstring on the Traveller class, pretty much explains its intended
use. The important thing about this, is the public interface: it’s absolutely
transparent for the user. An object that interacts with a Traveller
instance, gets a clean interface, with the properties exposed, without having
to worry about the underlying implementation.

So, we have two classes, with different responsibilities, but related, because
they interact towards a common goal. Traveller has two class attributes
that, are objects, instances of the descriptor.

Now le­t’s take a look at the other si­de of it, the in­ter­nal wo­rking of the ­des­crip­to­r.

Un­der this sche­ma, Py­thon wi­ll trans­la­te a ca­ll like:

traveller=Traveller()traveller.city='Stockholm'

To the one using the __set__ method in the descriptor, like:

Traveller.city.__set__(traveller,'Stockholm')

Which means that the __set__ method on the descriptor is going to receive
the instance of the object being accessed, as a first parameter, and then the
value that is being assigned.

Mo­re ge­ne­ra­lly we could say that so­me­thing like:

obj.<descriptor>=<value>

Trans­la­tes to:

type(obj).__set__(obj,<value>)

Wi­th the­se two pa­ra­me­ter­s, we can ma­ni­pu­la­te the in­te­rac­tion any way we wan­t,
whi­ch makes the pro­to­col rea­lly po­wer­fu­l.

In this example, we are taking advantage of this, by querying the original
object’s attribute dictionary (instance.__dict__), and getting the
value in order to compare it with the newly received one. By reading this
value, we calculate another attribute which will hold the count of the number
of times the attribute was modified, and then, both of them are updated in
the original dictionary for the instance.

An im­por­tant con­cept to point out is that this im­ple­men­ta­tion not on­ly wo­rks,
­but it al­so sol­ves the pro­blem in a mo­re ge­ne­ric fas­hio­n. In this exam­ple, it
was the ca­se of a tra­ve­lle­r, of whom we wanted to know how many ti­mes chan­ge­d
of lo­ca­tio­n, but the exact sa­me ob­ject could be us­ed for exam­ple to mo­ni­to­r
­ma­rket sto­cks, va­ria­bles in an equa­tio­n, etc. This ex­po­ses func­tio­na­li­ty as a
­sort of li­bra­r­y, toolki­t, or even fra­mewo­rk. In fac­t, many we­ll-k­no­wn
­fra­mewo­rks in Py­thon use des­crip­tors to ex­po­se their API.

Deleting Data

The __delete__() method is going to be called when an instruction of
the type del <instance>.<descriptor> is executed. See the following example.

In this exam­ple, we just want a pro­per­ty in the ob­jec­t, that can­not be de­le­te­d,
and des­crip­tor­s, agai­n, pro­vi­de one of the mul­ti­ple po­s­si­ble im­ple­men­ta­tion­s.

Caveats and recommendations

Re­­me­m­­ber that des­­cri­p­­tors should alwa­­ys be us­ed as cla­ss attri­­bu­­tes.

Da­ta should be sto­red in ea­ch ori­gi­nal ma­na­ged ins­tan­ce, ins­tead of doin­g
­da­ta bookkee­ping in the des­crip­to­r. Ea­ch ob­ject should ha­ve its da­ta in its
__­dic­t__.

Pre­ser­ve the abi­li­ty of ac­ce­s­sing the des­crip­tor from the cla­ss as we­ll, no­t
on­ly from ins­tan­ce­s. Mind the ca­se when ins­tan­ce is No­ne, so it can
­be ca­lled as ty­pe(ins­tan­ce).­des­crip­tor.

Do not ove­rri­de __­ge­ta­ttri­bu­te__(), or they mi­ght lo­se effec­t.

Food for thought

Descriptors provide a framework for abstracting away repetitive access logic.
The term framework here is not a coincidence. As the reader might have
noticed, by using descriptors, there is an inversion of control (IoC) on
the code, because Python will be calling the logic we put under the descriptor
methods, when accessing these attributes from the managed instance.

Un­der this con­si­de­ra­tions it is co­rrect to thi­nk that it be­ha­ves as a ­fra­mewo­rk.

Summary

Des­crip­tors pro­vi­de an API, to con­trol the co­re ac­ce­ss to an ob­jec­t’s da­ta
­mo­de­l, at its lo­w-­le­vel ope­ra­tion­s. By means of des­crip­tors we can con­trol the
exe­cu­tion of an ob­jec­t’s in­ter­fa­ce, be­cau­se they pro­vi­de a trans­pa­rent la­ye­r
­be­tween the pu­blic in­ter­fa­ce (what is ex­po­sed to user­s), and the in­ter­na­l
­re­pre­sen­ta­tion and sto­ra­ge of da­ta.

They are one of the most powerful features of Python, and their possibilities
are virtually unlimited, so in this post we’ve only scratched the surface of
them. More details, such as exploring the different types of descriptors with
their internal representation or data, the use of the new __set_name__
magic method, their relation with decorators, and analysis of good
implementations, are some of the topics for future entries.