views:

242

answers:

6

I'm looking for a concise and functional style way to apply a function to one element of a tuple and return the new tuple, in Python.

For example, for the following input:

inp = ("hello", "my", "friend")

I would like to be able to get the following output:

out = ("hello", "MY", "friend")

I came up with two solutions which I'm not satisfied with.

One uses a higher-order function.

def apply_at(arr, func, i):
    return arr[0:i] + [func(arr[i])] + arr[i+1:]

apply_at(inp, lambda x: x.upper(), 1)

One uses list comprehensions (this one assumes the length of the tuple is known).

[(a,b.upper(),c) for a,b,c in [inp]][0]

Is there a better way? Thanks!

+2  A: 
>>> inp = "hello", "my", "friend"
>>> index = 1
>>> inp[:index] + ( str.upper(inp[index]),) + inp[index + 1:]
('hello', 'MY', 'friend')

Seems simple, the only thing you may need to know is that to make a single element tuple, do (elt,)

hlfrk414
+2  A: 

Maybe some' like this?

>>>inp = ("hello", "my", "friend")
>>>out =  tuple([i == 1 and x.upper() or x for (x,i) in zip(t,range(len(t)))])

>>> out
('hello', 'MY', 'friend')

Note: rather than (x,i) in zip(t, range(len(t))) I should have thought of using the enumerate function : (i,x) in enumerate(t)

Making it a bit more general:
Rather than hard-coding the 1, we can place it in a variable.
Also, by using a tuple for that purpose, we can apply the function to elements at multiple indexes.

>>>inp = ("hello", "my", "friend")
>>>ix  = (0,2)
>>>out =  tuple([i in ix and x.upper() or x for (i, x) in enumerate(t)])

>>> out
('HELLO', 'my', 'FRIEND')

Also, we can "replace" the zip()/enumerate() by map(), in something like

out = tuple(map(lambda x,i : i == 1 and x.upper() or x, inp, range(len(inp)) ) )

Edit: (addressing comment about specifying the function to apply):
Could be something as simple as:

>>> f = str.upper  # or whatever function taking a single argument
>>> out = tuple(map(lambda x,i : i == 1 and f(x) or x, inp, range(len(inp)) ) )

Since we're talking about applying any function, we should mention the small caveat with the condition and if_true or if_false construct which is not exactly a substitute for the if/else ternary operator found in other languages. The limitation is that the function cannot return a value which is equivalent to False (None, 0, 0.0, '' for example). A suggestion to avoid this problem, is, with Python 2.5 and up, to use the true if-else ternary operator, as shown in Dave Kirby's answer (note the when_true if condition else when_false syntax of this operator)

mjv
Pretty nice use of logical "and" and "or"! A way to specify the function to apply is also needed. One way is to specify a function as parameter but this forces the user to use an anonymous function (lambda) if the function to apply is a method...
Mathieu
Dave Kirby
@Dave Kirby: thanks for the hint! I knew about enumerate, but not of the if-else ternary operator in 2.5+. I'll use this operator more often now, especially that this probably doesn't suffer the caveat I mention (re. False values). Let me +1 yr answer for teaching me this new trick!
mjv
+5  A: 

Here is a version that works on any iterable and returns a generator:

>>> inp = ("hello", "my", "friend")
>>> def apply_nth(fn, n, iterable):
...    return (fn(x) if i==n else x for (i,x) in enumerate(iterable))
... 
>>> tuple(apply_nth(str.upper, 1, inp))
('hello', 'MY', 'friend')

You can extend this so that instead of one position you can give it a list of positions:

>>> def apply_at(fn, pos_lst, iterable):
...    pos_lst = set(pos_lst)
...    return (fn(x) if i in pos_lst else x for (i,x) in enumerate(iterable))
... 
>>> ''.join(apply_at(str.upper, [2,4,6,8], "abcdefghijklmno"))
'abCdEfGhIjklmno'
Dave Kirby
Your solution is very elegant and Pythonic. Thanks!
Mathieu
A: 

I don't understand if you want to apply a certain function to every element in the tuple that passes some test, or if you would like it to apply the function to any element present at a certain index of the tuple. So I have coded both algorithms:

This is the algorithm (coded in Python) that I would use to solve this problem in a functional language like scheme:

This function will identify the element identifiable by id and apply func to it and return a list with that element changed to the output of func. It will do this for every element identifiable as id:

def doSomethingTo(tup, id):
    return tuple(doSomethingToHelper(list(tup), id))

def doSomethingToHelper(L, id):
    if len(L) == 0:
        return L
    elif L[0] == id:
        return [func(L[0])] + doSomethingToHelper(L[1:], id)
    else:
        return [L[0]] + doSomethingToHelper(L[1:], id)


This algorithm will find the element at the index of the tuple and apply func to it, and stick it back into its original index in the tuple

def doSomethingAt(tup, i): 
    return tuple(doSomethingAtHelper(list(tup), i, 0))

def doSomethingAtHelper(L, index, i):
if len(L) == 0: 
        return L
elif i == index: 
        return [func(L[0])] + L[1:]
else: 
        return [L[0]] + doSomethingAtHelper(L[1:], index, i+1)
inspectorG4dget
This would be massively inefficient in Python - If you had a list of length N it would create and destroy N lists, each of length N. It would also recurse to a depth of N, which may exceed the python stack limit.
Dave Kirby
@Dave Kirby: user189637 did ask for a functional approach. Most of what I saw was not functional, hence my solution. As for creating and destroying N many lists, that's what I learned functional programming does. Further, functional programming is meant to be run in a parallel computing environment, therefore it will be inefficient in a serial computing environment.
inspectorG4dget
+1  A: 

I commented in support of your first snippet, but here are a couple other ways for the record:

(lambda (a,b,c): [a,b.upper(),c])(inp)

(Won't work in Python 3.x.) And:

[inp[0], inp[1].upper(), inp[1]]
Darius Bacon
Given that what I needed this for is to take the a,b,c returned by a function and return a,b,c with b transformed, I guess your lambda solution is close to the spirit of what I wanted.It would be nice to have syntactic sugar such a: return inp as a, b.upper(), cThe reason I was not completely satisfied with my original apply_at function is that the transformation I want to apply exists only as a method, forcing me to use an anonymous function, unlike the list-comprehension solution.Your second solution seems to require an intermediate variable for inp if it's not in its own function.
Mathieu
Would the following work in Python 3.x? (lambda a,b,c: [a,b.upper(),c])(*inp)
Mathieu
Yes, it should. OK, how I would handle your problem as described: with two statements: a, b, c = inp; return a, b.upper(), c. It's unfortunate that Python isn't expression-oriented like, say, Scheme, but that's how it's meant to be written.
Darius Bacon
A: 

i also like the answer that Dave Kirby gave. however, as a public service announcement, i'd like to say that this is not a typical use case for tuples -- these are data structures that originated in Python as a means to move data (parameters, arguments) to and from functions... they were not meant for the programmer to use as general array-like data structures in applications -- this is why lists exist. naturally, if you're needing the read-only/immutable feature of tuples, that is a fair argument, but given the OP question, this should've been done with lists instead -- note how there is extra code to either pull the tuple apart and put the resulting one together and/or the need to temporarily convert to a list and back.

wescpy
I chose tuple on purpose. As I commented in Darius Bacon's solution, the tuple that I want to transform originates from a return in a function.
Mathieu
I agree, my original question should have been more detailed. Sorry, this is my first question on stackoverflow.
Mathieu