Являются ли возвращаемые структуры Python NamedTuple одним из немногих мест, где следует использовать изменяемые значения по умолчанию?

Методы возврата структур из функций Python подробно обсуждались в различных сообщениях. Два хороших здесь и здесь.

Однако, если я не пропустил это, ни одно из предложенных решений не определяет структуру в том же месте, где установлены ее элементы, и вместо этого либо повторяют список элементов при назначении (не DRY), либо полагаются на позицию (подвержено ошибкам).

Я ищу СУХОЙ способ сделать это как для скорости записи, так и для того, чтобы избежать ошибок несовпадения аргументов, распространенных, когда вы повторяетесь.

В приведенном ниже фрагменте кода показаны три попытки сделать это. Для краткости структура примера содержит только один элемент, но намерение, очевидно, состоит в том, чтобы структуры содержали несколько элементов.

Эти три метода являются СУХИМИ, встраивая определение структуры с инициализацией возвращаемого экземпляра.

Метод 1 подчеркивает потребность в лучшем способе, но иллюстрирует искомый синтаксис DRY, где структура и способ ее заполнения (решается во время выполнения) находятся в одном и том же месте, а именно в вызове dict().

Метод 2 использует typing.NamedTuple и, кажется, работает. Однако для этого используются изменяемые значения по умолчанию.

Метод 3 следует подходу метода 2, используя dataclasses.dataclass, а не typing.NamedTuple. Это терпит неудачу, потому что первый явно запрещает изменяемые значения по умолчанию, повышая ValueError: mutable default is not allowed

from collections import namedtuple
from dataclasses import dataclass
from typing import NamedTuple, List, Tuple

# Method 1
def ret_dict(foo_: float, bar_: float) -> Tuple:
    return_ = dict(foo_bar=[foo_, bar_])
    _ = namedtuple('_', return_.keys())
    return _(*return_.values())


# Method 2
def ret_nt(foo_: float, bar_: float) -> 'ReturnType':
    class ReturnType(NamedTuple):
        foo_bar: List[float] = [foo_, bar_]     # Mutable default value allowed
    return ReturnType()


# Method 3
def ret_dc(foo_: float, bar_: float) -> 'ReturnType':
    @dataclass
    class ReturnType:
        foo_bar: List[float] = [foo_, bar_]   # raises ValueError: mutable default is not allowed
    return ReturnType()


def main():
    rt1 = ret_dict(1, 0)
    rt1.foo_bar.append(3)
    rt2 = ret_dict(2, 0)
    print(rt1)
    print(rt2)

    rt1 = ret_nt(1, 0)
    rt1.foo_bar.append(3)   # amending the mutable default does not affect subsequent calls
    rt2 = ret_nt(2, 0)
    print(rt1)
    print(rt2)

    rt1 = ret_dc(1, 0)
    rt1.foo_bar.append(3)  # amending the default does not affect subsequent calls
    rt2 = ret_dc(2, 0)
    print(rt1)
    print(rt2)


if __name__ == "__main__":
    main()

Возникают следующие вопросы:

Является ли метод 2 разумным питоническим подходом?

Одна из проблем заключается в том, что изменяемые значения по умолчанию являются своего рода табу, особенно для аргументов функций. Однако мне интересно, допустимо ли их использование здесь, учитывая, что прилагаемый код предполагает, что эти значения по умолчанию NamedTuple (и, возможно, все определение ReturnType) оцениваются при каждом вызове функции, в отличие от значений аргументов функции по умолчанию, которые, как мне кажется, оцениваются только один раз и сохраняться вечно (отсюда и проблема).

Еще одна проблема заключается в том, что модуль dataclasses, похоже, явно запретил такое использование. Было ли это решение чрезмерно догматичным в данном случае? или защита от метода 2 оправдана?

Это неэффективно?

Я был бы счастлив, если бы синтаксис метода 2 означал:

1 - определить ReturnType один раз только при первом проходе

2 - вызывать __init__() с заданной (динамически заданной) инициализацией на каждом проходе

Однако я боюсь, что вместо этого это может означать следующее:

1 - Определить ReturnType и его значения по умолчанию при каждом проходе

2 - вызывать __init__() с заданной (динамически установленной) инициализацией на каждом проходе

Следует ли беспокоиться о неэффективности переопределения коротких ReturnType на каждом проходе, когда вызов зациклен? Разве эта неэффективность не проявляется всякий раз, когда класс определяется внутри функции? Должны ли классы определяться внутри функций?

