type.__setattr__
is used for classes, basically instances of metaclasses. object.__setattr__
on the other hand, is used for instances of classes. This
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.