Python: Easy way to replace attribute on nested namedtuple?

冷暖自知 提交于 2021-01-28 07:07:53

问题


I'm creating a data structure with nested namedtuples (practicing my immutable functional programming skills), but am struggling to find an easy way to replace values in nested namedtuples.

Let's say I have a data structure like this:

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

As you can see, changing the value for an item in the inventory is quite a pain, thanks to a) being forced to individually update all of the layers the value is nested under, and b) not having access to operators like +=.

This gets even uglier if the item in my inventory I'm updating is dynamic, thanks to calls to getattr being strewn everywhere.

Is there an easier way to handle this?


回答1:


Sorry, there's no nice way to do what you want -- your solution is pretty much the nicest one around.

It does suck, make no mistake, but as far as I can tell there are no improvements to that planned in upcoming releases of Python.

Honestly, if you want to play around with purity and functional programming constructs, you should look at another language (Clojure and Haskell are the best candidates for that). Python doesn't lend itself too well to enforced immutability and pure FP, and the core developers don't care for FP at all (at least as far as Python is concerned).




回答2:


I've created a function that will handle this problem a bit more cleanly. It also doubles as a general-purpose replacement for namedtuple._replace().

Gist here, code reproduced below.

The child parameter is a string, which is kinda kludgy, but I couldn't think of a way around that, and since namedtuples already have their attributes defined as strings, it's not a super off-base approach anyway.

(As for whether this dilemma only exists because Python is bad with immutable data (since Python isn't optimized for functional programming), please note that this StackOverflow answer indicates Haskell suffers from a very similar problem, and the Haskell library suggested arrived at solution that resembles my Python solution in spirit.)

I'll wait bit to mark this as the answer, to give the Internet a chance to offer something more elegant.

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
    )



回答3:


Tuples are immutable, therefore you can't replace attributes on them, and you can't replace nested ones. They're good for creating objects that you don't want changes to be made to their attributes.

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

And if you attempt to change an attribute:

>>> t.baz = 'foo'

Traceback (most recent call last):
  File "<pyshell#68>", line 1, in <module>
    t.baz = 'foo'
AttributeError: can't set attribute

To change any part of it, you'll have to reconstruct an entire new object.



来源:https://stackoverflow.com/questions/21893843/python-easy-way-to-replace-attribute-on-nested-namedtuple

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!