The __set_name__ method for descriptors

De­scrip­tors gen­er­al­ly have to in­ter­act with at­tributes of the man­aged ob­jec­t, and this is done by in­spect­ing __­dic­t__ on that ob­ject (or call­ing getat­tr/se­tat­tr, but the prob­lem is the same), and find­ing the key un­der­ the spe­cif­ic name.

For this rea­son, the de­scrip­tor will have to know the name of the key to look ­for, which is re­lat­ed to the name of the at­tribute is man­ag­ing.

On pre­vi­ous ver­sions of Python this had to be done ex­plic­it­ly. If we want­ed to ­work around it, there were some more ad­vanced ways to do so. Luck­i­ly, af­ter PEP-487 (added in Python 3.6), there are some en­hance­ments re­gard­ing class cre­ation, which al­so af­fect­s de­scrip­tors.

Let’s re­view the prob­lem, the pre­vi­ous ap­proach­es to tack­le it, and the mod­ern way of solv­ing it.

Configure the name of the descriptor

The de­scrip­tor needs to some­how know which at­tribute will be mod­i­fy­ing, and for this, the most com­mon so­lu­tion is to store the at­tribute name in­ter­nal­ly. For ex­am­ple in:

class LoggedAttr:
    def __init__(self, name=None):
        self.name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__[self.name]


class Managed:
    descriptor = LoggedAttr('descriptor')

What we re­quire is to check that the name is passed to the de­scrip­tor prop­er­ly, ba­si­cal­ly:

assert Managed.descriptor.name == 'descriptor'

But we don’t want to pass the string 'de­scrip­tor' as a pa­ram­e­ter when ­con­struct­ing it, be­cause it’s repet­i­tive. In­stead, we want this to be ­con­fig­ured au­to­mat­i­cal­ly. Let’s see some op­tion­s.

A class decorator

With a class dec­o­ra­tor, we could de­fine all dec­o­ra­tors for the class as ­pa­ram­e­ters of the dec­o­ra­tor, and make the as­sign­ment of the name in it as well.

Some­thing like this:

class configure_descriptors:
    def __init__(self, **kwargs):
        self.descs = {dname: dcls(dname) for dname, dcls in kwargs.items()}

    def __call__(self, class_):
        for dname, descriptor in self.descs.items():
            setattr(class_, dname, descriptor)
        return class_


@configure_descriptors(
    descriptor=LoggedAttr
)
class DecoratedManaged:
    """The descriptor is provided by the decorator"""

The con­di­tion is pre­served:

assert DecoratedManaged.descriptor.name == 'descriptor'

In this decorator, we provide the name and the class of the descriptor to be created, and the decorator instantiates the class with this name. We could also have created the instance directly in the descriptor, and then update the value with setattr(descriptor, 'name', dname), which is more general, in case you want to create descriptors that take multiple arguments on their __init__ method, but for this case it’s just fine.

Then we set the new de­scrip­tor (the one that has the name al­ready up­dat­ed on it), to the wrapped class.

How­ev­er, it still seems a bit un­fa­mil­iar or coun­ter-in­tu­itive that we’re defin­ing the de­scrip­tor not in the body of the class, but as a pa­ram­e­ter of a dec­o­ra­tor.

There must be an­oth­er way.

A meta-class

Imag­ine we flag the class by adding a __set_­name = True at­tribute on it, in or­der to hint the meta-­class that this is go­ing to be one of the at­tributes that need its name changed. Then the meta-­class would look some­thing like:

class MetaDescriptor(type):
    def __new__(cls, clsname, bases, cls_kwargs):
        for attrname, cls_attr in cls_kwargs.items():
            mangled_attr = "_{0}__set_name".format(cls_attr.__class__.__name__)
            if hasattr(cls_attr, mangled_attr):
                setattr(cls_attr, 'name', attrname)
        return super().__new__(cls, clsname, bases, cls_kwargs)


class MetaManaged(metaclass=MetaDescriptor):
    descriptor = LoggedAttr()

And again:

assert MetaManaged.descriptor.name == 'descriptor'

One de­tail is that the __init__ of the de­scrip­tor ac­cepts the name to be nul­lable so this work­s. An­oth­er op­tion would have been defin­ing on­ly the de­scrip­tor as­signed to the class, and then, re-map­ping the at­tribute with the in­stance, pass­ing the name when it’s be­ing con­struct­ed on the meta-­class. Both­ op­tions are the same, and the ex­am­ple was made with sim­plic­i­ty in mind.

This works but it has a cou­ple of is­sues. First we have to some­how iden­ti­fy when the class at­tribute needs to be up­dat­ed (in this case, a flag was added to it, but oth­er al­ter­na­tives are no bet­ter at al­l). The sec­ond prob­lem should be rather ob­vi­ous: it’s not a good use of meta-­class­es, and this is overkill (to say the least) for what should be a sim­ple task.

There must be a bet­ter way.

__set_name__

And there is. At least for Python 3.6 and high­er. The __set_­name__ method­ was in­clud­ed, which is au­to­mat­i­cal­ly called when the class is be­ing cre­at­ed, and it re­ceives two pa­ram­e­ter­s: the class and the name of the at­tribute as it ap­pears de­fined in the class.

With this, the prob­lem is re­duced to just sim­ply:

class LoggedAttr:
    ...
    def __set_name__(self, owner, name):
        self.name = name

And that’s it, no oth­er code is need­ed. The so­lu­tion is much sim­pler, and it en­tails less prob­lem­s.

Ac­tu­al­ly, I de­lib­er­ate­ly named the flag __set_­name, to get an idea of what’s com­ing, and to hint that with __set_­name__, Python must be do­ing ­some­thing sim­i­lar to the ex­am­ple, but in this case we should­n’t wor­ry about it.

Conclusion

Even though it’s fine to just know about the last method, and we could sim­ply use that, it’s still im­por­tant to have fol­lowed this path, think­ing about how things were done pre­vi­ous­ly, be­cause it’s not fair to just as­sume things were al­ways good, and take that for grant­ed. Oth­er­wise, we would miss the evo­lu­tion of the lan­guage, and as­sume there were nev­er is­sues, prob­lems or things that need­ed re­vi­sion.

And more im­por­tant­ly, there still are. Python still has lots of oth­er ar­eas for im­prove­men­t. Just as in this ex­am­ple __set_­name__ seems to solve a smal­l­, yet an­noy­ing prob­lem, there are many oth­er sce­nar­ios on which things are not crys­tal clear in Python, so the lan­guage still needs to evolve.