type.__setattr__
is used for classes, basically instances of metaclasses. object.__setattr__
on the other hand, is used for instances of classes. This is totally understood.
I don't see a significant difference between the two method, at least at Python level, I notice the two use the same procedures for attribute assignment, correct me if I'm wrong:
Suppose a
is an instance of a user-defined class, just a normal class:
class A:
pass
a = A()
a.x = ...
then a.x = ..
invokes type(a).__setattr__(...)
which performs the following steps:
Note: type(a).__setattr__
will find __setattr__
in object
builtin class
1) Look for a data descriptor in type(a).__mro__
.
2) If a data descriptor was found, call its __set__
method and exit.
3) If no data descriptor was found in type(a).__mro__
, then add attribute to a.__dict__
, a.__dict__['x'] = ...
With classes--instances of metaclasses, the process is similar:
class A(metaclass=type):
pass
then: A.x = ...
is translated to type(A).__setattr__(...)
which performs the following steps:
Note: type(A).__setattr__
will find __setattr__
in type
builtin class
1) Look for a data descriptor in type(A).__mro__
2) If a data descriptor was found, call its __set__
method and exit.
3) If no data descriptor was found in type(A).__mro__
, then add attribute to A.__dict__
, a.__dict__['x'] = ...
But object.__setattr__
doesn't work for classes:
>>> object.__setattr__(A, 'x', ...)
TypeError: can't apply this __setattr__ to type object
and vice versa, type.__setattr__
doesn't work for instances of A
:
>>> type.__setattr__(A(), 'x', ...)
TypeError: descriptor '__setattr__' requires a 'type' object but received a 'A'
Hmmm! There must be something different between the two methods. This is subtle, but true nonetheless!
Presumably the two methods perform the same steps inside __setattr__
, what is the difference between type.__setattr__
and object.__setattr__
so that type.__setattr__
is limited to classes and object.__setattr__
is limited to instances of classes?
type.__setattr__
has a check to prevent setting attributes on types like int
, and it does a bunch of invisible cleanup that isn't needed for normal objects.
Let's take a look under the hood! Here's type.__setattr__
:
static int
type_setattro(PyTypeObject *type, PyObject *name, PyObject *value)
{
if (!(type->tp_flags & Py_TPFLAGS_HEAPTYPE)) {
PyErr_Format(
PyExc_TypeError,
"can't set attributes of built-in/extension type '%s'",
type->tp_name);
return -1;
}
if (PyObject_GenericSetAttr((PyObject *)type, name, value) < 0)
return -1;
return update_slot(type, name);
}
and if we examine PyBaseObject_Type
, we see it uses PyObject_GenericSetAttr
for its __setattr__
, the same call that appears halfway through type_setattro
.
Thus, type.__setattr__
is like object.__setattr__
, but with some additional handling wrapped around it.
First, the if (!(type->tp_flags & Py_TPFLAGS_HEAPTYPE))
check prohibits attribute assignment on types written in C, like int
or numpy.array
, because assigning attributes on those can seriously screw up the Python internals in ways someone unfamiliar with the C API might not expect.
Second, after the PyObject_GenericSetAttr
call updates the type's dict or calls an appropriate descriptor from the metaclass, update_slot
fixes up any slots affected by the attribute assignment. These slots are C-level function pointers that implement functionality like instance allocation, in
checks, +
, deallocation, etc. Most of them have corresponding Python-level methods, like __contains__
or __add__
, and if one of those Python-level methods is reassigned, the corresponding slot (or slots) have to be updated, too. update_slot
also updates slots on all descendants of the class, and it invalidates entries in an internal attribute cache used for type object attributes.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With