排序列表时嵌套的 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 ——那么你应该去争取它!),首先要考虑的是可读性。所以你真的应该不做任何嵌套的lambda
s 或任何其他花哨的构造(除了可能作为练习)。
长话短说,只需使用您的#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)
(或更好)算法复杂度的函数,例如min
或max
,key
的开销会更大。那么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之通过某几个关键字排序分组一个字典列表(列表中嵌套字典)