1

我正在创建一个带有嵌套命名元组的数据结构(练习我不可变的函数式编程技能),但我正在努力寻找一种简单的方法来替换嵌套命名元组中的值。

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

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到处都是呼吁。

有没有更简单的方法来处理这个?

4

3 回答 3

2

我创建了一个函数,可以更干净地处理这个问题。它还兼作namedtuple._replace().

要点在这里,代码复制如下。

参数是一个字符串,这child有点笨拙,但我想不出办法解决这个问题,而且由于 namedtuples 已经将它们的属性定义为字符串,所以无论如何这不是一个超级离基的方法。

(至于这种困境是否仅因为 Python 不具备不可变数据而存在(因为 Python 没有针对函数式编程进行优化),请注意这个 StackOverflow 答案表明 Haskell 遇到了非常相似的问题,建议的 Haskell 库得到了解决方案这在精神上类似于我的 Python 解决方案。)

我会稍等片刻将其标记为答案,让 Internet 有机会提供更优雅的东西。

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
    )
于 2014-02-22T05:40:10.817 回答
1

抱歉,没有很好的方法来做你想做的事——你的解决方案几乎是最好的解决方案。

确实很糟糕,毫无疑问,但据我所知,在即将发布的 Python 版本中没有对计划进行改进。

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

于 2014-02-19T22:29:47.113 回答
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

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

于 2014-02-19T22:25:15.663 回答