Setting a class __name__ declaratively

2020-03-18 21:07发布

问题:

Why can't you override a class name declaratively, e.g. to use a class name which is not a valid identifier?

>>> class Potato:
...     __name__ = 'not Potato'
...     
>>> Potato.__name__  # doesn't stick
'Potato'
>>> Potato().__name__  # .. but it's in the dict
'not Potato'

I thought maybe it was simply a case that this was overwritten after the class definition block completes. But seems that's not true, because the name is writable yet apparently not set in the class dict:

>>> Potato.__name__ = 'no really, not Potato'
>>> Potato.__name__  # works
'no really, not Potato'
>>> Potato().__name__  # but instances resolve it somewhere else
'not Potato'
>>> Potato.__dict__
mappingproxy({'__module__': '__main__',
              '__name__': 'not Potato',  # <--- setattr didn't change that
              '__dict__': <attribute '__dict__' of 'no really, not Potato' objects>,
              '__weakref__': <attribute '__weakref__' of 'no really, not Potato' objects>,
              '__doc__': None})
>>> # the super proxy doesn't find it (unless it's intentionally hiding it..?)
>>> super(Potato).__name__
AttributeError: 'super' object has no attribute '__name__'

Questions:

  1. Where does Potato.__name__ resolve?
  2. How is Potato.__name__ = other handled (inside and outside of a class definition block)?

回答1:

Where does Potato.__name__ resolve?

Most documented dunder methods and attributes actually exist in the native code side of the object. In the case of CPython, they are set as pointers in a slot in a C Struct defined in the object model. (defined here - https://github.com/python/cpython/blob/04e82934659487ecae76bf4a2db7f92c8dbe0d25/Include/object.h#L346 , but with fields easier to visualize when one actually creates a new class in C, like here: https://github.com/python/cpython/blob/04e82934659487ecae76bf4a2db7f92c8dbe0d25/Objects/typeobject.c#L7778 , where the "super" type is defined)

Therefore, __name__ is set there by the code in type.__new__, to which it is the first parameter.

How is Potato.__name__ = other handled (inside and outside of a class definition block)?

A class's __dict__ parameter is not a plain dictionary - it is an special mapping proxy object, and the reason for that is exactly so that all attribute settings on the class itself don't go through the __dict__, and instead go through the __setattr__ method in type. In there, assignments to these slotted dunder methods are actually filled in the C object's C structure, and then reflected on the class.__dict__ attribute.

So, outside a class block, cls.__name__ is set in this way - as it takes place after the class has been created.

Inside a class block, all attributes and methods are collected into a plain dict (though that can be customized). This dict is passed to type.__new__ and other metaclass methods - but as said above, this method fills in the __name__ slot from the explicit passed name parameter (that is, the "name" argument passed in the call to type.__new__)- even though it just updates the class __dict__ proxy with all names in the dict used as namespace.

That is why cls.__dict__["__name__"] can start with a different content from what is in the cls.__name__ slot, but subsequent assignments put both in sync.

An interesting anecdote is that three days ago I came across some code trying to reuse the __dict__ name explicitly in the class body, which has similarly puzzling side-effects. I even wondered whether there should be a bug report on that, and queried the Python developers - and as I had thought of, the authoritative answer was:

...all __dunder__ names are reserved for the implementation and they should
only be used according to the documentation. So, indeed, it's not illegal,
but you are not guaranteed that anything works, either.

(G. van Rossum)

And it applies just the same to trying to define __name__ in the class body.

https://mail.python.org/pipermail/python-dev/2018-April/152689.html


And if one actually wants to override __name__ as an attribute in the classbody, a metaclass for that is a simple as a metaclass can be:

class M(type):
    def __new__(metacls, name, bases, namespace, **kw):
         name = namespace.get("__name__", name)
         return super().__new__(metacls, name, bases, namespace, **kw)