Home > Net >  Why can we inherit `typing.NamedTuple`?
Why can we inherit `typing.NamedTuple`?

Time:09-04

After Python 3.6, we have typing.NamedTuple, which is a typed version of collections.namedtuple(), we can inherit it like a class:

class Employee(NamedTuple):
    name: str
    id: int

Compared with collections.namedtuple, this syntax is more beautiful, but I still can't understand its implementation, whether we look at typing.py file, or do some simple tests, we will find that it is a function rather than a class:

# Python 3.10.6 typing.py
def NamedTuple(typename, fields=None, /, **kwargs):
    """..."""
    if fields is None:
        fields = kwargs.items()
    elif kwargs:
        raise TypeError("Either list of fields or keywords"
                        " can be provided to NamedTuple, not both")
    try:
        module = sys._getframe(1).f_globals.get('__name__', '__main__')
    except (AttributeError, ValueError):
        module = None
    return _make_nmtuple(typename, fields, module=module)
>>> type(NamedTuple)
<class 'function'>

I understand that it uses some metaclass magic, but I don't understand what happens when using class MyClass(NamedTuple). For this reason, I have tried to customize a function to inherit:

>>> def func_for_inherit(*args, **kwargs):
...     print(args, kwargs)
...
>>> class Foo(func_for_inherit):
...     foo: str
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: function() argument 'code' must be code, not str

Well, this got a result that I can't understand. When inheriting a user-defined function, it seems that its class was called. What happened behind this?

CodePudding user response:

typing.NamedTuple uses a really esoteric feature, __mro_entries__:

If a base that appears in class definition is not an instance of type, then an __mro_entries__ method is searched on it. If found, it is called with the original bases tuple. This method must return a tuple of classes that will be used instead of this base. The tuple may be empty, in such case the original base is ignored.

Immediately after the NamedTuple function definition, the following code appears:

_NamedTuple = type.__new__(NamedTupleMeta, 'NamedTuple', (), {})

def _namedtuple_mro_entries(bases):
    if len(bases) > 1:
        raise TypeError("Multiple inheritance with NamedTuple is not supported")
    assert bases[0] is NamedTuple
    return (_NamedTuple,)

NamedTuple.__mro_entries__ = _namedtuple_mro_entries

This sets NamedTuple.__mro_entries__ to a function that tells the class creation system to use an actual class, _NamedTuple, as the base class. (_NamedTuple then uses metaclass features to customize the class creation process further, and the end result is a class that directly inherits from tuple.)

  • Related