[英]List comprehension vs map
在某些情況下, map
可能在微觀上更快(當您不是為此目的創建 lambda 時,而是在 map 和 listcomp 中使用相同的函數時)。 在其他情況下,列表推導可能會更快,並且大多數(不是全部)pythonistas 認為它們更直接和更清晰。
使用完全相同的函數時 map 的微小速度優勢的示例:
$ 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
當 map 需要 lambda 時,性能比較如何完全顛倒的示例:
$ 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
通常是合理的,盡管它被認為是“unpythonic”。 例如, 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 循環也是最有效的,例如列表推導式和地圖優化(例如求和或制作樹等)——至少在內存方面高效(不一定在時間方面,我認為最壞的情況是一個常數因素,除非一些罕見的病理性垃圾收集打嗝)。“蟒蛇主義”
我不喜歡“pythonic”這個詞,因為我不覺得pythonic在我眼中總是優雅的。 盡管如此, map
和filter
以及類似的函數(如非常有用的itertools
模塊)在風格方面可能被認為是非 Pythonic 的。
懶惰
在效率方面,和大多數函數式編程結構一樣, MAP CAN BE LAZY ,實際上在 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 ^^^^^^^^^
因此,如果您不會使用所有數據,或者不提前知道您需要多少數據,在 python3 中map
(以及在 python2 或 python3 中的生成器表達式)將避免計算它們的值,直到需要的最后一刻。 通常這通常會超過使用map
任何開銷。 不利的一面是,與大多數函數式語言相比,這在 python 中非常有限:只有當您“按順序”從左到右訪問數據時,您才能獲得這種好處,因為 python 生成器表達式只能按順序計算x[0], x[1], x[2], ...
.
然而,假設我們有一個預制的函數f
我們想要map
,並且我們通過立即強制使用list(...)
評估來忽略map
的惰性。 我們得到了一些非常有趣的結果:
% 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 是在 2010 年左右的 Intel 工作站上使用 python 3.?.? 執行的,B 和 C 是在 2013 年左右的 AMD 工作站上使用 python 3.2.1 執行的具有極其不同的硬件。 結果似乎是 map 和 list comprehensions 在性能上具有可比性,這受其他隨機因素的影響最大。 我們唯一能說的似乎是,奇怪的是,雖然我們期望列表推導式[...]
比生成器表達式(...)
表現得更好,但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
而不是列表推導式。 即使它們不是“Pythonic”,您也應該更喜歡它們的客觀原因是:
它們需要函數/lambdas 作為參數,這引入了一個新的 scope 。
我不止一次被這個咬過:
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
(在itertools
)!
實際上, 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
返回一個普通的舊列表,就像列表理解在兩種語言中所做的那樣。 關鍵是 Python 3 中的map
(以及 Python 2 中的imap
)的返回值不是一個列表——它是一個迭代器!
迭代迭代器時會消耗元素,這與迭代列表時不同。 這就是為什么squares
在最后一個print(list(squares))
行中看起來是空的。
總結一下:
如果您計划編寫任何異步、並行或分布式代碼,您可能更喜歡map
而非列表推導式——因為大多數異步、並行或分布式包都提供了一個map
函數來重載 python 的map
。 然后通過將適當的map
函數傳遞給其余代碼,您可能不必修改原始串行代碼以使其並行運行(等)。
這是一種可能的情況:
map(lambda op1,op2: op1*op2, list1, list2)
相對:
[op1*op2 for op1,op2 in zip(list1,list2)]
我猜 zip() 是一個不幸且不必要的開銷,如果您堅持使用列表推導而不是地圖,則需要沉迷其中。 如果有人以肯定或否定的方式澄清這一點,那就太好了。
我發現列表推導式通常比map
更能表達我想要做的事情——它們都完成了,但前者省去了試圖理解什么可能是復雜的lambda
表達式的心理負擔。
還有一個采訪在某處(我無法立即找到),其中 Guido 將lambda
和函數列為他最后悔接受 Python 的事情,因此您可以通過以下方式論證它們不是 Pythonic憑借這一點。
所以由於 Python 3, map()
是一個迭代器,你需要記住你需要什么:迭代器或list
對象。
正如@AlexMartelli 已經提到的,只有當您不使用lambda
函數時, map()
才比列表理解更快。
我將向您展示一些時間比較。
Python 3.5.2 和 CPython
我使用過Jupiter notebook ,尤其是%timeit
內置魔法命令
測量:s == 1000 ms == 1000 * 1000 µs = 1000 * 1000 * 1000 ns
設置:
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 Martelli 的回復)。 在這里,我查看了以下方法:
# 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]
我查看了整數(Python int
)和浮點數(Python float
)的列表(存儲在變量vals
)以增加列表大小。 考慮以下虛擬類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
技術)對於對象中的兩種類型的添加都是最快的,尤其是對於較短的列表。
訪問此 pastebin以獲取用於生成繪圖和數據的源。
我嘗試了@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 也需要相同的時間,而使用列表理解需要大量時間,這從我的代碼中可以看出。 因此,除了被認為是“非pythonic”之外,我還沒有遇到任何與地圖使用相關的性能問題。
我認為最 Pythonic 的方法是使用列表理解而不是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
所需的額外lambda
表達式。 此外,理解還允許輕松過濾,而map
需要filter
才能進行過濾。
正如其他人所指出的, map
實際上只返回一個迭代器,因此它是一個恆定時間的操作。 當通過list()
實現迭代器時,它與列表推導式相當。 根據表達的不同,任何一個都可能有輕微的優勢,但並不重要。
請注意,像x ** 2
這樣的算術運算在 NumPy 中要快得多,尤其是在輸入數據已經是 NumPy 數組的情況下。
hex
:
x ** 2
:
重現圖的代碼:
import perfplot
def standalone_map(data):
return map(hex, data)
def list_map(data):
return list(map(hex, data))
def comprehension(data):
return [hex(x) for x in data]
b = perfplot.bench(
setup=lambda n: list(range(n)),
kernels=[standalone_map, list_map, comprehension],
n_range=[2 ** k for k in range(20)],
equality_check=None,
)
b.save("out.png")
b.show()
import perfplot
import numpy as np
def standalone_map(data):
return map(lambda x: x ** 2, data[0])
def list_map(data):
return list(map(lambda x: x ** 2, data[0]))
def comprehension(data):
return [x ** 2 for x in data[0]]
def numpy_asarray(data):
return np.asarray(data[0]) ** 2
def numpy_direct(data):
return data[1] ** 2
b = perfplot.bench(
setup=lambda n: (list(range(n)), np.arange(n)),
kernels=[standalone_map, list_map, comprehension, numpy_direct, numpy_asarray],
n_range=[2 ** k for k in range(20)],
equality_check=None,
)
b.save("out2.png")
b.show()
我的用例:
def sum_items(*args):
return sum(args)
list_a = [1, 2, 3]
list_b = [1, 2, 3]
list_of_sums = list(map(sum_items,
list_a, list_b))
>>> [3, 6, 9]
comprehension = [sum(items) for items in iter(zip(list_a, list_b))]
我發現自己開始使用更多 map,我認為 map 可能比 comp 慢,因為傳遞和返回 arguments,這就是我找到這篇文章的原因。
我相信使用 map 可能更具可讀性和靈活性,尤其是當我需要構建列表的值時。
如果你用過map,其實看了你就明白了。
def pair_list_items(*args):
return args
packed_list = list(map(pair_list_items,
lista, *listb, listc.....listn))
加上靈活性獎金。 並感謝所有其他答案,以及績效獎金。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.