3

This is extremely likely to be a duplicate of something, but my serach foo is failing me.

It is known that tuples are immutable, so you can't really change them. Sometimes, however, it comes in handy to do something to the effect of changing, say, (1, 2, "three") to (1, 2, 3), perhaps in a similar vein to the Haskell record update syntax. You wouldn't actually change the original tuple, but you'd get a new one that differs in just one (or more) elements.

A way to go about doing this would be:

elements = list(old_tuple)
elements[-1] = do_things_to(elements[-1])
new_tuple = tuple(elements)

I feel that changing a tuple to a list however kind of defeats the purpose of using the tuple type for old_tuple to begin with: if you were using a list instead, you wouldn't have had to build a throw-away list copy of the tuple in memory per operation.

If you were to change, say, just the 3rd element of a tuple, you could also do this:

def update_third_element(a, b, c, *others):
  c = do_things_to(c)
  return tuple(a, b, c, *others)

new_tuple = update_third_element(*old_tuple)

This would resist changes in the number of elements in the tuple better than the naive approach:

a, b, c, d, e, f, g, h, i, j = old_tuple
c = do_things_to(c)
new_tuple = (a, b, c, d, e, f, g, h, j, j) # whoops

...but it doesn't work if what you wanted to change was the last, or the n-th to last element. It also creates a throw away list (others). It also forces you to name all elements up to the n-th.

Is there a better way?

Community
  • 1
  • 1
badp
  • 11,409
  • 3
  • 61
  • 89
  • 3
    You say that "changing a tuple to a list however kind of defeats the purpose of using the tuple type for `old_tuple` to begin with". In fact, needing to change an element is what defeats the purpose of using the tuple type to begin with. If you find yourself needing to change elements of your tuples a lot, they probably shouldn't be tuples in the first place. If you don't need to do it that often, it's okay for it to be a bit ugly. – BrenBarn Apr 27 '14 at 22:07
  • @BrenBarn I don't agree that immutability (if you hold a reference to this object, it will never change) means you shouldn't want to take an object and make new one from it that differs slightly from the original. – badp Apr 27 '14 at 22:23
  • For those who are not convinced, the prime example of this is, of course, `str`s, which have a `replace` method "despite" being immutable. – badp Apr 27 '14 at 22:32

1 Answers1

3

I would use collections.namedtuple instead:

>>> from collections import namedtuple

>>> class Foo(namedtuple("Foo", ["a", "b", "c"])):
        pass

>>> f = Foo(1, 2, 3)  # or Foo(a=1, b=2, c=3)
>>> f._replace(a = 5)
Foo(a=5, b=2, c=3)

namedtuples also support indexing so you can use them in place of plain tuples.

If you must use a plain tuple, just use a helper function:

>>> def updated(tpl, i, val):
        return tpl[:i] + (val,) + tpl[i + 1:]

>>> tpl = (1, 2, 3)
>>> updated(tpl, 1, 5)
(1, 5, 3)
Erik Kaplun
  • 37,128
  • 15
  • 99
  • 111
  • 2
    +1: `_replace` is essentially the tuple method I'm looking for; too bad `tuple` doesn't actually have it for some reason. – badp Apr 27 '14 at 22:01