2014-02-19 10 views
1

Я создаю структуру данных с вложенными namedtuples (практикуя свои неизменные навыки функционального программирования), но изо всех сил пытаюсь найти простой способ заменить значения во вложенных namedtuples.Python: простой способ заменить атрибут на вложенном namedtuple?

Скажем, у меня есть структура данных, как это:

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 

Как вы можете видеть, изменяя значение для элемента в инвентаре является довольно боль, благодаря а) вынуждены индивидуально обновить все уровней, вложенных в значение, и b) не имеющих доступа к операторам, например +=.

Это становится еще более уродливым, если элемент в моем инвентаре, который я обновляю, является динамичным, благодаря звонкам на getattr, разбросанных повсюду.

Есть ли более простой способ справиться с этим?

+0

Почему бы не использовать словари или классы? – jonrsharpe

+1

'nametuple' просто не правильный контейнер данных для вас. – roippi

+0

@jonrsharpe Я пытаюсь практиковать свои навыки функционального программирования в этом проекте, а неизменные структуры данных являются основным компонентом функционального программирования. Помимо этого, я хотел бы извлечь выгоду, если это возможно, из того факта, что неизменные структуры данных очень четко показывают, откуда возникают обновления ценности, помогая сократить количество ошибок. – spiffytech

ответ

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 

Чтобы изменить какую-либо часть его, вам придется восстановить весь новый объект.

+0

Да, я понимаю, что кортежи неизменяемы, и что обновление их значений на самом деле представляет собой создание нового кортежа. Мой вопрос: синтаксис для этого должен быть таким же уродливым, как то, что я представил? – spiffytech

+0

Вам нужно будет построить целый новый объект, поэтому вам придется пройти атрибуты старого объекта и определить атрибут, который вы собираетесь изменить, и воссоздать все это. Это будет уродливо. –

1

Извините, нет отличного способа сделать то, что вы хотите - ваше решение в значительной степени самое приятное.

Это делает сосать, не ошибитесь, но насколько я могу судить, в предстоящих выпусках Python никаких улучшений не планируется.

Честно говоря, если вы хотите поиграть с конструкциями чистоты и функционального программирования, вы должны посмотреть на другой язык (Clojure и Haskell - лучшие кандидаты для этого). Python не слишком хорошо справляется с принудительной неизменностью и чистым FP, а основные разработчики вообще не заботятся о FP (по крайней мере, в Python).

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 
    )