Есть ли способ (надеюсь, хороший) для создания экземпляра определения DRY с использованием нового модуля dataclasses (python 3.7)?

И наконец, есть ли лучший синтаксис определения DRY?


person OldSchool    schedule 08.07.2020    source источник
comment
Да, это неэффективно. Вы создаете совершенно новый тип каждый раз, когда вызываете одну из функций.   -  person chepner    schedule 08.07.2020
comment
@chepner Спасибо. Есть ли у вас ощущение дополнительных накладных расходов по сравнению с обычно наблюдаемым созданием dict или SimpleNamespace со всеми парами ключ-значение?   -  person OldSchool    schedule 09.07.2020
comment
@OldSchool: быстрый тест показывает, что создание нового типа namedtuple каждый раз происходит примерно в 220 раз медленнее и занимает примерно в 45 раз больше места, чем использование одного типа namedtuple. См. ideone.com/TVGklL и ideone.com/zqjqXk. (Порядок вывода немного странный из-за буферизации.)   -  person user2357112 supports Monica    schedule 09.07.2020


Ответы (1)


Однако я боюсь, что вместо этого это может означать следующее:

1 — определить ReturnType и его значения по умолчанию при каждом проходе

2 - вызывать __init__() с заданной (динамически заданной) инициализацией на каждом проходе

Вот что это значит, и это стоит много времени и места. Кроме того, это делает ваши аннотации недействительными - -> 'ReturnType' требует определения ReturnType на уровне модуля. Это также нарушает травление.

Придерживайтесь уровня модуля ReturnType и не используйте изменяемые значения по умолчанию. Или, если все, что вам нужно, это доступ к членам с помощью записи через точку, и вы действительно не заботитесь о создании осмысленного типа, просто используйте types.SimpleNamespace:

return types.SimpleNamespace(thing=whatever, other_thing=stuff)
person user2357112 supports Monica    schedule 08.07.2020
comment
Спасибо за предложение SimpleNamespace. Я понял из вашего ответа, что вы не верите, что существует хороший СУХОЙ способ сделать это, если вы хотите, чтобы ReturnType извлекал выгоду из функциональности NamedTuple или класса данных? На другой заметке. У меня сложилось впечатление, что функция def внутри другой функции def обрабатывается только при первом проходе. Это правильно? Если да, то почему определения классов обрабатываются при каждом вызове? Есть ли для этого цель или это просто особенность реализации? - person OldSchool; 09.07.2020
comment
@OldSchool: определение вложенной функции также выполняется повторно каждый раз при вызове внешней функции. Определения функций и операторы классов являются обязательными в Python. - person user2357112 supports Monica; 09.07.2020
comment
Простите мое невежество. Из-за магии %autoreload, используемой в интерактивном режиме для обеспечения повторного анализа определений объектов при отладке, я сделал необоснованное предположение, что интерпретатор анализирует и сохраняет определения объектов при первом их обнаружении, а в следующий раз держит их в рукаве. для повторного использования. Вот как я рационализирую использование %autoreload для изменения этого поведения. Я полагаю, вы говорите, что интерпретатор этого не делает, а вместо этого воссоздает все, что находит? Он воссоздает вложенные классы выше. Я правильно понял? для чего нужна %autoreload? - person OldSchool; 09.07.2020
comment
@OldSchool: большая часть кода не переопределяет каждый класс, который он использует, каждый раз, когда он хочет создать экземпляр. Ваш код повторно выполняет операторы класса, потому что оператор класса находится внутри функции. В большинстве кодов операторы класса не помещаются в определения функций. - person user2357112 supports Monica; 09.07.2020
comment
Интересно. Однако я вижу много кода с определениями функций внутри инкапсулирующих функций (в отличие от определения класса внутри инкапсулирующей функции). Анализируются ли эти вложенные определения функций каждый раз, когда вызывается инкапсулирующая функция? - person OldSchool; 10.07.2020
comment
@OldSchool: они выполняются повторно, создавая новый объект функции. Они не анализируются повторно. - person user2357112 supports Monica; 10.07.2020
comment
(Это намного дешевле, чем повторное выполнение определения класса, но не бесплатно.) - person user2357112 supports Monica; 10.07.2020
comment
Я отметил это как ответ, поскольку он кажется довольно правдоподобным. Покупатель, будьте осторожны, я не проверял его точность - person OldSchool; 29.07.2020