Вложенные выражения лямбда при сортировке списков
Я хочу отсортировать список ниже по числу, а затем по тексту.
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
Я придумал решение ниже. Но я надеюсь, что есть более сжатое решение с помощью питоновских заявлений lambda
.
def sorter_func(x):
text, num = x.split('-')
return int(num), text
res = sorted(lst, key=sorter_func)
Я посмотрел на Понимание поведения вложенной лямбда-функции в python, но не смог напрямую адаптировать это решение. Есть ли более краткий способ переписать вышеприведенный код?
Ответы
Ответ 1
Почти во всех случаях я бы просто пошел с вашей второй попыткой. Это читаемо и красноречиво (я бы предпочел три простые строки над одной сложной строкой каждый раз!) - хотя имя функции может быть более наглядным. Но если вы используете его как локальную функцию, которая не имеет большого значения.
Вы также должны помнить, что Python использует key
функцию, а не функцию cmp
(compare). Таким образом, для сортировки итерабельности длины n
key
функция вызывается ровно n
раз, но при сортировке обычно выполняются сравнения O(n * log(n))
. Поэтому всякий раз, когда ваша ключевая функция имеет алгоритмическую сложность O(1)
накладные расходы на вызов функции ключа не будут иметь большого значения (много). Это потому, что:
O(n*log(n)) + O(n) == O(n*log(n))
Там есть одно исключение и лучший выбор для sort
Pythons: в лучшем случае 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])))
Это довольно быстро (2-е или 3-е место), но все же медленнее, чем использование вашей собственной функции.
Обратите внимание, что key
служебные данные были бы более значительными, если бы вы использовали функцию с алгоритмической сложностью O(n)
(или лучше), например min
или max
. Тогда постоянные факторы ключевой функции будут более значительными!
Ответ 2
Есть 2 замечания:
- Однострочные ответы не обязательно лучше. Использование названной функции, вероятно, сделает ваш код более легким для чтения.
- Вероятно, вы не ищете вложенную инструкцию
lambda
, так как состав функций не входит в стандартную библиотеку (см. Примечание № 1). То, что вы можете сделать легко, - это одна lambda
функция, возвращающая результат другой 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 рассматривается.
Ответ 3
Мы можем обернуть список, возвращенный 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']
Ответ 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']
Ответ 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))
Увидимся здесь. Думаю, что у @jpp есть лучшее решение, но забавный маленький головоломщик :-)
Ответ 7
В общем, с FOP (функционально-ориентированное программирование) вы можете поместить все это в один лайнер и гнездо lambdas в пределах одного слоя, но это, как правило, плохой этикет, поскольку после двух функций вложенности все становится совсем нечитаемым.
Лучший способ подойти к этой проблеме - разделить ее на несколько этапов:
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
может создавать более однострочное решение и что вы можете фактически внедрить недискретный тип вложения лямбда следующим образом:
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
ли содержимое list a
arent ничего, кроме двух типов строк, разделенных символом '-'
, функция 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. В общем, у вас есть 2 способа приблизиться к lambda functions
:
mult = lambda x: x*2
mu_add= lambda x: mult(x)+x #calling lambda from lambda
Этот способ полезен для типичного FOP, где у вас есть постоянные данные, и вам нужно манипулировать каждым элементом этих данных. Но если вам нужно разрешить list,tuple,string,dict
в lambda
эти операции не очень полезны, так как если присутствует какой-либо из этих типов 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.
Некоторые люди называют этот тип синтаксиса как вложенные lambdas, я называю его indiscreet, так как вы можете видеть все.
И вы можете использовать рекурсивное назначение лямбда:
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] тире" следующие индексы за пределами [2:] всегда будут числом, то вы можете заменить split на slice, или вы можете использовать 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]))