Вложенные выражения лямбда при сортировке списков

Я хочу отсортировать список ниже по числу, а затем по тексту.

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