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__()
并使用给定的(动态设置的)初始化
当调用处于“紧密”循环中时,是否应该担心每次传递都重新定义大块 ReturnType
s 的效率低下?每当在函数中定义类时,这种低效率是否会出现?类应该在函数内部定义吗?
有没有一种(希望是好的)方法来使用新的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 返回结构是少数应该使用可变默认值的地方之一吗?的主要内容,如果未能解决你的问题,请参考以下文章