嵌套的lambda语句在对列表进行排序时的应用

15

我希望首先按数字大小,再按文本对下面的列表进行排序。

lst = ['b-3', 'a-2', 'c-4', 'd-2']

# result:
# ['a-2', 'd-2', 'b-3', 'c-4']

尝试 1

res = sorted(lst, key=lambda x: (int(x.split('-')[1]), x.split('-')[0]))

我对此不太满意,因为它需要分割字符串两次以提取相关组件。

尝试2

我想出了以下解决方案。但我希望能通过Pythonic的lambda语句找到更简洁的解决方案。

def sorter_func(x):
    text, num = x.split('-')
    return int(num), text

res = sorted(lst, key=sorter_func)

我看了一下《理解Python中嵌套lambda函数的行为》,但是直接使用该解决方案不太适用。是否有更简洁的方式重写上述代码?


3
我的首选解决方案是尝试第二种方法,至少在采用PEP-572或类似方案之前都是如此。即使那时,它也似乎允许类似key=lambda x:(int((x.split('-') as y)[0]), y[1])的语法,而我对这种语法并不感兴趣。 - chepner
@chepner,我同意。确保这是我的答案中的第一点! - jpp
2
另外,对于这个特定的问题,您应该考虑如果需要 2 个条件,例如如果数字在范围 1-9 内是整数,就像您的示例一样,那么您可以使用反向字典序排序,如 sorted(lst, key= lambda x: x[::-1]) - Chris_Rands
@Chris_Rands,没错,我同意。这个赏金的原因之一是引起对PEP 572 -- 赋值表达式的关注。共识似乎是“不要去那里”。 - jpp
PEP 572是一个非常有趣的前景,我实际上写了一个关于它的问答,但由于它只是一个提案,尚未被Python 3.8接受,所以我将其删除了。https://dev59.com/21UL5IYBdhLWcg3weH4j - Chris_Rands
显示剩余2条评论
8个回答

18

需要注意两点:

  • 单行回答未必更好。使用命名函数可能会使您的代码更易于阅读。
  • 您可能 不需要 嵌套lambda语句,因为函数合成不是标准库的一部分(请参见注释#1)。您可以轻松地使用一个 lambda 函数返回另一个 lambda 函数的结果。

因此,正确答案可以在Lambda inside lambda中找到。

针对您的特定问题,您可以使用:

res = sorted(lst, key=lambda x: (lambda y: (int(y[1]), y[0]))(x.split('-')))

要记住,lambda只是一个函数。你可以在定义后立即调用它,甚至可以在同一行上调用。

注意1:第三方toolz库允许组合:

from toolz import compose

res = sorted(lst, key=compose(lambda x: (int(x[1]), x[0]), lambda x: x.split('-')))

注意 #2:正如@chepner指出的那样,这个解决方案的不足之处(重复的函数调用)是PEP-572在Python 3.8中被认为实现的原因之一。


1
很高兴看到这个解决方案,非常感谢您的投入。 - Austin
嵌套的lambda表达式最大的问题在于它会产生额外的(用户定义的)函数调用开销,这对于大型列表来说可能是相当显著的。 - chepner
@chepner,非常好的观点。我很高兴看到有替代方案。我还没有被说服,认为这比其他一行代码的替代方案更糟糕。 - jpp
1
毫无疑问 :) 所有内联解决方案的缺陷是为什么正在考虑PEP-572的原因。 - chepner
@chepner,我认为toolz.compose对此有一个不错的解决方案,可以避免重复的函数调用。可惜它不在标准库中。 - jpp
1
我会使用 methodcaller("split", "-") 来代替第二个 lambda。 - chepner

7
我们可以将split('-')返回的列表再次嵌套在另一个列表中,然后使用循环来处理它:
# Using list-comprehension
>>> sorted(lst, key=lambda x: [(int(num), text) for text, num in [x.split('-')]])
['a-2', 'd-2', 'b-3', 'c-4']
# Using next()
>>> sorted(lst, key=lambda x: next((int(num), text) for text, num in [x.split('-')]))
['a-2', 'd-2', 'b-3', 'c-4']

