Reputation:
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'
Upvotes: 8
Views: 667
Reputation: 11
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)
Upvotes: 1
Reputation: 110561
__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'
Upvotes: 6