列表推导与map比较

971 投票
14 回答
336472 浏览
提问于 2025-04-15 13:27

有没有什么理由让我们更喜欢用 map() 而不是列表推导式,或者反过来呢?这两者在效率上有没有谁更好,或者说哪种写法更符合 Python 的风格呢?

14 个回答

112

Python 2:你应该使用 mapfilter,而不是列表推导式。

一个客观的理由是,尽管它们看起来不太“Python风格”,但你还是应该更倾向于使用它们:
因为它们需要函数或匿名函数作为参数,这引入了新的作用域

我曾经因此吃过亏不止一次:

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 并不在同一个作用域里。
问题出现在我内部代码块移动到代码的其他部分后(注意:这是维护时的问题,不是开发时的问题),而我没有预料到这一点。

是的,如果你从来不犯这个错误,那么列表推导式确实更优雅。
但根据我的个人经验(以及看到其他人犯同样的错误),我见过太多次这种情况,所以我觉得当这些bug悄悄进入你的代码时,所经历的痛苦是完全不值得的。

结论:

使用 mapfilter。它们可以防止一些难以诊断的作用域相关的bug。

附注:

如果适合你的情况,别忘了考虑使用 imapifilter(在 itertools 中)!

600

情况说明

  • 常见情况: 在python中,几乎总是建议使用列表推导式,因为这样能让初学者更容易理解你的代码。(这在其他语言中可能不适用,其他语言有其他的写法。)对于python程序员来说,列表推导式是迭代的标准写法,大家都习惯了。
  • 不太常见的情况: 不过,如果你已经定义了一个函数,那么使用map也是合理的,尽管这被认为是不太“python风格”的。例如,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循环: 当然,你也可以直接使用for循环。虽然从函数式编程的角度看不够优雅,但在像python这样的命令式编程语言中,有时非局部变量能让代码更清晰,因为人们习惯于这样阅读代码。当你只是进行一些复杂操作而不是像列表推导式和map那样构建列表时,for循环通常是最有效的——至少在内存方面是有效的(不一定在时间上,最坏情况下我认为也只是一个常数因子,除非遇到一些罕见的垃圾回收问题)。

“Python风格”

我不喜欢“pythonic”这个词,因为在我看来,pythonic并不总是优雅的。不过,mapfilter和类似的函数(比如非常有用的itertools模块)在风格上可能被认为是不太“python风格”的。

懒惰

在效率方面,像大多数函数式编程构造一样,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中相对于大多数函数式语言是非常有限的:你只有在按顺序从左到右访问数据时才能获得这个好处,因为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和列表推导式在性能上是可比的,最强烈地受到其他随机因素的影响。我们唯一能确定的是,奇怪的是,尽管我们预期列表推导式[...]的性能会优于生成器表达式(...),但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类在反汇编时有点不透明,但我们可以通过速度测试来得出结论。

853

在某些情况下,使用 map 可能会稍微快一点(尤其是当你不是为了这个目的而写一个 lambda 函数,而是直接在 map列表推导式 中使用同一个函数时)。而在其他情况下,列表推导式可能会更快,而且大多数(不是全部)Python 爱好者认为它们更直接、更清晰。

这是一个使用完全相同函数时 map 的微小速度优势的例子:

$ python -m timeit -s'xs=range(10)' 'map(hex, xs)'
100000 loops, best of 3: 4.86 usec per loop

$ python -m timeit -s'xs=range(10)' '[hex(x) for x in xs]'
100000 loops, best of 3: 5.58 usec per loop

这是一个当 map 需要一个 lambda 函数时,性能比较完全反转的例子:

$ python -m timeit -s'xs=range(10)' 'map(lambda x: x+2, xs)'
100000 loops, best of 3: 4.24 usec per loop

$ python -m timeit -s'xs=range(10)' '[x+2 for x in xs]'
100000 loops, best of 3: 2.32 usec per loop

撰写回答