很高兴你发布了这个,这是我中间的一次尝试!但是后来我想所有这些列表创建并不一定是好的。 - jpp
1
@jpp 我们可以通过将列表推导式替换为类似于你的版本的next()调用来减少一个列表创建。 - Ashwini Chaudhary

3
几乎在所有情况下,我都会选择你的第二种尝试。它易读且简洁(每次我都更喜欢三个简单的行而不是一行复杂的代码!)——即使函数名可以更具描述性。但如果您将其用作本地函数,则这并不重要。
您还必须记住,Python使用 key 函数而不是 cmp(比较)函数。因此,为了对长度为 n 的可迭代对象进行排序,key 函数被调用正好 n 次,但排序通常执行 O(n * log(n)) 比较。因此,每当您的键函数具有算法复杂度为 O(1) 时,键函数调用开销就不会太大。这是因为:
O(n*log(n)) + O(n)   ==  O(n*log(n))

有一个例外情况,那就是对于Python的sort来说是最佳情况:在最佳情况下,sort仅进行O(n)次比较,但这仅在可迭代对象已经排序(或几乎排序)时才会发生。如果Python有一个比较函数(在Python 2中确实有一个),那么函数的常数因子将更加重要,因为它将被调用O(n * log(n))次(每个比较调用一次)。
所以不要担心变得更简洁或更快(除非您可以在不引入太大常数因子的情况下减少大O-复杂度 - 那么您应该这样做!),第一关注点应该是可读性。因此,您真的不应该使用任何嵌套的lambda或任何其他花哨的构造(除非作为练习)。
长话短说,只需使用您的#2:
def sorter_func(x):
    text, num = x.split('-')
    return int(num), text

res = sorted(lst, key=sorter_func)

顺便说一下,这也是所有提出方法中最快的(尽管差别不大):

enter image description here

摘要: 它是可读和快速的!

复制基准测试的代码。需要安装simple_benchmark才能正常工作(免责声明:这是我自己的库),但可能有类似的框架来执行此类任务,但我只熟悉它:

# My specs: Windows 10, Python 3.6.6 (conda)

import toolz
import iteration_utilities as it

def approach_jpp_1(lst):
    return sorted(lst, key=lambda x: (int(x.split('-')[1]), x.split('-')[0]))

def approach_jpp_2(lst):
    def sorter_func(x):
        text, num = x.split('-')
        return int(num), text
    return sorted(lst, key=sorter_func)

def jpp_nested_lambda(lst):
    return sorted(lst, key=lambda x: (lambda y: (int(y[1]), y[0]))(x.split('-')))

def toolz_compose(lst):
    return sorted(lst, key=toolz.compose(lambda x: (int(x[1]), x[0]), lambda x: x.split('-')))

def AshwiniChaudhary_list_comprehension(lst):
    return sorted(lst, key=lambda x: [(int(num), text) for text, num in [x.split('-')]])

def AshwiniChaudhary_next(lst):
    return sorted(lst, key=lambda x: next((int(num), text) for text, num in [x.split('-')]))

def PaulCornelius(lst):
    return sorted(lst, key=lambda x: tuple(f(a) for f, a in zip((int, str), reversed(x.split('-')))))

def JeanFrançoisFabre(lst):
    return sorted(lst, key=lambda s : [x if i else int(x) for i,x in enumerate(reversed(s.split("-")))])

def iteration_utilities_chained(lst):
    return sorted(lst, key=it.chained(lambda x: x.split('-'), lambda x: (int(x[1]), x[0])))

from simple_benchmark import benchmark
import random
import string

funcs = [
    approach_jpp_1, approach_jpp_2, jpp_nested_lambda, toolz_compose, AshwiniChaudhary_list_comprehension,
    AshwiniChaudhary_next, PaulCornelius, JeanFrançoisFabre, iteration_utilities_chained
]

