Лямбда и понимание по спискам

Недавно я опубликовал вопрос с использованием лямбда-функции, и в ответ кто-то упомянул, что лямбда выходит из употребления, вместо этого использует списки. Я относительно новичок в Python. Я проверил простой тест:

import time

S=[x for x in range(1000000)]
T=[y**2 for y in range(300)]
#
#
time1 = time.time()
N=[x for x in S for y in T if x==y]
time2 = time.time()
print 'time diff [x for x in S for y in T if x==y]=', time2-time1
#print N
#
#
time1 = time.time()
N=filter(lambda x:x in S,T)
time2 = time.time()
print 'time diff filter(lambda x:x in S,T)=', time2-time1
#print N
#
#
#http://snipt.net/voyeg3r/python-intersect-lists/
time1 = time.time()
N = [val for val in S if val in T]
time2 = time.time()
print 'time diff [val for val in S if val in T]=', time2-time1
#print N
#
#
time1 = time.time()
N= list(set(S) & set(T))
time2 = time.time()
print 'time diff list(set(S) & set(T))=', time2-time1
#print N  #the results will be unordered as compared to the other ways!!!
#
#
time1 = time.time()
N=[]
for x in S:
    for y in T:
        if x==y:
            N.append(x)
time2 = time.time()
print 'time diff using traditional for loop', time2-time1
#print N

Все они печатают один и тот же N, поэтому я прокомментировал, что print stmt out (за исключением последнего способа его неупорядочения), но возникающие разности во времени были интересны повторными тестами, как показано в этом примере:

time diff [x for x in S for y in T if x==y]= 54.875
time diff filter(lambda x:x in S,T)= 0.391000032425
time diff [val for val in S if val in T]= 12.6089999676
time diff list(set(S) & set(T))= 0.125
time diff using traditional for loop 54.7970001698

Итак, хотя я считаю, что понимание списков в целом проще для чтения, по-видимому, некоторые проблемы производительности, по крайней мере, в этом примере.

Итак, два вопроса:

  • Почему лямбда и т.д. оттесняются?

  • Для путей понимания списка есть ли более эффективная реализация и как бы вы ЗНАЛИ ее более эффективно без тестирования? Я имею в виду, что lambda/map/filter должен был быть менее эффективным из-за дополнительных вызовов функций, но он кажется более эффективным.

Пол

Ответы

Ответ 1

Ваши тесты очень разные. Когда S является 1M элементами, а T равно 300:

[x for x in S for y in T if x==y]= 54.875

Эта опция делает сравнения сравнений 300M.

 

filter(lambda x:x in S,T)= 0.391000032425

Этот параметр выполняет 300 линейных поисков по S.

 

[val for val in S if val in T]= 12.6089999676

Эта опция выполняет 1M линейный поиск через T.

 

list(set(S) & set(T))= 0.125

Эта опция выполняет две заданные конструкции и одно множество пересечений.


Различия в производительности между этими параметрами гораздо больше связаны с алгоритмами, которые каждый использует, а не с разницей между пониманиями списков и lambda.

Ответ 2

Когда я исправляю ваш код, чтобы понимание списка и вызов filter на самом деле выполняли одни и те же рабочие вещи, многое изменилось:

import time

S=[x for x in range(1000000)]
T=[y**2 for y in range(300)]
#
#
time1 = time.time()
N=[x for x in T if x in S]
time2 = time.time()
print 'time diff [x for x in T if x in S]=', time2-time1
#print N
#
#
time1 = time.time()
N=filter(lambda x:x in S,T)
time2 = time.time()
print 'time diff filter(lambda x:x in S,T)=', time2-time1
#print N

Затем результат больше похож:

time diff [x for x in T if x in S]= 0.414485931396
time diff filter(lambda x:x in S,T)= 0.466315984726

Таким образом, понимание списка имеет время, которое обычно довольно близко и обычно меньше, чем выражение лямбда.

Причина появления лямбда-выражений заключается в том, что многие люди считают, что они намного менее читабельны, чем понимание списков. Я неохотно соглашаюсь.

Ответ 3

В: Почему лямбда и т.д. отбрасываются?

