Python:替换嵌套namedtuple上的属性的简单方法?
我正在用嵌套的namedtuples创建一个数据结构(练习我不变的函数编程技能),但我正在努力找到一种简单的方法来替换嵌套namedtuples中的值 假设我有这样一个数据结构:Python:替换嵌套namedtuple上的属性的简单方法?,python,namedtuple,Python,Namedtuple,我正在用嵌套的namedtuples创建一个数据结构(练习我不变的函数编程技能),但我正在努力找到一种简单的方法来替换嵌套namedtuples中的值 假设我有这样一个数据结构: from collections import namedtuple Root = namedtuple("Root", "inventory history") Inventory = namedtuple("Inventory", "item1 item2") Item = namedtuple("Item",
from collections import namedtuple
Root = namedtuple("Root", "inventory history")
Inventory = namedtuple("Inventory", "item1 item2")
Item = namedtuple("Item", "name num")
Event = namedtuple("Event", "action item num")
r = Root(
inventory=Inventory(
item1=Item(name="item1", num=1),
item2=Item(name="item2", num=2)
),
history=(
Event(action="buy", item="item1", num=1),
Event(action="buy", item="item2", num=2)
)
)
# Updating nested namedtuples is very clunky
num_bought = 4
r_prime = r._replace(
history = r.history + (Event(action="buy", item="item2", num=num_bought),),
inventory = r.inventory._replace(
item2 = r.inventory.item2._replace(
num = r.inventory.item2.num + num_bought
)
)
)
# Contrast with the ease of using a version of this based on mutable classes:
r.history += Event(action="buy", item="item2", num=num_bought),
r.inventory.item2.num += num_bought
如您所见,更改库存中某个项目的值是一件非常痛苦的事情,这要感谢a)被迫单独更新该值嵌套的所有层,b)无法访问诸如+=
之类的操作符
由于调用getattr
到处都是,如果我正在更新的库存中的物品是动态的,这会变得更加糟糕
有没有更简单的方法来处理这个问题?元组是不可变的,因此不能替换元组上的属性,也不能替换嵌套的属性。它们非常适合创建您不希望更改其属性的对象
>>> import collections
>>> MyTuple = collections.namedtuple('MyTuple', 'foo bar baz')
>>> t = MyTuple(MyTuple('foo', 'bar', 'baz'), 'bar', 'baz')
>>> t
MyTuple(foo=MyTuple(foo='foo', bar='bar', baz='baz'), bar='bar', baz='baz')
>>> isinstance(t, tuple)
True
如果您试图更改属性:
>>> t.baz = 'foo'
Traceback (most recent call last):
File "<pyshell#68>", line 1, in <module>
t.baz = 'foo'
AttributeError: can't set attribute
t.baz='foo'
回溯(最近一次呼叫最后一次):
文件“”,第1行,在
t、 baz=‘foo’
AttributeError:无法设置属性
要更改它的任何部分,您必须重新构建一个完整的新对象。对不起,没有好的方法可以实现您想要的功能—您的解决方案几乎是最好的 这确实很糟糕,毫无疑问,但据我所知,在即将发布的Python版本中并没有计划对其进行任何改进
老实说,如果你想玩弄纯粹性和函数式编程结构,你应该看看另一种语言(Clojure和Haskell是最好的选择)。Python不太适合强制不变性和纯FP,核心开发人员根本不关心FP(至少就Python而言)。我创建了一个函数,可以更清晰地处理这个问题。它还兼作
namedtuple.\u replace()
的通用替代品
,代码转载如下
child
参数是一个字符串,这有点麻烦,但我想不出解决方法,因为namedtuples已经将其属性定义为字符串,所以无论如何,这不是一种超基本的方法
(至于这一困境是否只是因为Python不适合使用不可变数据(因为Python没有针对函数式编程进行优化),请注意,这表明Haskell遇到了一个非常类似的问题,Haskell库提出的解决方案与我的Python解决方案在精神上类似。)
我会等待一点,以标记这是答案,给互联网一个机会,提供更优雅的东西
def attr_update(obj, child=None, _call=True, **kwargs):
'''Updates attributes on nested namedtuples.
Accepts a namedtuple object, a string denoting the nested namedtuple to update,
and keyword parameters for the new values to assign to its attributes.
You may set _call=False if you wish to assign a callable to a target attribute.
Example: to replace obj.x.y.z, do attr_update(obj, "x.y", z=new_value).
Example: attr_update(obj, "x.y.z", prop1=lambda prop1: prop1*2, prop2='new prop2')
Example: attr_update(obj, "x.y", lambda z: z._replace(prop1=prop1*2, prop2='new prop2'))
Example: attr_update(obj, alpha=lambda alpha: alpha*2, beta='new beta')
'''
def call_val(old, new):
if _call and callable(new):
new_value = new(old)
else:
new_value = new
return new_value
def replace_(to_replace, parts):
parent = reduce(getattr, parts, obj)
new_values = {k: call_val(getattr(parent, k), v) for k,v in to_replace.iteritems()}
new_parent = parent._replace(**new_values)
if len(parts) == 0:
return new_parent
else:
return {parts[-1]: new_parent}
if child in (None, ""):
parts = tuple()
else:
parts = child.split(".")
return reduce(
replace_,
(parts[:i] for i in xrange(len(parts), -1, -1)),
kwargs
)
为什么不使用字典或类?
nametuple
s不是适合您的数据容器。@jonrsharpe我正在尝试在这个项目上练习我的函数式编程技能,而不可变数据结构是函数式编程的核心组件。除此之外,如果可能的话,我希望受益于这样一个事实:不可变的数据结构非常清楚值更新的来源,有助于减少错误计数。是的,我理解元组是不可变的,更新它们的值实际上意味着创建一个新的元组。我的问题是,这样做的语法必须像我提供的那样难看吗?你必须构造一个完整的新对象,因此你必须遍历旧对象的属性,识别你要更改的属性,然后重新创建整个对象。这将是丑陋的。