Python:在嵌套的namedtuple上替换属性的简单方法?

时间:2014-02-19 22:22:35

标签: python namedtuple

我正在使用嵌套的namedtuples创建一个数据结构(练习我的不可变函数编程技巧),但我很难找到一种简单的方法来替换嵌套的namedtuples中的值。

假设我有这样的数据结构:

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遍布各处。

有没有更简单的方法来解决这个问题?

3 个答案:

答案 0 :(得分:1)

抱歉,没有什么好方法可以做你想做的事 - 你的解决方案几乎是最好的解决方案。

确实糟透了,不要搞错,但据我所知,在即将发布的Python版本中没有改进计划。

老实说,如果你想要使用纯度和函数式编程结构,你应该看另一种语言(Clojure和Haskell是最好的候选者)。 Python并不能很好地强制实现不可变性和纯FP,核心开发人员根本不关心FP(至少就Python而言)。

答案 1 :(得分:1)

我已经创建了一个能够更清晰地处理这个问题的函数。它还兼作namedtuple._replace()的通用替代品。

Gist here,代码如下。

child参数是一个字符串,有点kludgy,但我无法想到解决这个问题,并且由于namedtuples已将其属性定义为字符串,因此它不是一种超级基础方法反正。

(至于这种困境是否只存在,因为Python对于不可变数据是不好的(因为Python没有针对函数式编程进行优化),请注意this StackOverflow answer表示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
    )

答案 2 :(得分:0)

元组是不可变的,因此您无法替换它们上的属性,也无法替换嵌套的元组。它们适用于创建不希望对其属性进行更改的对象。

>>> 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

要更改它的任何部分,您必须重建一个完整的新对象。