A: Учет списков и выражений генератора обычно считаются хорошим сочетанием мощности и удобочитаемости. Чистый стиль функционального программирования, в котором вы используете map(), reduce() и filter() с функциями (часто lambda), считается не столь ясным. Кроме того, Python добавил встроенные функции, которые прекрасно обрабатывают все основные применения для reduce().

Предположим, вы хотели бы суммировать список. Вот два способа сделать это.

lst = range(10)
print reduce(lambda x, y: x + y, lst)

print sum(lst)

Подпишите меня как поклонника sum(), а не поклонника reduce(), чтобы решить эту проблему. Здесь другая, аналогичная проблема:

lst = range(10)
print reduce(lambda x, y: bool(x or y), lst)

print any(lst)

Решение any() не только упрощается, но и намного быстрее; он имеет оценку короткого замыкания, так что он перестанет оценивать, как только он найдет какое-либо истинное значение. reduce() должен прокручиваться по всему списку. Эта разница в производительности была бы абсолютной, если бы список составлял миллион элементов, а первый элемент оценивался как true. Кстати, any() был добавлен в Python 2.5; если у вас его нет, вот версия для более старых версий Python:

def any(iterable):
    for x in iterable:
        if x:
            return True
    return False

Предположим, вы хотели составить список квадратов четных чисел из некоторого списка.

lst = range(10)
print map(lambda x: x**2, filter(lambda x: x % 2 == 0, lst))

print [x**2 for x in lst if x % 2 == 0]

Теперь предположим, что вы хотели бы суммировать этот список квадратов.

lst = range(10)
print sum(map(lambda x: x**2, filter(lambda x: x % 2 == 0, lst)))

# list comprehension version of the above
print sum([x**2 for x in lst if x % 2 == 0])

# generator expression version; note the lack of '[' and ']'
print sum(x**2 for x in lst if x % 2 == 0)

Выражение генератора фактически возвращает возвращаемый объект. sum() берет итерабельность и вытягивает из нее значения, один за другим, суммируя их, пока все значения не будут потреблены. Это самый эффективный способ решить эту проблему в Python. Напротив, решение map() и эквивалентное решение с пониманием списка внутри вызова sum() должны сначала создать список; этот список затем передается в sum(), который используется один раз и отбрасывается. Время, чтобы создать список, а затем удалить его снова, просто потрачено впустую. (EDIT: и обратите внимание, что версия с map и filter должна создавать два списка, один из которых создан filter и один из них создан map, оба списка отбрасываются.) (EDIT: Но в Python 3.0 и newer, map() и filter() теперь являются "ленивыми" и производят итератор вместо списка, поэтому этот пункт менее правдоподобен, чем раньше. Кроме того, в Python 2.x вы могли использовать itertools. imap() и itertools.ifilter() для карты и фильтра на основе итератора. Но я по-прежнему предпочитаю решения выражения генератора над любыми решениями карт/фильтров.)

Составляя map(), filter() и reduce() в комбинации с функциями lambda, вы можете делать много мощных вещей. Но у Python есть идиоматические способы решения тех же проблем, которые одновременно лучше выполняют и легче читать и понимать.

Ответ 4

Многие люди уже указывали, что вы сравниваете яблоки с апельсинами и т.д. и т.д. Но я думаю, что никто не показывал, как действительно простое сравнение - список по сравнению с картой плюс лямбда с небольшим количеством, чтобы мешать - - и это может быть:

$ python -mtimeit -s'L=range(1000)' 'map(lambda x: x+1, L)'
1000 loops, best of 3: 328 usec per loop
$ python -mtimeit -s'L=range(1000)' '[x+1 for x in L]'
10000 loops, best of 3: 129 usec per loop

Здесь вы можете очень резко оценить стоимость лямбда - около 200 микросекунд, что в случае достаточно простой операции, такой как эта, боксирует сама операция.

Числа очень похожи с фильтром, конечно, поскольку проблема заключается в не фильтре или карте, а сама лямбда:

$ python -mtimeit -s'L=range(1000)' '[x for x in L if not x%7]'
10000 loops, best of 3: 162 usec per loop
$ python -mtimeit -s'L=range(1000)' 'filter(lambda x: not x%7, L)'
1000 loops, best of 3: 334 usec per loop

