Почему модуль namedtuple не использует метакласс для создания объектов класса nt?
Я потратил некоторое время на изучение модуля collections.namedtuple
несколько недель назад. Модуль использует функцию factory, которая заполняет динамические данные (имя нового класса namedtuple
и имена атрибутов класса) в очень большую строку. Затем exec
выполняется со строкой (которая представляет код) в качестве аргумента, и возвращается новый класс.
Кто-нибудь знает, почему это было сделано таким образом, когда есть конкретный инструмент для такого рода вещей, который легко доступен, т.е. метакласс? Я не пытался сделать это сам, но похоже, что все, что происходит в модуле namedtuple
, могло быть легко выполнено с использованием метакласса namedtuple
, например:
class namedtuple(type):
и т.д.
Ответы
Ответ 1
В проблеме 3974 есть несколько советов. Автор предложил новый способ создания названных кортежей, который был отклонен со следующими комментариями:
Кажется, преимущество исходной версии в том, что она быстрее, благодаря критическим методам жесткого кодирования. - Антуан Питроу
Нет ничего нечестивого в использовании exec. Более ранние версии использовали другие и они оказались излишне сложными и неожиданными проблемы. Это ключевая функция для названных кортежей, что они точно эквивалентно рукописному классу. - Раймонд Хеттингер
Кроме того, вот часть описания оригинального рецепта namedtuple
:
... рецепт превратился в его текущий стиль exec, где мы получаем все быстродействующего встроенного аргумента Python для проверки. Новый стиль здания и исполнение шаблона сделали как __new__, так и __repr__ функционирует быстрее и чище, чем в предыдущих версиях этого рецепта.
Если вы ищете альтернативные варианты:
Ответ 2
В качестве побочного элемента: Другим возражением, которое я вижу чаще всего при использовании exec
, является то, что некоторые местоположения (читые компании) отключили его по соображениям безопасности.
Помимо расширенных Enum
и NamedConstant
, библиотека aenum * также имеет NamedTuple
, который является metaclass
.
* aenum
написан автором Enum
и enum34
backport.
Ответ 3
Вот другой подход.
""" Subclass of tuple with named fields """
from operator import itemgetter
from inspect import signature
class MetaTuple(type):
""" metaclass for NamedTuple """
def __new__(mcs, name, bases, namespace):
cls = type.__new__(mcs, name, bases, namespace)
names = signature(cls._signature).parameters.keys()
for i, key in enumerate(names):
setattr(cls, key, property(itemgetter(i)))
return cls
class NamedTuple(tuple, metaclass=MetaTuple):
""" Subclass of tuple with named fields """
@staticmethod
def _signature():
" Override in subclass "
def __new__(cls, *args):
new = super().__new__(cls, *args)
if len(new) == len(signature(cls._signature).parameters):
return new
return new._signature(*new)
if __name__ == '__main__':
class Point(NamedTuple):
" Simple test "
@staticmethod
def _signature(x, y, z): # pylint: disable=arguments-differ
" Three coordinates "
print(Point((1, 2, 4)))
Если у этого подхода есть какое-либо достоинство, то это простота. Это было бы еще проще без NamedTuple.__new__
, который служит только для обеспечения принудительного подсчета количества элементов. Без этого он, к счастью, допускает дополнительные анонимные элементы после именованных, и основной эффект пропуска элементов - это IndexError
для пропущенных элементов при доступе к ним по имени (с небольшой работой, которая может быть переведена в AttributeError
). Сообщение об ошибке для неправильного количества элементов немного странно, но оно дает понять, в чем дело. Я не ожидал, что это будет работать с Python 2.
Есть место для дальнейших осложнений, таких как метод __repr__
. Я понятия не имею, как производительность сравнивается с другими реализациями (может помочь кэширование длины подписи), но я предпочитаю соглашение о вызовах по сравнению с собственной реализацией namedtuple
.
Ответ 4
Есть еще одна причина, по которой ни один из других ответов не встречается *.
Класс может иметь только 1 метакласс. Одна из причин этого заключается в том, что метакласс действует как фабрика, которая создает класс. Невозможно смешать фабрики вместе. Вы должны создать либо "комбинаторную фабрику", которая знает, как вызывать несколько фабрик в правильном порядке, либо "дочернюю фабрику", которая знает о "родительской фабрике" и использует ее правильно.
Если namedtuple
использует свой собственный метакласс, наследование с использованием любого другого метакласса будет namedtuple
:
>>> class M1(type): ...
...
>>> class M2(type): ...
...
>>> class C1(metaclass=M1): ...
...
>>> class C2(metaclass=M2): ...
...
>>> class C(C1, C2): ...
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
Вместо этого, если вы хотите иметь свой собственный метакласс и наследовать от класса namedtuple
, вам придется использовать какой-то так называемый метакласс namedtuple_meta
чтобы сделать это:
from namedtuple import namedtuple_meta # pretending this exists
class MyMeta(type): ...
class MyMetaWithNT(namedtuple_meta, MyMeta): ...
class C(metaclass=MyMetaWithNT): ...
... или просто наследовать пользовательский метакласс напрямую от namedtuple_meta
:
class MyMeta(namedtuple_meta): ...
class C(metaclass=MyMeta): ...
Поначалу это выглядит легко, но написание собственного метакласса, который хорошо сочетается с некоторым (сложным) метаклассом nt, может стать проблемой очень быстро. Это ограничение, вероятно, возникнет не часто, но достаточно часто, что будет препятствовать использованию namedtuple
. Так что, безусловно, преимущество заключается в том, чтобы все классы namedtuple
были type
type, и это устраняет сложность пользовательского метакласса.
Комментарий Раймонда Хеттингера намекает на это:
Для именованных кортежей ключевым моментом является то, что они в точности эквивалентны рукописному классу.