Есть ли разница между типами, созданными с помощью namedtuple и NamedTuple?

Сравниваем типы, созданные с использованием typing.NamedTuple и collections.namedtuple. Хотя получившийся код будет работать во всех случаях одинаково, есть несколько нюансов, о которых стоит знать.

Согласно документации к модулю typing, можно сказать, что два приведенных ниже фрагмента кода эквивалентны:

from typing import NamedTuple

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

а также:

from collections import namedtuple

Employee = namedtuple('Employee', ['name', 'id'])

Действительно ли реализации namedtuple и NamedTuple не отличаются?

Тип, созданный подклассом от typing.NamedTuple, эквивалентен типу, возвращаемому функцией collections.namedtuple, однако в первом случае дополнительно создаются атрибуты __annotations__, _field_types и _field_defaults. На практике получившийся код будет работать во всех случаях одинаково, потому что в Python в настоящее время ничто не оказывает влияние на эти атрибуты, связанные с типизацией (ваша IDE может их использовать). Создание именованных кортежей с помощью typing-модуля позволяет разработчику использовать более естественный декларативный интерфейс:

  • Можно легко указать значения по умолчанию для полей.
  • Не нужно повторять имя типа дважды (Employee).
  • Можно напрямую модифицировать тип (например, добавить строку документации или какие-то методы).

Пользовательский класс в этом случае останется подклассом tuple (не будет являться подклассом NamedTuple), а его экземпляры будут экземплярами tuple:

>>> class Employee(NamedTuple):
...     name: str
...     id: int
...     
>>> issubclass(Employee, NamedTuple)
False
>>> isinstance(Employee(name='guido', id=1), NamedTuple)
False

Почему так происходит? typing.NamedTuple — это класс, который использует метаклассы и кастомизированный интерфейс __new__ для управления аннотациями и затем делегирует collections.namedtuple создание и возврат типа. Обратите внимание: название collections.namedtuple определено в нижнем регистре. Это не тип или класс, а фабричная функция. Она работает следующим образом: создается строка из исходного Python-кода, а затем вызывается exec для нее. Получившийся конструктор вытягивается из пространства имен и включается в трехаргументный вызов метакласса type, чтобы создать и вернуть пользовательский класс. Этим объясняется странное наследование (Employee не является подклассом NamedTuple, см. выше), NamedTuple использует метакласс, который задействует другой метакласс для создания экземпляра.


Спасибо wim.