Python: adding descriptor with setattr doesn't call __set_name__
Asked Answered
O

2

8

This question probably has an answer somewhere, but I couldn't find it.

I'm using setattr to add a long list of descriptors to a class, and I have discovered that __set_name__ of the descriptor is not called when I use setattr.

It is not a big issue for me because I can just set the name of the descriptor through __init__ instead, I just want to understand why __set_name__ is not called and if it's because I'm doing something wrong.

class MyDescriptor:
    def __set_name__(self, owner, name):
        self._owner = owner
        self._name = name

    def __get__(self, instance, owner):
        return self._name


class MyClass:
    a = MyDescriptor()


setattr(MyClass, 'b', MyDescriptor())  # dynamically adding descriptor, __set_name__ is not called!

mc = MyClass()

print(mc.a)  # prints 'a'
print(mc.b)  # raises AttributeError: 'MyDescriptor' object has no attribute '_name'
Operation answered 5/8, 2022 at 23:36 Comment(2)
Relevant section of the docs docs.python.org/3/howto/… - Since the update logic is in type.__new__(), notifications only take place at the time of class creation. If descriptors are added to the class afterwards, __set_name__() will need to be called manually Entrance
@IainShelvington awesome thx for digging that out for me!Operation
C
6

__set_name__ is called for each descriptor when the class is created in the type.__new__ function: it is not a "reactive protocol", so to say, to be triggered whenever a descriptor is created.

The plain fix to that is to call __set_name__ manually just after you call setattr:

descr_name = 'b'

setattr(MyClass, descr_name, MyDescriptor()) 
getattr(MyClass, 'b').__set_name__(MyClass, descr_name)

Although it is possible to have a metaclass with a custom __setattr__ method that will do that for you. __setattr__ method implementation in your classes is Pythons way of adding reactive behavior to attribute setting - its only that this time you need the behavior in a class, instead of in an instance.

If you do this in a lot of places, and really would like it to be transparent it could be a thing to do:

class Meta(type):
    def __setattr__(cls, attr, value):
        super().__setattr__(attr, value)
        if (setname:=getattr(value, "__set_name__", None) ) and callable(setname):
            setname(cls, attr)

class MyClass(metaclass=Meta):
    a = MyDescriptor()

And now, checking it on the interactive prompt:


In [83]: setattr(MyClass, "b", MyDescriptor())

In [84]: MyClass.b
Out[84]: 'b'

In [85]: MyClass.__dict__["b"]._name
Out[85]: 'b'
Cameral answered 7/9, 2022 at 14:11 Comment(0)
I
1

After setattr(...) you can access the instance of the descriptor by using vars() or MyClass.__dict__.

If you are just adding one descriptor you can use the following:

setattr(MyClass, 'b', MyDescriptor())
vars(MyClass)['b'].__set_name__(MyClass, 'b')

If you are adding multiple descriptors try using a function:

def add_descriptor(cls, attr_name, descriptor):
    setattr(cls, attr_name, descriptor)
    vars(cls)[attr_name].__set_name__(cls, attr_name) 

add_descriptor(MyClass, 'b', MyDescriptor)
Impaste answered 3/11, 2023 at 15:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.