12

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?

GIZ
  • 4,409
  • 1
  • 24
  • 43

1 Answers1

8

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.

user2357112
  • 260,549
  • 28
  • 431
  • 505
  • One question about the second step you mentioned. Let's say `Klass` started out initially without the `__contains__`. and `__contains__` was assigned after the class was created.if I assign to a class like: `Klass.__contains__ = ...`, `PyObject_GenericSetAttr` will update `Klass.__dict__` and `update_slot` will update the slot of `__contain__`? This also means `__contains__` slot for `Klass` was initially `Null` before assignment? – GIZ May 02 '17 at 19:48
  • 1
    @direprobs: `update_slot` will set the slot corresponding to `__contains__`. That slot is `->tp_as_sequence->sq_contains`, and more than just `sq_contains` being null, it might not have even existed yet, since `tp_as_sequence` itself could have been null. (None of this should ever be important to you unless you need to work with the C API.) – user2357112 May 02 '17 at 19:53