Why can we inherit `typing.NamedTuple`?
Asked Answered
L

1

5

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?

Levorotatory answered 3/9, 2022 at 16:17 Comment(4)
@azro I know it's not a class, but I wonder how it behaves like a class?Levorotatory
FYI, collections.namedtuple is also a functionCletis
@RafazZ: It is, but you can't do class Whatever(collections.namedtuple): and have that work.Monjo
Oh I see what you mean. TBH, I look at the collections.namedtuple as a factory; while I see the typing.NamedTuple as a class that the said factory generates (or an instance of it). I know that this is not technically true, but that's how my brain is dealing with ignorance :)Cletis
M
8

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.)

Monjo answered 3/9, 2022 at 16:28 Comment(1)
Well, it seems that I only focus on the function itself and forget to look around it. The magic of all this seems to have nothing to do with the function itself but lies in this __mro_entries__.Levorotatory

© 2022 - 2024 — McMap. All rights reserved.