Чи є причина віддавати перевагу використанню map()
над розумінням списку чи навпаки? Чи будь-який з них, як правило, більш ефективний чи вважається загалом більш пітонічним, ніж інший?
Чи є причина віддавати перевагу використанню map()
над розумінням списку чи навпаки? Чи будь-який з них, як правило, більш ефективний чи вважається загалом більш пітонічним, ніж інший?
Відповіді:
map
може бути мікроскопічно швидшим у деяких випадках (коли ви НЕ створюєте лямбда з цією метою, але використовуєте ту саму функцію у map та listcomp). Зрозуміння списків може бути швидшим в інших випадках, і більшість (не всіх) пітоністів вважають їх більш прямими і зрозумілішими.
Приклад крихітної переваги швидкості карти при використанні точно тієї ж функції:
$ python -mtimeit -s'xs=range(10)' 'map(hex, xs)'
100000 loops, best of 3: 4.86 usec per loop
$ python -mtimeit -s'xs=range(10)' '[hex(x) for x in xs]'
100000 loops, best of 3: 5.58 usec per loop
Приклад того, як порівняння продуктивності повністю змінюється, коли карта потребує лямбда:
$ python -mtimeit -s'xs=range(10)' 'map(lambda x: x+2, xs)'
100000 loops, best of 3: 4.24 usec per loop
$ python -mtimeit -s'xs=range(10)' '[x+2 for x in xs]'
100000 loops, best of 3: 2.32 usec per loop
map(operator.attrgetter('foo'), objs)
легше читати, ніж [o.foo for o in objs]
?!
o
тут, а ваші приклади показують, чому.
str()
прикладом.
Справи
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 змушує вас використовувати стиль генератора, щоб говорити про ітерацію; ви не можете легко узагальнити це. (Це іноді добре, а іноді і погано.) Ви, мабуть, можете знайти рідкісні приклади пітонів, де map(f, *lists)
це розумно робити. Найближчим прикладом, який я можу придумати, може бути sumEach = partial(map,sum)
, який є однолінійним, який приблизно відповідає:def sumEach(myLists):
return [sum(_) for _ in myLists]
for
-loop : Ви, звичайно, також можете просто використовувати цикл for. Хоча це не настільки елегантно з точки зору функціонального програмування, іноді нелокальні змінні роблять код зрозумілішим в імперативних мовах програмування, таких як python, оскільки люди дуже звикли читати код таким чином. Для циклів також є, як правило, найефективнішими, коли ви просто робите будь-яку складну операцію, яка не створює список, наприклад, розуміння списків і карта, оптимізовані для (наприклад, підбиття підсумків, створення дерева тощо) - принаймні ефективний з точки зору пам’яті (не обов'язково з точки зору часу, де я б очікував в гіршому випадку постійний фактор, що забороняє деякі рідкісні патологічні ікони для збору сміття)."Піфонізм"
Мені не подобається слово "пітонік", тому що я не знаю, що пітонік завжди елегантний в моїх очах. Тим не менш, map
і filter
подібні функції (як дуже корисний itertools
модуль), мабуть, вважаються непітонічними з точки зору стилю.
Лінь
З точки зору ефективності, як і більшість функціональних програм програмування, МАП МОЖЕТ ЛІЗИТИ , а насправді лінивий у пітоні. Це означає, що ви можете це зробити (у 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 підтримує розуміння лінивого списку у вигляді виразів генератора таким чином:
>>> (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 circa-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
клас трохи непрозорий для розбирання, але ми можемо зробити це завдяки нашому тесту на швидкість.
map
і filter
поряд зі стандартною бібліотекою itertools
суттєво поганий стиль. Якщо GvR насправді не каже, що вони були або жахливою помилкою, або виключно для виконання, єдиний природний висновок, якщо саме так говорить "Pythonicness" - це забути про це як про дурне ;-)
map
filter
крап / була чудовою ідеєю для Python 3 , і лише бунт інших Pythonistas утримував їх у вбудованому просторі імен (поки він reduce
був переміщений functools
). Я особисто не погоджуюсь ( map
і filter
чудово lambda
налаштований на заздалегідь визначені, особливо вбудовані, функції, просто ніколи не використовую їх, якщо це знадобиться), але GvR роками називав їх не Pythonic.
itertools
? Частина, яку я цитую у цій відповіді, - це головне твердження, яке мене бентежить. Я не знаю, чи є в його ідеальному світі, map
і filter
хотів би переїхати до itertools
(або functools
) чи повністю піти, але що б це не було, колись хтось каже, що itertools
нефітонічний у всій своїй повноті, то я не знаю, що таке "піфонічний" це повинно означати, але я не думаю, що це може бути щось подібне до "того, що рекомендує людям GvR".
map
/ filter
, ні itertools
. Функціональне програмування прекрасно Pythonic ( itertools
, functools
і operator
були розроблені спеціально з функціональним програмуванням на увазі, і я використовую функціональні ідіоми в Python весь час), і itertools
надає можливості , які були б біль , щоб реалізувати себе, Це конкретно map
і filter
бути надмірними з виразами генератора це змусило Гвідо ненавидіти їх. itertools
завжди було добре.
map
і filter
замість спискові.Мета причина , чому ви повинні віддати перевагу їх , навіть якщо вони не є «Pythonic» полягає в наступному:
вони вимагають функції / лямбда в якості аргументів, які ввести нову область .
Мене це покусало не раз:
for x, y in somePoints:
# (several lines of code here)
squared = [x ** 2 for x in numbers]
# Oops, x was silently overwritten!
але якби замість цього я сказав:
for x, y in somePoints:
# (several lines of code here)
squared = map(lambda x: x ** 2, numbers)
тоді все було б добре.
Можна сказати, що я нерозумно використовую те саме ім’я змінної в тій же області.
Я не був. Спочатку код був чудовим - x
вони не були в одній і тій же області.
Лише після того, як я перемістив внутрішній блок на інший розділ коду, виникла проблема (читайте: проблема під час обслуговування, а не розробка), і я цього не очікував.
Так, якщо ти ніколи не помилишся, то розуміння списку є більш елегантним.
Але з особистого досвіду (і від того, як бачать інші, які роблять ту саму помилку), я бачив, що це трапляється достатньо разів, я вважаю, що це не варто того болю, який вам доведеться зазнати, коли ці помилки прокрадаються у ваш код.
Використовуйте map
і filter
. Вони запобігають тонким важко діагностувати помилки, пов'язані із сферою застосування.
Не забудьте розглянути можливість використання imap
та ifilter
(in itertools
), якщо вони відповідають вашій ситуації!
map
та / або filter
. Якщо що-небудь, то найбільш прямий і логічний переклад, щоб уникнути вашої проблеми, полягає не в тому, map(lambda x: x ** 2, numbers)
а в генераторному виразі, list(x ** 2 for x in numbers)
який не протікає, як уже вказував JeromeJ. Поглянь Мехрдад, не сприймай так особисто, я просто не згоден з твоїми міркуваннями.
Власне, map
і розуміння списку поводиться зовсім по-різному в мові Python 3. Погляньте на наступну програму Python 3:
def square(x):
return x*x
squares = map(square, [1, 2, 3])
print(list(squares))
print(list(squares))
Ви можете очікувати, що він надрукує рядок "[1, 4, 9]" двічі, але замість цього він друкує "[1, 4, 9]", а потім "[]". Перший раз, коли ви дивитесь на squares
це, схоже, що він поводиться як послідовність з трьох елементів, але другий раз як порожній.
У мові Python 2 мова map
повертає звичайний старий список, як і розуміння списку на обох мовах. Суть у тому, що повернене значення map
в Python 3 (і imap
в Python 2) не є списком - це ітератор!
Елементи споживаються, коли ви повторюєте ітератор, на відміну від ітерації списку. Ось чому squares
в останньому print(list(squares))
рядку виглядає порожнім .
Узагальнити:
map
щоб створити структуру даних, а не ітератор. Але, можливо, ліниві ітератори простіші, ніж ледачі структури даних. Їжа для роздумів. Дякую @MnZrK
Я вважаю, що розуміння списку, як правило, більше виражає те, що я намагаюся зробити, ніж те, що map
вони обидва роблять, але колишній економить розумовий навантаження, намагаючись зрозуміти, що може бути складним lambda
виразом.
Там же є десь інтерв'ю (я не можу його знайти навпіл), де Гвідо перераховує lambda
s та функціональні функції як річ, яку він найбільше шкодує про прийняття в Python, так що ви можете зробити аргумент того, що вони не пітонічні в силу того.
const
ключове слово в C ++ є чудовим тріумфом у цих напрямках.
lambda
, вони були зроблені настільки кульгавими (немає заяв ..), що їх важко використовувати і обмежити в будь-якому випадку.
Ось один можливий випадок:
map(lambda op1,op2: op1*op2, list1, list2)
проти:
[op1*op2 for op1,op2 in zip(list1,list2)]
Я здогадуюсь, що zip () - нещасна і непотрібна накладні витрати, на яку вам потрібно потурати, якщо ви наполягаєте на використанні розуміння списку замість карти. Було б чудово, якби хтось уточнив це, будь то ствердно чи негативно.
zip
лінуватися, скориставшисьitertools.izip
map(operator.mul, list1, list2)
. Саме в цих дуже простих лівих бокових виразах розуміння стають незграбними.
Якщо ви плануєте писати будь-який асинхронний, паралельний або розподілений код, ви, мабуть, віддасте перевагу map
перед розумінням списку - оскільки більшість асинхронних, паралельних або розподілених пакетів забезпечують map
функцію перевантаження пітона map
. Тоді, передаючи відповідну map
функцію решті коду, можливо, вам не доведеться змінювати свій початковий серійний код, щоб він працював паралельно (тощо).
Отже, оскільки Python 3 map()
є ітератором, ви повинні мати на увазі, що вам потрібно: ітератор або list
об’єкт.
Як вже зазначалося @AlexMartelli , map()
швидше, ніж розуміння списку, лише якщо ви не використовуєте lambda
функцію.
Я представлю вам порівняння часу.
Python 3.5.2 і CPython
Я використовував ноутбук Юпітера і особливо %timeit
вбудовану магічну команду
Виміри : s == 1000 мс == 1000 * 1000 мкс = 1000 * 1000 * 1000 нс
Налаштування:
x_list = [(i, i+1, i+2, i*2, i-9) for i in range(1000)]
i_list = list(range(1000))
Вбудована функція:
%timeit map(sum, x_list) # creating iterator object
# Output: The slowest run took 9.91 times longer than the fastest.
# This could mean that an intermediate result is being cached.
# 1000000 loops, best of 3: 277 ns per loop
%timeit list(map(sum, x_list)) # creating list with map
# Output: 1000 loops, best of 3: 214 µs per loop
%timeit [sum(x) for x in x_list] # creating list with list comprehension
# Output: 1000 loops, best of 3: 290 µs per loop
lambda
функція:
%timeit map(lambda i: i+1, i_list)
# Output: The slowest run took 8.64 times longer than the fastest.
# This could mean that an intermediate result is being cached.
# 1000000 loops, best of 3: 325 ns per loop
%timeit list(map(lambda i: i+1, i_list))
# Output: 1000 loops, best of 3: 183 µs per loop
%timeit [i+1 for i in i_list]
# Output: 10000 loops, best of 3: 84.2 µs per loop
Існує також таке вираження генератора, див. PEP-0289 . Тому я подумав, що було б корисно додати його до порівняння
%timeit (sum(i) for i in x_list)
# Output: The slowest run took 6.66 times longer than the fastest.
# This could mean that an intermediate result is being cached.
# 1000000 loops, best of 3: 495 ns per loop
%timeit list((sum(x) for x in x_list))
# Output: 1000 loops, best of 3: 319 µs per loop
%timeit (i+1 for i in i_list)
# Output: The slowest run took 6.83 times longer than the fastest.
# This could mean that an intermediate result is being cached.
# 1000000 loops, best of 3: 506 ns per loop
%timeit list((i+1 for i in i_list))
# Output: 10000 loops, best of 3: 125 µs per loop
list
об’єкт:Використовуйте розуміння списку, якщо це спеціальна функція, використовуйте, list(map())
якщо є вбудована функція
list
об’єкт, вам просто потрібен ітерабельний:Завжди використовуйте map()
!
Я провів швидкий тест, порівнюючи три методи для виклику методу об’єкта. Різниця у часі, в даному випадку, є незначним , і є предметом даної функції (див @Alex Мартеллі в відповідь ). Тут я розглянув такі методи:
# map_lambda
list(map(lambda x: x.add(), vals))
# map_operator
from operator import methodcaller
list(map(methodcaller("add"), vals))
# map_comprehension
[x.add() for x in vals]
Я роздивився списки (що зберігаються у змінній vals
) як цілих чисел (Python int
), так і чисел з плаваючою комою (Python float
) для збільшення розмірів списку. Розглядається наступний клас манекенів DummyNum
:
class DummyNum(object):
"""Dummy class"""
__slots__ = 'n',
def __init__(self, n):
self.n = n
def add(self):
self.n += 5
Зокрема, add
метод. __slots__
Атрибут є простою оптимізацією в Python , щоб визначити загальний обсяг пам'яті , необхідний клас (атрибути), зменшуючи розмір пам'яті. Ось отримані сюжети.
Як було сказано раніше, застосовувана техніка має мінімальну різницю, і ви повинні кодувати таким способом, який є найбільш зрозумілим для вас, або за конкретних обставин. У цьому випадку розуміння списку ( map_comprehension
техніка) є найшвидшим для обох типів доповнень в об'єкті, особливо з коротшими списками.
Відвідайте цю пастину для джерела, що використовується для генерування графіку та даних.
map
швидше, лише якщо функція викликається точно таким же чином (тобто [*map(f, vals)]
проти [f(x) for x in vals]
). Так list(map(methodcaller("add"), vals))
швидше, ніж [methodcaller("add")(x) for x in vals]
. map
може не бути швидшим, коли циклічний аналог використовує інший метод виклику, який дозволяє уникнути накладних витрат (наприклад, x.add()
уникає methodcaller
накладних виразів або лямбда). Для цього конкретного тесту, [*map(DummyNum.add, vals)]
буде швидше (тому DummyNum.add(x)
і x.add()
мають в основному однакову продуктивність).
list()
дзвінки дещо повільніше, ніж розуміння списку. Для справедливого порівняння потрібно написати [*map(...)]
.
list()
виклики збільшуються накладні. Якщо ви витратили більше часу на читання відповідей. Я повторно проведу ці тести для справедливого порівняння, хоч які мізерні відмінності можуть бути.
Я вважаю, що найбільш пітонічним способом є використання розуміння списку замість map
і filter
. Причина в тому, що розуміння списку чіткіше, ніж map
та filter
.
In [1]: odd_cubes = [x ** 3 for x in range(10) if x % 2 == 1] # using a list comprehension
In [2]: odd_cubes_alt = list(map(lambda x: x ** 3, filter(lambda x: x % 2 == 1, range(10)))) # using map and filter
In [3]: odd_cubes == odd_cubes_alt
Out[3]: True
Як бачите, розуміння не потребує додаткових lambda
висловлювань як map
потреб. Крім того, розуміння також дозволяє легко фільтрувати, хоча map
вимагає filter
дозволити фільтрацію.
Я спробував код @ alex-martelli, але виявив деякі розбіжності
python -mtimeit -s "xs=range(123456)" "map(hex, xs)"
1000000 loops, best of 5: 218 nsec per loop
python -mtimeit -s "xs=range(123456)" "[hex(x) for x in xs]"
10 loops, best of 5: 19.4 msec per loop
карта займає стільки ж часу навіть для дуже великих діапазонів, а використання розуміння списку займає багато часу, як видно з мого коду. Тому, окрім того, що вважався "непітонічним", я не стикався з жодними проблемами продуктивності, пов'язаними з використанням карти.
map
повертає список. У Python 3 map
оцінюється ліниво, тому просто виклик map
не обчислює жоден з нових елементів списку, отже, чому ви отримуєте такі короткі терміни.