arguments = {2**i: ['-'.join([random.choice(string.ascii_lowercase),
                              str(random.randint(0, 2**(i-1)))]) 
                    for _ in range(2**i)] 
             for i in range(3, 15)}

b = benchmark(funcs, arguments, 'list size')

%matplotlib notebook
b.plot_difference_percentage(relative_to=approach_jpp_2)

我想在这里介绍一下我自己的库iteration_utilities.chained,并给出一个函数组合的方法:

from iteration_utilities import chained
sorted(lst, key=chained(lambda x: x.split('-'), lambda x: (int(x[1]), x[0])))

这很快(第二或第三名),但仍然比使用自己的函数慢。


请注意,如果您使用的是具有O(n)(或更好)算法复杂度的函数,例如minmax,那么key开销将会更加显着。然后,键函数的常数因素将变得更加重要!

1
非常好的答案,感谢您添加性能维度,这是我没有考虑过的。故事的寓意是:一旦您需要更多于简单的lambda,请改用显式函数! - jpp

2
lst = ['b-3', 'a-2', 'c-4', 'd-2']
res = sorted(lst, key=lambda x: tuple(f(a) for f, a in zip((int, str), reversed(x.split('-')))))
print(res)

['a-2', 'd-2', 'b-3', 'c-4']

保罗刚刚用一行代码击败了那个两页的答案。干得好,保罗。 - dejoma

1
只有在反转拆分列表时,才能将项目的索引转换为整数(仅当索引为0时)。除了用于比较的两个元素列表(split的结果之外),唯一创建的对象是迭代器。
sorted(lst,key = lambda s : [x if i else int(x) for i,x in enumerate(reversed(s.split("-")))])

作为一件小事,当涉及到数字时,- 标记并不是特别好,因为它会使负数的使用变得复杂(但可以通过 s.split("-",1) 解决)。

0
lst = ['b-3', 'a-2', 'c-4', 'd-2']
def xform(l):
    return list(map(lambda x: x[1] + '-' + x[0], list(map(lambda x: x.split('-'), lst))))
lst = sorted(xform(lst))
print(xform(lst))

在这里查看 我认为@jpp有一个更好的解决方案,但这是一个有趣的小谜题 :-)


0
一般来说,使用FOP(函数式编程)可以将所有内容放在一行中,并在其中嵌套lambda表达式,但这通常是不好的礼仪,因为在嵌套2个函数后,它变得难以阅读。
解决这种问题的最佳方法是将其分成几个阶段:
1:将字符串拆分为元组:
lst = ['b-3', 'a-2', 'c-4', 'd-2']
res = map( lambda str_x: tuple( str_x.split('-') ) , lst)   

2:根据您的要求对元素进行排序:

lst = ['b-3', 'a-2', 'c-4', 'd-2']
res = map( lambda str_x: tuple( str_x.split('-') ) , lst)  
res = sorted( res, key=lambda x: ( int(x[1]), x[0] ) ) 

由于我们将字符串分割成元组,它将返回一个表示为元组列表的映射对象。因此,现在第三步是可选的:

3:按照您的要求表示数据:

lst = ['b-3', 'a-2', 'c-4', 'd-2']
res = map( lambda str_x: tuple( str_x.split('-') ) , lst)  
res = sorted( res, key=lambda x: ( int(x[1]), x[0] ) ) 
res = map( '-'.join, res )  

现在请注意,lambda嵌套可以产生更简洁的解决方案,而且您实际上可以像下面这样嵌入非离散嵌套类型的lambda:

a = ['b-3', 'a-2', 'c-4', 'd-2']
resa = map( lambda x: x.split('-'), a)
resa = map( lambda x: ( int(x[1]),x[0]) , a) 
# resa can be written as this, but you must be sure about type you are passing to lambda 
resa = map( lambda x: tuple( map( lambda y: int(y) is y.isdigit() else y , x.split('-') ) , a)  

但是,正如你所看到的,如果列表a的内容不是由'-'分隔的2个字符串类型,那么lambda函数将会引发错误,你将会很困惑地想弄清楚到底发生了什么。


最后,我想向你展示一些编写第三步程序的方法:
1:
lst = ['b-3', 'a-2', 'c-4', 'd-2']
res = map( '-'.join,\
             sorted(\ 
                  map( lambda str_x: tuple( str_x.split('-') ) , lst),\
                       key=lambda x: ( int(x[1]), x[0] )\
              )\
         )

2:

lst = ['b-3', 'a-2', 'c-4', 'd-2']
res = map( '-'.join,\
        sorted( map( lambda str_x: tuple( str_x.split('-') ) , lst),\
                key=lambda x: tuple( reversed( tuple(\
                            map( lambda y: int(y) if y.isdigit() else y ,x  )\
                        )))\
            )\
    )  # map isn't reversible

3:

res = sorted( lst,\
             key=lambda x:\
                tuple(reversed(\
                    tuple( \
                        map( lambda y: int(y) if y.isdigit() else y , x.split('-') )\
                    )\
                ))\
            )

你可以看到这个过程可以变得非常复杂和难以理解。当阅读我的或他人的代码时,我经常喜欢看到这个版本:

res = map( lambda str_x: tuple( str_x.split('-') ) , lst) # splitting string 
res = sorted( res, key=lambda x: ( int(x[1]), x[0] ) ) # sorting for each element of splitted string
res = map( '-'.join, res ) # rejoining string  

这就是我要说的全部内容。祝玩得愉快。我已经在py 3.6中测试了所有的代码。


PS. 一般来说,你有两种方法来使用lambda函数

mult = lambda x: x*2  
mu_add= lambda x: mult(x)+x #calling lambda from lambda

这种方法对于典型的FOP很有用,其中您拥有常量数据,并且需要操作该数据的每个元素。但是,如果您需要在lambda中解决list、tuple、string、dict等容器/包装类型,这些操作就不是非常有用了,因为如果任何这些容器/包装类型存在,则容器内部元素的数据类型变得可疑。因此,我们需要提高抽象级别并确定如何根据其类型操作数据。

mult_i = lambda x: x*2 if isinstance(x,int) else 2 # some ternary operator to make our life easier by putting if statement in lambda 

现在您可以使用另一种类型的lambda函数:
int_str = lambda x: ( lambda y: str(y) )(x)*x # a bit of complex, right?  
# let me break it down. 
#all this could be written as: 
str_i = lambda x: str(x) 
int_str = lambda x: str_i(x)*x 
## we can separate another function inside function with ()
##because they can exclude interpreter to look at it first, then do the multiplication  
# ( lambda x: str(x)) with this we've separated it as new definition of function  
# ( lambda x: str(x) )(i) we called it and passed it i as argument.  

有些人称这种语法为嵌套 lambda,我则称其为 indiscreet,因为你可以看到全部。

而且您可以使用递归 lambda 分配:

def rec_lambda( data, *arg_lambda ):  
    # filtering all parts of lambda functions parsed as arguments 
    arg_lambda = [ x for x in arg_lambda if type(x).__name__ == 'function' ]  

    # implementing first function in line
    data = arg_lambda[0](data)  

    if arg_lambda[1:]: # if there are still elements in arg_lambda 
        return rec_lambda( data, *arg_lambda[1:] ) #call rec_lambda
    else: # if arg_lambda is empty or []
        return data # returns data  

#where you can use it like this  
a = rec_lambda( 'a', lambda x: x*2, str.upper, lambda x: (x,x), '-'.join) 
>>> 'AA-AA' 

-3

我认为*如果您确定格式始终是"[0]字母 [1]破折号",那么超出[2:]的索引将始终是数字,那么您可以用切片替换分割(split),或者您可以使用str.index('-')

sorted(lst, key=lambda x:(int(x[2:]),x[0]))

# str.index('-') 
sorted(lst, key=lambda x:(int(x[x.index('-')+1 :]),x[0])) 

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