在Python中将函数应用于列表中的一个元素

5
我希望以简明、有效的方式将函数应用于元组的一个元素并返回新的元组,在Python中。
例如,对于以下输入:
inp = ("hello", "my", "friend")

我希望能够获得以下输出结果:
out = ("hello", "MY", "friend")

我想到了两种解决方案,但都不太满意。

其中一种使用高阶函数。

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

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

使用列表推导式(假设元组长度已知)。

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

有更好的方法吗?谢谢!

1
你可以将元组转换为(可变)列表,更改其第n个元素,如果需要,再转换回元组。但是,如果您只需要遍历新元组,为什么不创建一个生成器,它简单地产生每个元素,除了第i个元素,其中它会产生func(element)?这只是一个想法。 - Attila O.
当然,我可以只做mylist[idx] = func(mylist[idx]),但我想要一个函数式的一行解决方案,这样我就可以在return语句中使用它。 - Mathieu
你的第一个解决方案清晰、简洁、直截了当。如果这让你感到困扰,那就说明它解决了错误的问题。(顺便说一句,arr[0:i] 可以简写为 arr[:i]。) - Darius Bacon
6个回答

7
这里有一个适用于任何可迭代对象并返回生成器的版本:
>>> 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')

您可以将其扩展,以便不仅可以给出一个位置,还可以给出位置列表:
>>> 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'

你的解决方案非常优雅和Pythonic。谢谢! - Mathieu

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

看起来很简单,你需要知道的唯一一件事是,要创建一个仅包含一个元素的元组,需要使用(elt,)的格式。


2
也许是这样的吗?
>>>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')

注意:与其使用(x,i) in zip(t, range(len(t))),我应该考虑使用enumerate函数:(i,x) in enumerate(t) 使其更加通用: 不要硬编码1,我们可以将其放在一个变量中。 此外,通过使用元组来实现这个目的,我们可以将函数应用于多个索引处的元素。
>>>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')

此外,我们可以使用map()来“替换”zip()/enumerate(),例如:
out = tuple(map(lambda x,i : i == 1 and x.upper() or x, inp, range(len(inp)) ) )

编辑:(回应有关指定要应用的函数的评论):
可以简单地做成这样:

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

既然我们正在谈论应用任何函数,我们应该提到condition and if_true or if_false结构中的一个小细节,它并不完全替代其他语言中的if/else三元运算符。其限制是函数不能返回等同于False的值(例如None、0、0.0、'')。为避免此问题,建议使用Python 2.5及以上版本中的真正的if-else三元运算符,如Dave Kirby的答案所示(请注意此运算符的when_true if condition else when_false语法)。


相当不错地运用了逻辑“与”和“或”!还需要一种指定要应用的函数的方法。一种方法是将函数作为参数来指定,但这会强制用户使用匿名函数(lambda)如果要应用的函数是一个方法... - Mathieu
1
看看我的答案,这是更pythonic的版本 - python 2.5及以后有一个if-else三元运算符,所以你不需要使用and&or,并且枚举函数为序列中的每个元素返回一个(index, value)元组,因此你不需要瞎搞range和len。 - Dave Kirby
@Dave Kirby:感谢你的提示!我知道enumerate,但不知道2.5+中的if-else三元运算符。我现在会更经常使用这个运算符,特别是它可能不会遇到我提到的陷阱(关于False值)。让我为你的答案点赞,因为教会了我这个新技巧! - mjv

2

我支持你的第一个片段,但是这里还有一些其他方法供参考:

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

(在Python 3.x中不起作用。)并且:

[inp[0], inp[1].upper(), inp[2]]

考虑到我需要的是从函数返回a、b、c并将b进行转换,我想你的lambda解决方案已经接近我想要的精神了。如果有这样的语法糖就好了: return inp as a, b.upper(), c我对我的原始apply_at函数不完全满意的原因是,我想要应用的转换仅存在于一个方法中,这迫使我使用匿名函数,而不是列表推导式的解决方案。如果inp不在自己的函数中,则您的第二个解决方案似乎需要一个中间变量。 - Mathieu
以下代码在Python 3.x中是否可行?(lambda a,b,c: [a,b.upper(),c])(*inp) - Mathieu
是的,应该可以。好的,根据您所描述的问题,我会用两个语句来处理:a、b、c = inp;返回 a、b.upper()、c。很遗憾 Python 不像 Scheme 那样面向表达式,但这就是它的编写方式。 - Darius Bacon
我更喜欢的替代方案是 apply_one = lambda a,f,i: a[:i]+[f(a[i])]+a[i+1:],其中 afi 分别表示列表、函数和索引。 - AviFS

0

我不明白您是想对通过某些测试的元组中的每个元素应用某个特定函数,还是希望将该函数应用于元组中某个特定索引处存在的任何元素。因此,我编写了两种算法:

这是我在像Scheme这样的函数式语言中解决此问题时将使用的算法(使用Python编码):

此函数将识别由id标识的元素,并将func应用于它,并返回一个包含已更改为func输出的该元素的列表。它将为可识别为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)


这个算法将会找到元组中指定索引位置的元素并应用 func 函数,然后将其插入回原来的索引位置。

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)

这在Python中将会非常低效 - 如果你有一个长度为N的列表,它将创建和销毁N个长度为N的列表。它还会递归到深度N,可能超过Python的堆栈限制。 - Dave Kirby
@Dave Kirby:user189637确实要求使用函数式编程方法。我看到的大部分内容都不是函数式的,因此我提供了我的解决方案。至于创建和销毁N个列表,这正是我学习函数式编程时所了解到的。此外,函数式编程旨在在并行计算环境中运行,因此在串行计算环境中效率会降低。 - inspectorG4dget

0
我也喜欢Dave Kirby给出的答案。然而,作为公共服务宣传,我想说这不是元组的典型用例——元组是Python中的数据结构,用于在函数之间传递数据(参数、参数),并不意味着程序员可以将其用作应用程序中的一般类似数组的数据结构——这就是列表存在的原因。当然,如果您需要元组的只读/不可变特性,那是一个合理的论点,但考虑到OP的问题,这应该使用列表来完成——请注意,有额外的代码要么拆开元组并重新组合结果,要么需要临时转换为列表再转回去。

我故意选择了元组。正如我在Darius Bacon的解决方案中所评论的那样,我想要转换的元组源自于函数返回。 - Mathieu
我同意,我的原始问题应该更详细。抱歉,这是我在stackoverflow上的第一个问题。 - Mathieu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接