python 的 NamedTuple 返回结构是少数应该使用可变默认值的地方之一吗?

Posted

技术标签:

【中文标题】python 的 NamedTuple 返回结构是少数应该使用可变默认值的地方之一吗?【英文标题】:Are python's NamedTuple return structures one of the few places where mutable defaults should be used? 【发布时间】:2020-10-29 06:18:12 【问题描述】:

从 python 函数返回结构的方法已在各种帖子中详细讨论过。两个不错的here 和here。

但是,除非我错过了,否则建议的解决方案都没有在设置其成员的同一位置定义结构,而是重复分配的成员列表(不是 DRY)或依赖位置(容易出错)。

我正在寻找一种 DRY 方法来执行此操作,既可以提高写作速度,又可以避免重复自己时常见的参数错位错误。

以下代码 sn-p 显示了执行此操作的三种尝试。为简洁起见,示例的结构仅包含一个元素,但其意图显然是结构包含多个元素。

这三个方法都是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 是一种明智的 Python 方法吗?

一个问题是可变默认值在某种程度上是一种禁忌,尤其是对于函数参数。但是,我想知道它们是否可以在这里使用,因为附加的代码表明这些NamedTuple 默认值(可能还有整个ReturnType 定义)在每个函数调用上都会被评估,这与我认为的函数参数默认值相反只评估一次并永远存在(因此问题)。

另一个问题是 dataclasses 模块似乎已经不遗余力地明确禁止这种用法。在这种情况下,这个决定是否过于教条?还是有必要防范方法 2?

这样效率低吗?

如果方法 2 的语法意味着:

1 - 仅在第一次通过时定义 ReturnType 一次

2 - 在每次传递时调用__init__() 并使用给定的(动态设置的)初始化

但是,恐怕它可能意味着以下内容:

1 - 定义 ReturnType 及其每次传递的默认值

2 - 在每次传递时调用__init__() 并使用给定的(动态设置的)初始化

当调用处于“紧密”循环中时,是否应该担心每次传递都重新定义大块 ReturnTypes 的效率低下?每当在函数中定义类时,这种低效率是否会出现?类应该在函数内部定义吗?

有没有一种(希望是好的)方法来使用新的dataclasses 模块(python 3.7)实现 DRY 定义实例化?

最后,有没有更好的 DRY 定义-实例化语法?

【问题讨论】:

是的,效率低下。每次调用其中一个函数时,您都在创建一个全新的类型。 @chepner 谢谢。与使用所有键值对创建 dict 或 SimpleNamespace 相比,您是否知道额外的开销? @OldSchool:一个快速测试表明,每次创建一个新的 namedtuple 类型比使用单个 namedtuple 类型慢大约 220 倍,并且占用大约 45 倍的空间。请参阅 ideone.com/TVGklL 和 ideone.com/zqjqXk。 (由于缓冲,输出顺序有点奇怪。) 【参考方案1】:

但是,恐怕它可能意味着以下内容:

1 - 定义 ReturnType 及其每次传递的默认值

2 - 在每次传递时调用__init__() 并使用给定的(动态设置的)初始化

就是这个意思,而且要耗费大量的时间和空间。此外,它会使您的注释无效 - -> 'ReturnType' 需要模块级别的 ReturnType 定义。它还会破坏酸洗。

坚持使用模块级ReturnType,不要使用可变默认值。或者,如果您只想通过点符号访问成员,并且您并不真正关心创建有意义的类型,则只需使用 types.SimpleNamespace

return types.SimpleNamespace(thing=whatever, other_thing=stuff)

【讨论】:

感谢 SimpleNamespace 的建议。,我从你的回答中得知,如果你想让 ReturnType 从 NamedTuple 或数据类功能中受益,你不相信有一个好的 DRY 方法吗?另一个注意事项。我的印象是,另一个函数 def 中的函数 def 仅在第一遍处理时才被处理。那是对的吗?如果是这样,为什么每次调用都要处理类定义?这样做是有目的的还是只是一个实现的怪癖? @OldSchool:每次调用外部函数时,也会重新执行嵌套函数定义。函数定义和类语句在 Python 中都是必不可少的。 原谅我的无知。由于 %autoreload 以交互方式使用以确保在调试时重新解析对象定义,我一直在做出未受过教育的假设,即解释器在第一次遇到对象定义时解析并存储它们,然后在下一次将它们藏起来供重复使用。这就是我如何合理化使用 %autoreload 来改变这种行为。我猜您是说解释器没有这样做,而是在找到时重新创建所有内容?它重新创建了上面的嵌套类。我是否正确理解? %autoreload 有什么用? @OldSchool:大多数代码不会在每次想要创建实例时重新定义它使用的每个类。您的代码重新执行类语句,因为类语句在函数内部。大多数代码不会将类语句放在函数定义中。 有趣。我确实在封装函数中看到了很多带有函数定义的代码(与封装函数中的类 def 相反)每次调用封装函数时是否都会解析这些嵌套函数定义?

以上是关于python 的 NamedTuple 返回结构是少数应该使用可变默认值的地方之一吗?的主要内容,如果未能解决你的问题,请参考以下文章

Python数据结构与算法---namedtuple

Python数据结构与算法---namedtuple

python 命名元组(namedtuple)

namedtuple工厂函数精讲

Python namedtuple 命名元组

namedtuple工厂函数,创造一个像实例对象的元祖(感觉到了Python的奇妙与可爱之处)。