Случаи
- Общий случай : Почти всегда вы захотите использовать списочное понимание в python , потому что это будет более очевидно, что вы делаете для начинающих программистов, читающих ваш код. (Это не относится к другим языкам, где могут применяться другие идиомы.) Будет даже более очевидно, что вы делаете с программистами на python, поскольку списочные выражения являются фактическим стандартом в python для итерации; они ожидаются .
- Менее распространенный случай : Однако, если у вас уже есть определенная функция , часто разумно использовать
map
, хотя это считается «непифоническим». Например, map(sum, myLists)
более элегантно / кратко, чем [sum(x) for x in myLists]
. Вы получаете элегантность того, что вам не нужно составлять фиктивную переменную (например, sum(x) for x...
или sum(_) for _...
или sum(readableName) for readableName...
), которую нужно вводить дважды, просто для повторения. Тот же аргумент справедлив для filter
и reduce
и всего из модуля itertools
: если у вас уже есть удобная функция, вы можете пойти дальше и заняться функциональным программированием. В некоторых ситуациях это повышает удобочитаемость, а в других - (например, начинающие программисты, несколько аргументов) ... но читаемость вашего кода в любом случае сильно зависит от ваших комментариев.
- Почти никогда : вы можете захотеть использовать функцию
map
в качестве чисто абстрактной функции при выполнении функционального программирования, когда вы отображаете map
или карри map
, или иным образом говорить о map
как о функции. Например, в Haskell интерфейс функтора, называемый fmap
, обобщает отображение для любой структуры данных. Это очень редко встречается в python, потому что грамматика python заставляет вас использовать генераторный стиль, чтобы говорить об итерации; Вы не можете легко обобщить это. (Это иногда хорошо, а иногда плохо.) Возможно, вы найдете редкие примеры на python, в которых map(f, *lists)
- разумное решение. Самый близкий пример, который я могу придумать, был бы sumEach = partial(map,sum)
, который является однострочным, что очень приблизительно эквивалентно:
def sumEach(myLists):
return [sum(_) for _ in myLists]
- Просто используя
for
-loop : Конечно, вы также можете просто использовать цикл for. Хотя это не так элегантно с точки зрения функционального программирования, иногда нелокальные переменные делают код более понятным в императивных языках программирования, таких как python, потому что люди очень привыкли читать код таким образом. Циклы for также, как правило, наиболее эффективны, когда вы просто выполняете какую-либо сложную операцию, которая не строит список, например, списки и карты оптимизированы (например, суммирование, создание дерева и т. Д.) - по крайней мере эффективный с точки зрения памяти (не обязательно с точки зрения времени, где в худшем случае я бы ожидал постоянного фактора, за исключением некоторого редкого патологического сбоя при сборке мусора).
"Pythonism"
Мне не нравится слово "pythonic", потому что я не считаю, что pythonic всегда элегантен в моих глазах. Тем не менее, map
и filter
и аналогичные функции (например, очень полезный модуль itertools
), вероятно, считаются непитонными с точки зрения стиля.
Лень
С точки зрения эффективности, как и большинство функциональных программных конструкций, MAP МОЖЕТ ЛЕНИТЬ , и на самом деле ленив в Python. Это означает, что вы можете сделать это (в python3 ), и ваш компьютер не исчерпает память и потеряет все ваши несохраненные данные:
>>> map(str, range(10**100))
<map object at 0x2201d50>
Попробуйте сделать это с пониманием списка:
>>> [str(n) for n in range(10**100)]
# DO NOT TRY THIS AT HOME OR YOU WILL BE SAD #
Обратите внимание, что списочные выражения также по своей природе ленивы, но python решил реализовать их как не ленивые . Тем не менее, python поддерживает ленивые списки в форме выражений генератора, как показано ниже:
>>> (str(n) for n in range(10**100))
<generator object <genexpr> at 0xacbdef>
В принципе, вы можете думать о синтаксисе [...]
как о передаче выражения генератора в конструктор списка, например list(x for x in range(5))
.
Краткий надуманный пример
from operator import neg
print({x:x**2 for x in map(neg,range(5))})
print({x:x**2 for x in [-y for y in range(5)]})
print({x:x**2 for x in (-y for y in range(5))})
Понимания списков не ленивы, поэтому могут потребовать больше памяти (если вы не используете генератор пониманий). Квадратные скобки [...]
часто проясняют ситуацию, особенно в скобках. С другой стороны, иногда вы становитесь многословными, как если вы наберете [x for x in...
До тех пор, пока вы сохраняете свои переменные итератора короткими, списки обычно более понятны, если вы не делаете отступ в своем коде. Но вы всегда можете сделать отступ для своего кода.
print(
{x:x**2 for x in (-y for y in range(5))}
)
или разбить вещи:
rangeNeg5 = (-y for y in range(5))
print(
{x:x**2 for x in rangeNeg5}
)
Сравнение эффективности для python3
map
теперь ленивый:
% python3 -mtimeit -s 'xs=range(1000)' 'f=lambda x:x' 'z=map(f,xs)'
1000000 loops, best of 3: 0.336 usec per loop ^^^^^^^^^
Поэтому, если вы не будете использовать все свои данные или заранее не знаете, сколько данных вам нужно, map
в python3 (и выражениях генератора в python2 или python3) будут избегать вычисления их значений до последний момент необходим. Обычно это перевешивает любые накладные расходы от использования map
. Недостатком является то, что в python это очень ограничено по сравнению с большинством функциональных языков: вы получаете это преимущество, только если обращаетесь к своим данным слева направо «по порядку», потому что выражения генератора питона могут оцениваться только в порядке x[0], x[1], x[2], ...
.
Однако предположим, что у нас есть готовая функция f
, которую мы хотели бы map
, и мы игнорируем лень map
, немедленно форсируя оценку с помощью list(...)
. Мы получаем очень интересные результаты:
% python3 -mtimeit -s 'xs=range(1000)' 'f=lambda x:x' 'z=list(map(f,xs))'
10000 loops, best of 3: 165/124/135 usec per loop ^^^^^^^^^^^^^^^
for list(<map object>)
% python3 -mtimeit -s 'xs=range(1000)' 'f=lambda x:x' 'z=[f(x) for x in xs]'
10000 loops, best of 3: 181/118/123 usec per loop ^^^^^^^^^^^^^^^^^^
for list(<generator>), probably optimized
% python3 -mtimeit -s 'xs=range(1000)' 'f=lambda x:x' 'z=list(f(x) for x in xs)'
1000 loops, best of 3: 215/150/150 usec per loop ^^^^^^^^^^^^^^^^^^^^^^
for list(<generator>)
Результаты представлены в виде AAA / BBB / CCC, где A был выполнен на рабочей станции Intel около 2010 года с python 3.?.?, А B и C были выполнены на рабочей станции AMD около 2013 года с python 3.2 .1, с крайне разным оборудованием. В результате получается, что представления карт и списков сопоставимы по производительности, что сильнее всего зависит от других случайных факторов. Единственное, что мы можем сказать, это то, что, как ни странно, хотя мы ожидаем, что понимание списка [...]
будет работать лучше, чем выражения генератора (...)
, map
ТАКЖЕ более эффективно, чем выражения генератора (опять-таки при условии, что все значения вычисляются б).
Важно понимать, что эти тесты предполагают очень простую функцию (тождественную функцию); однако это хорошо, потому что если бы функция была сложной, то потери производительности были бы незначительными по сравнению с другими факторами в программе. (Еще может быть интересно проверить с другими простыми вещами, такими как f=lambda x:x+x
)
Если вы хорошо разбираетесь в сборке Python, вы можете использовать модуль dis
, чтобы увидеть, действительно ли это происходит за кулисами:
>>> listComp = compile('[f(x) for x in xs]', 'listComp', 'eval')
>>> dis.dis(listComp)
1 0 LOAD_CONST 0 (<code object <listcomp> at 0x2511a48, file "listComp", line 1>)
3 MAKE_FUNCTION 0
6 LOAD_NAME 0 (xs)
9 GET_ITER
10 CALL_FUNCTION 1
13 RETURN_VALUE
>>> listComp.co_consts
(<code object <listcomp> at 0x2511a48, file "listComp", line 1>,)
>>> dis.dis(listComp.co_consts[0])
1 0 BUILD_LIST 0
3 LOAD_FAST 0 (.0)
>> 6 FOR_ITER 18 (to 27)
9 STORE_FAST 1 (x)
12 LOAD_GLOBAL 0 (f)
15 LOAD_FAST 1 (x)
18 CALL_FUNCTION 1
21 LIST_APPEND 2
24 JUMP_ABSOLUTE 6
>> 27 RETURN_VALUE
>>> listComp2 = compile('list(f(x) for x in xs)', 'listComp2', 'eval')
>>> dis.dis(listComp2)
1 0 LOAD_NAME 0 (list)
3 LOAD_CONST 0 (<code object <genexpr> at 0x255bc68, file "listComp2", line 1>)
6 MAKE_FUNCTION 0
9 LOAD_NAME 1 (xs)
12 GET_ITER
13 CALL_FUNCTION 1
16 CALL_FUNCTION 1
19 RETURN_VALUE
>>> listComp2.co_consts
(<code object <genexpr> at 0x255bc68, file "listComp2", line 1>,)
>>> dis.dis(listComp2.co_consts[0])
1 0 LOAD_FAST 0 (.0)
>> 3 FOR_ITER 17 (to 23)
6 STORE_FAST 1 (x)
9 LOAD_GLOBAL 0 (f)
12 LOAD_FAST 1 (x)
15 CALL_FUNCTION 1
18 YIELD_VALUE
19 POP_TOP
20 JUMP_ABSOLUTE 3
>> 23 LOAD_CONST 0 (None)
26 RETURN_VALUE
>>> evalledMap = compile('list(map(f,xs))', 'evalledMap', 'eval')
>>> dis.dis(evalledMap)
1 0 LOAD_NAME 0 (list)
3 LOAD_NAME 1 (map)
6 LOAD_NAME 2 (f)
9 LOAD_NAME 3 (xs)
12 CALL_FUNCTION 2
15 CALL_FUNCTION 1
18 RETURN_VALUE
Кажется, лучше использовать синтаксис [...]
, чем list(...)
. К сожалению, класс map
немного непрозрачен для разборки, но мы можем сделать это с помощью нашего теста скорости.