簡體   English   中英

列表理解與 map

[英]List comprehension vs map

是否有理由更喜歡使用map()而不是列表理解,反之亦然? 它們中的任何一個通常比另一個更有效還是通常被認為更像 Pythonic?

在某些情況下, 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

案例

  • 常見情況:幾乎總是,您會希望在Python 中使用列表推導式,因為對於閱讀您的代碼的新手程序員來說,您所做的事情會更加明顯。 (這不適用於其他語言,其他習語可能適用。)對於 Python 程序員而言,您所做的事情甚至會更加明顯,因為列表推導式是 Python 中事實上的迭代標准; 他們是意料之中的
  • 不太常見的情況:但是,如果您已經定義了一個函數,則使用map通常是合理的,盡管它被認為是“unpythonic”。 例如, map(sum, myLists)[sum(x) for x in myLists]更優雅/簡潔。 您獲得了不必組成虛擬變量(例如sum(x) for x...sum(_) for _...sum(readableName) for readableName... )的優雅,您必須鍵入兩次,只是為了迭代。 同樣的論點也適用於filterreduce以及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在我眼中總是優雅的。 盡管如此, mapfilter以及類似的函數(如非常有用的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類對於反匯編來說有點不透明,但我們可以通過我們的速度測試來彌補。

Python 2:您應該使用mapfilter而不是列表推導式。

即使它們不是“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不在同一個范圍內。
只是在我內部塊移動到代碼的不同部分后才出現問題(閱讀:維護期間的問題,而不是開發期間的問題),我沒想到。

是的,如果你從不犯這個錯誤,那么列表推導會更優雅。
但是根據個人經驗(以及看到其他人犯同樣的錯誤),我已經看到這種情況發生的次數太多了,我認為當這些錯誤潛入您的代碼時,您必須經歷的痛苦是不值得的。

結論:

使用mapfilter 它們可以防止與范圍相關的難以診斷的細微錯誤。

邊注:

如果它們適合您的情況,請不要忘記考慮使用imapifilter (在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 中的一個簡單優化,用於定義類(屬性)所需的總內存,減少內存大小。 這是結果圖。

映射 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”之外,我還沒有遇到任何與地圖使用相關的性能問題。

在此處輸入圖片說明

圖片來源:Experfy

你可以自己看看哪個更好 - List Comprehension 和 Map Function

(與 map 函數相比,List Comprehension 處理 100 萬條記錄所需的時間更少)

希望能幫助到你! 祝你好運 :)

我認為最 Pythonic 的方法是使用列表理解而不是mapfilter 原因是列表推導式比mapfilter更清晰。

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才能進行過濾。

我用perfplot (我的一個項目)對一些結果進行計時

正如其他人所指出的, 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM