排序列表时嵌套的 lambda 语句

Posted

技术标签:

【中文标题】排序列表时嵌套的 lambda 语句【英文标题】:Nested lambda statements when sorting lists 【发布时间】:2018-09-15 06:05:57 【问题描述】:

我希望先按数字排序下面的列表,然后按文本。

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)

我查看了Understanding nested lambda function behaviour in python,但无法直接调整此解决方案。有没有更简洁的方式来重写上面的代码?

【问题讨论】:

尝试 2 将是我的首选解决方案,至少在 PEP-572 或类似的东西被采用之前。 (即使那样,它似乎也允许像 key=lambda x: (int((x.split('-') as y)[0]), y[1]) 这样的东西,我不喜欢它。) @chepner,我同意。确保这是我回答的第一点! 实际上,我什至不确定 PEP-572 是否可以在这里工作; y 可能是对 int 的调用的本地,而不是元组...... ick。 就我个人而言,我发现 Attempt 2 比迄今为止建议的任何其他解决方案更具可读性和 Python 风格;除非性能很关键,否则其他任何事情都可能是微优化(尽管一个好的基准可以说服我不这样做) 您还应该考虑这个特定问题,如果需要 2 个标准,例如,如果数字是 1-9 范围内的整数,如您的示例中那样,那么您可以只进行反向 lexographic 排序喜欢sorted(lst, key= lambda x: x[::-1]) 【参考方案1】:

有2点需要注意:

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

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

针对您的具体问题,您可以使用:

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

请记住,lambda 只是一个函数。您可以在定义后立即调用它,即使在同一行。

注意 #1:第 3 方 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。

【讨论】:

很高兴看到解决方案,非常感谢您的奉献。 嵌套 lambda 的最大问题是它会产生额外的(用户定义的)函数调用的开销,这对于大型列表可能很重要。 @chepner,非常好的观点。我很高兴看到替代品。我不相信(还)它比其他单线替代品更糟糕。 那里没有争论 :) 所有在线解决方案的缺陷是考虑 PEP-572 的原因。 我会使用methodcaller("split", "-") 代替第二个 lambda。【参考方案2】:

我们可以将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 我们可以通过使用与您的版本类似的next() 调用替换 LC 来减少创建一个列表。【参考方案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) 比较,但这只发生在 iterable 已经排序(或几乎排序)的情况下。如果 Python 有一个 compare 函数(在 Python 2 中确实有一个),那么该函数的常数因子会更重要,因为它会被调用 O(n * log(n)) 次(每次比较调用一次)。

所以不要为更简洁或让它更快(除非你可以在不引入太大常数因子的情况下减少大 O ——那么你应该去争取它!),首先要考虑的是可读性。所以你真的应该做任何嵌套的lambdas 或任何其他花哨的构造(除了可能作为练习)。

长话短说,只需使用您的#2:

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

res = sorted(lst, key=sorter_func)

顺便说一下,它也是所有提议的方法中最快的(虽然差别不大):

总结:它可读且快速

重现基准的代码。它需要安装 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])))

它相当快(第 2 或第 3 名),但仍然比使用您自己的函数慢。


请注意,如果您使用具有O(n)(或更好)算法复杂度的函数,例如minmaxkey 的开销会更大。那么key-function的常数因子会更显着!

【讨论】:

很好的答案,感谢您添加性能维度,这是我没有考虑过的。故事的寓意:只要您需要的不仅仅是简单的lambda,请改用显式函数!【参考方案4】:
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']

【讨论】:

Paul 在一行中击败了 2 页的答案。干得好保罗【参考方案5】:

只有当项目的索引为 0 时(反转拆分列表时),您才能转换为整数。创建的唯一对象(除了split 的结果)是用于比较的2 元素列表。其余的只是迭代器。

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

顺便说一句,- 标记在涉及数字时并不是特别好,因为它使负数的使用变得复杂(但可以使用 s.split("-",1) 解决

【讨论】:

【参考方案6】:
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))

看here 我认为@jpp 有一个更好的解决方案,但是一个有趣的小脑筋急转弯:-)

【讨论】:

【参考方案7】:

通常使用 FOP(面向函数的编程),您可以将所有内容放在一个内衬中,并将 lambdas 嵌套在一个内衬中,但这通常是不好的礼仪,因为在 2 个嵌套函数之后,所有内容都变为很不可读。

解决此类问题的最佳方法是将其分成几个阶段:

1:将字符串拆分成tuple:

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 nesting 可以产生更单行的解决方案,并且您实际上可以嵌入非离散嵌套类型的 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)  

但是您可以看到 list 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 functions

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

这种方式对于典型的 FOP 很有用,在这种情况下,您有常量数据,并且您需要操作该数据的每个元素。但是,如果您需要在 lambda 中解析 list,tuple,string,dict,这些操作并不是很有用,因为如果存在这些 container/wrapper 类型中的任何一个,那么容器内元素的数据类型就会变得可疑.因此,我们需要提升一个抽象层,并确定如何根据数据类型来操作数据。

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,我称其为轻率,因为您可以看到所有内容。

你可以使用递归 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' 

【讨论】:

【参考方案8】:

我认为*如果您确定格式始终是“[0]alphabet [1]dash”,那么 [2:] 之后的索引将始终是数字,那么您可以将 split 替换为 slice,或者您可以使用 str.索引('-')

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

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

【讨论】:

以上是关于排序列表时嵌套的 lambda 语句的主要内容,如果未能解决你的问题,请参考以下文章

Python字典字典列表嵌套字典排序

Python字典字典列表嵌套字典排序

学不会的python之通过某几个关键字排序分组一个字典列表(列表中嵌套字典)

学不会的python之通过某几个关键字排序分组一个字典列表(列表中嵌套字典)

如何使用 lambda 流迭代嵌套列表?

如何使用 lambda 表达式查询嵌套列表