Foreword
Thanks to all those who gave me valuable feedbacks to my other proposal of Make name lookups follow MRO within a class definition, I am now convinced that in trying to bring the mental model of a subclass consistent across attribute lookups of a class object and name lookups within a class body, the proposed approach of redefining a class scope would indeed create too much potential for compatibility issues given the maturity of Python.
Special thanks to @Gouvernathor, who suggested the idea of making super()
work in a class definition. I had considered the possibility before making that proposal, since it was also suggested in the popular StackOverflow question, but decided at the time that making a bare name lookup follow MRO would be the cleaner approach.
Now onto the actual proposal.
Motivation
The current usage of super()
requires a class object, either passed in as the first argument or obtained through __class__
in the current namespace. This prevents the usage of super()
during the execution of a class body, where the class object is not yet created.
So a subclass designed as an extension to a base class can have methods that call super()
to gain easy access to attributes of the base classes, while within the class definition itself one needs to explicitly reference the name of the base class in order to access its attributes:
class Base:
__slots__ = 'foo',
def __init__(self, foo):
self.foo = foo
class Child(Base):
__slots__ = Base.__slots__ + ('bar',)
def __init__(self, foo, bar):
super().__init__(foo)
self.bar = bar
This requires a breaking of the DRY rule, hindering any change in class name, to quote the Rationale of PEP-3135:
The goal of this proposal is to generalize the usage of super()
so it can intuitively work in a class definition like it does in a method:
class Base:
__slots__ = 'foo',
def __init__(self, foo):
self.foo = foo
class Child(Base):
__slots__ = super().__slots__ + ('bar',)
def __init__(self, foo, bar):
super().__init__(foo)
self.bar = bar
Research
Despite the current super()
requiring a class object to work, we can see from the implementation of its attribute getter descriptor _super_lookup_descr
that all it really wants from a given class object is just its MRO:
And the MRO of a class is currently obtained by calling the mro
method of the class, defined in its metaclass:
Similarly, the actual implemetation of the mro
method, mro_implementation_unlocked
, needs only the bases from a given class object:
The Proposal
From the research above, it is now apparent we can bypass super()
's requirement of a class object with the following implementation.
First, let the class builder make the calculated MRO available as __mro__
to the namespace in which the class body executes. The MRO is calculated based solely on the given bases, without a class object. More on that in the second point that follows.
Note that the actual implementation should insert __mro__
only if the parser finds any call to super()
in the class body, just like how the parser only inserts __class__
to the namespace of a method if it finds the use of super()
in the method:
import builtins
from types import resolve_bases, _calculate_meta
def build_class(func, name, *bases, metaclass=NewType, **kwargs):
metaclass = _calculate_meta(metaclass, bases)
namespace = metaclass.__prepare__(name, bases, **kwargs)
resolved_bases = resolve_bases(bases)
namespace['__mro__'] = metaclass.mro(resolved_bases)
exec(func.__code__, globals(), namespace)
if resolved_bases != bases:
namespace['__orig_bases__'] = bases
del namespace['__mro__']
return metaclass(name, resolved_bases, namespace)
builtins.__build_class__ = build_class
Secondly, make the type.mro
method test if the sole argument it gets is a class, in which case it will do what it currently does by getting the bases from class; otherwise the first argument is treated as bases for the rest of MRO calculations.
Since there is no separately exposed API from type.mro
with just the MRO calculation logics, the following Python equivalent implementation for illustration purposes creates an intermediate class from the given base classes and calls its mro
method in order to emulate calling type.mro
with a sequence of bases. The actual implementation should be to make mro_implementation_unlocked
take a PyObject *cls_or_bases
instead, and treat cls_or_bases
as a class and call lookup_tp_bases(type)
only if it passes a type check, or else assign it directly to bases
:
class NewType(type):
def mro(cls_or_bases):
if isinstance(cls_or_bases, type):
return super().mro()
return __class__('_TempClass', cls_or_bases, {}).mro()
Finally, make the no-argument form of super()
test if there is __mro__
in the namespace after failing to find __class__
in the namespace. If there is, meaning that it’s called from a class body, use __mro__
as MRO instead of calculating it from a class object.
Since the current implementation of super.__init__
looks up locals
directly from the current frame and therefore can’t be called from an overriding method of a subclass, the following Python implementation for emulation purposes skips the conditional statement that checks for the presence of __class__
, and includes only the fallback logics to handle __mro__
for a class defintion, by again creating an intermediate class so to call the current super().__init__
with a class object. The actual implementation should be a conditional statement within the _super_lookup_descr
function to use __mro__
instead of lookup_tp_mro(su_obj_type)
:
import sys
class class_super(super):
def __init__(self):
cls = type('_TempClass', tuple(sys._getframe(1).f_locals['__mro__']), {})
super().__init__(cls, cls)
so that:
class Base:
__slots__ = 'foo',
def __init__(self, foo):
self.foo = foo
class Child(Base):
__slots__ = class_super().__slots__ + ('bar',)
def __init__(self, foo, bar):
super().__init__(foo)
self.bar = bar
print(Child.__slots__) # outputs ('foo', 'bar')
child = Child(1, 2)
print(child.foo) # outputs 1
print(child.bar) # outputs 2
Demo: mlEiPT - Online Python3 Interpreter & Debugging Tool - Ideone.com
Backward Compatibility
Maintainers of metaclasses with a custom mro
method defined should be advised to make mro
check the type of the first argument, and use it directly as bases for MRO calculations if it isn’t a class, if their metaclass is to support this feature.
Performance Impact
The existing code base is minimally affected performance-wise in the additional check for the presence of super()
in class defintions that the parser will be performing, and in the additional type check of the first argument to type.mro
.