Лямбда и понимание по спискам
Недавно я опубликовал вопрос с использованием лямбда-функции, и в ответ кто-то упомянул, что лямбда выходит из употребления, вместо этого использует списки. Я относительно новичок в 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 будет ниже.
Лямбда сама по себе не имеет значения.