Несомненно, тот факт, что лямбда может быть менее ясной или ее странная связь со Спартой (у спартанцев была Лямбда, для "Лакедамона", нарисованная на их щитах - это говорит о том, что лямбда довольно диктаторская и кровавая;-) по крайней мере, так же, как и его медленное падение моды, поскольку его производительность стоит. Но последние вполне реальны.

Ответ 5

Прежде всего, проверьте следующее:

import timeit

S=[x for x in range(10000)]
T=[y**2 for y in range(30)]

print "v1", timeit.Timer('[x for x in S for y in T if x==y]',
             'from __main__ import S,T').timeit(100)
print "v2", timeit.Timer('filter(lambda x:x in S,T)',
             'from __main__ import S,T').timeit(100)
print "v3", timeit.Timer('[val for val in T if val in S]',
             'from __main__ import S,T').timeit(100)
print "v4", timeit.Timer('list(set(S) & set(T))',
             'from __main__ import S,T').timeit(100)

И в основном вы делаете разные вещи каждый раз, когда проверяете. Когда вы перепишете понимание списка, например,

[val for val in T if val in S]

производительность будет соответствовать конструкции lambda/filter.

Ответ 6

Наборы - правильное решение для этого. Однако попробуйте поменять S и T и посмотреть, сколько времени потребуется!

filter(lambda x:x in T,S)

$ python -m timeit -s'S=[x for x in range(1000000)];T=[y**2 for y in range(300)]' 'filter(lambda x:x in S,T)'
10 loops, best of 3: 485 msec per loop
$ python -m timeit -r1 -n1 -s'S=[x for x in range(1000000)];T=[y**2 for y in range(300)]' 'filter(lambda x:x in T,S)'
1 loops, best of 1: 19.6 sec per loop

Итак, вы видите, что порядок S и T весьма важен.

Изменение порядка понимания списка в соответствии с фильтром дает

$ python -m timeit  -s'S=[x for x in range(1000000)];T=[y**2 for y in range(300)]' '[x for x in T if x in S]'
10 loops, best of 3: 441 msec per loop

Итак, если факт, что понимание списка немного быстрее, чем лямбда на моем компьютере

Ответ 7

Ваше понимание списка и лямбда делают разные вещи, понимание списка, соответствующее лямбда, будет [val for val in T if val in S].

Эффективность не является причиной предпочтения списка (в то время как они фактически немного быстрее почти во всех случаях). Причина, по которой они предпочтительнее, - читаемость.

Попробуйте с меньшим телом цикла и более крупными контурами, например, сделайте T набором и перейдите на S. В этом случае на моей машине понимание списка почти в два раза быстрее.

Ответ 8

Профилирование сделано неправильно. Посмотрите модуль timeit и повторите попытку.

lambda определяет анонимные функции. Основная их проблема заключается в том, что многие люди не знают всю библиотеку python и используют их для повторной реализации функций, которые уже находятся в модуле operator, functools и т.д. (И намного быстрее).

Сопоставление списков не имеет ничего общего с lambda. Они эквивалентны стандартным функциям filter и map от функциональных языков. LC предпочтительны, поскольку они также могут использоваться в качестве генераторов, не говоря уже о читаемости.

Ответ 9

Это довольно быстро:

def binary_search(a, x, lo=0, hi=None):
    if hi is None:
        hi = len(a)
    while lo < hi:
        mid = (lo+hi)//2
        midval = a[mid]
        if midval < x:
            lo = mid+1
        elif midval > x: 
            hi = mid
        else:
            return mid
    return -1

time1 = time.time()
N = [x for x in T if binary_search(S, x) >= 0]
time2 = time.time()
print 'time diff binary search=', time2-time1

Просто: меньше сравнений, меньше времени.

Ответ 10

Пояснения к спискам могут иметь большее значение, если вам нужно обработать ваши отфильтрованные результаты. В вашем случае вы просто создаете список, но если вам нужно было сделать что-то вроде этого:

n = [f(i) for i in S if some_condition(i)]

вы выиграли бы от оптимизации LC над этим:

n = map(f, filter(some_condition(i), S))

просто потому, что последний должен создать промежуточный список (или кортеж или строку, в зависимости от природы S). Как следствие, вы также заметите другое влияние на память, используемую каждым методом, LC будет ниже.

Лямбда сама по себе не имеет значения.