关于Python list 和 map的理解

List comprehension vs map

是否有理由更喜欢使用map()而不是列表理解,或者相反?他们中的一个通常比另一个更有效还是被认为比另一个更通吃?


在某些情况下,map在显微镜下可能更快(当您不是为此目的而制作lambda,而是在map和listcomp中使用相同的函数时)。在其他情况下,清单理解可能更快,而且大多数(不是全部)Python患者认为它们更直接、更清晰。

使用完全相同的功能时地图的微小速度优势示例:

1
2
3
4
$ 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

当映射需要lambda时,性能比较如何完全颠倒的示例:

1
2
3
4
$ 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通常是合理的,尽管它被认为是"非对称的"。例如,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),它是一个单衬层,大致相当于:

好的。

1
2
def sumEach(myLists):
    return [sum(_) for _ in myLists]
  • 只使用一个for循环:当然也可以使用for循环。虽然从函数式编程的角度看没有那么优雅,但有时非局部变量在诸如python之类的命令式编程语言中使代码更清晰,因为人们非常习惯这样读取代码。对于循环,一般来说,当你仅仅是做一些复杂的操作,而不是构建一个像列表这样的列表理解和映射时,它是最有效的(例如求和,或者生成一棵树等等),至少在内存方面是最有效的(不一定是在时间方面,在最坏的情况下,我期望一个常量因子,除了一些罕见的病理性垃圾收集打嗝)。

"巨蛇座"好的。

我不喜欢"Python"这个词,因为我不觉得我眼中的Python总是那么优雅。然而,从风格上来说,mapfilter以及类似的功能(如非常有用的itertools模块)可能被认为是不对等的。好的。

懒惰好的。

在效率方面,和大多数函数式编程构造一样,map可能是懒惰的,实际上在python中也是懒惰的。这意味着您可以这样做(在python3中),并且您的计算机不会耗尽内存并丢失所有未保存的数据:好的。

1
2
>>> map(str, range(10**100))
<map object at 0x2201d50>

试着用一个清单来理解:好的。

1
2
>>> [str(n) for n in range(10**100)]
# DO NOT TRY THIS AT HOME OR YOU WILL BE SAD #

请注意,列表理解本身也是懒惰的,但Python选择将其实现为非懒惰的。然而,python支持生成器表达式形式的惰性列表理解,如下所示:好的。

1
2
>>> (str(n) for n in range(10**100))
<generator object <genexpr> at 0xacbdef>

基本上可以将[...]语法看作是将生成器表达式传递给列表构造函数,如list(x for x in range(5))。好的。

简单的人为例子好的。

1
2
3
4
5
6
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...那样冗长。只要保持迭代器变量简短,如果不缩进代码,列表理解通常会更清晰。但是,您总是可以缩进代码。好的。

1
2
3
print(
    {x:x**2 for x in (-y for y in range(5))}
)

或者打破现状:好的。

1
2
3
4
rangeNeg5 = (-y for y in range(5))
print(
    {x:x**2 for x in rangeNeg5}
)

python3的效率比较好的。

map现在懒惰了:好的。

1
2
% 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,我们忽略了map的懒惰,立即用list(...)强制评估。我们得到了一些非常有趣的结果:好的。

1
2
3
4
5
6
7
8
9
10
11
% 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>)

结果以a a a/bbb/ccc的形式出现,其中a是在Circa-2010 Intel工作站上用python 3执行的。?,B和C使用Circa-2013 AMD工作站和python 3.2.1执行,硬件非常不同。其结果似乎是地图和列表理解在性能上具有可比性,这是受其他随机因素影响最大的。我们唯一能说的似乎是,奇怪的是,虽然我们期望清单理解[...]比生成器表达式(...)执行得更好,但map也比生成器表达式更有效(再次假设所有值都被计算/使用)。好的。

重要的是要认识到,这些测试假定了一个非常简单的函数(标识函数);但是这是很好的,因为如果函数很复杂,那么与程序中的其他因素相比,性能开销可以忽略不计。(用其他简单的东西如f=lambda x:x+x进行测试可能仍然很有趣)好的。

如果您擅长阅读python程序集,那么可以使用dis模块查看这是否是幕后发生的事情:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
>>> 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

nbsp;好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> 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

nbsp;好的。

1
2
3
4
5
6
7
8
9
>>> 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类对于反汇编有点不透明,但是我们可以通过速度测试来实现。好的。好啊。


你应该使用mapfilter,而不是列表理解。

即使他们不是"Python"你也应该喜欢他们的一个客观原因是:它们需要函数/lambda作为参数,这将引入一个新的范围。

我被这个咬了不止一次:

1
2
3
4
for x, y in somePoints:
    # (several lines of code here)
    squared = [x ** 2 for x in numbers]
    # Oops, x was silently overwritten!

但如果我说:

1
2
3
for x, y in somePoints:
    # (several lines of code here)
    squared = map(lambda x: x ** 2, numbers)

那么一切都会好起来的。

你可以说我在同一个范围内使用相同的变量名是愚蠢的。

我没有。代码本来很好——两个x不在同一范围内。只有在我将内部块移动到代码的另一部分之后,问题才出现(请阅读:维护期间的问题,而不是开发过程中的问题),我没有预料到。

是的,如果你从未犯过这个错误,那么列表理解就更优雅了。但是从个人经验(以及从看到别人犯同样的错误)来看,我已经看到这种情况发生了很多次,以至于我认为当这些bug蔓延到代码中时,你所经历的痛苦是不值得的。

结论:

使用mapfilter。它们可以防止细微的难以诊断的范围相关的错误。

边注:

如果适合您的情况,不要忘记考虑使用imapifilter(在itertools中)!


实际上,在python 3语言中,map和list理解的行为非常不同。看看下面的python 3程序:

1
2
3
4
5
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中EDOCX1的返回值(python 2中EDOCX1的返回值(9))不是一个列表-它是一个迭代器!

元素在迭代迭代器时使用,与在列表上迭代时不同。这就是为什么squares在最后一行中看起来是空的。

总结:

  • 在处理迭代器时,必须记住它们是有状态的,并且在遍历它们时它们会发生变化。
  • 列表更容易预测,因为它们只在您显式地改变它们时发生变化;它们是容器。
  • 还有一个好处:数字、字符串和元组更容易预测,因为它们根本不能改变;它们是值。


如果您计划编写任何异步、并行或分布式代码,那么您可能更喜欢使用map而不是列表理解,因为大多数异步、并行或分布式包都提供了map函数来重载python的map。然后,通过将适当的map函数传递给代码的其余部分,您可能不必修改原始的串行代码,使其并行运行(等等)。


我发现列表理解通常比map更能表达我想做的事情,它们都能完成,但前者省去了理解复杂lambda表达的心理负担。

还有一个采访在那里(我不能马上找到),在那里guido列出了lambda,功能功能功能是他最后悔接受进入python的东西,所以你可以这样认为他们是非pythonic的。


这里有一个可能的例子:

1
map(lambda op1,op2: op1*op2, list1, list2)

对比:

1
[op1*op2 for op1,op2 in zip(list1,list2)]

我猜zip()是一种不幸的、不必要的开销,如果您坚持使用列表理解而不是地图,那么您需要投入其中。如果有人能肯定地或否定地澄清这一点,那就太好了。


因此,由于python 3,map()是一个迭代器,您需要记住您需要什么:迭代器或list对象。

正如@alexmartelli已经提到的,只有在不使用lambda函数的情况下,map()才比列表理解更快。

我会给你一些时间比较。

python 3.5.2和cpythoni使用了jupiter笔记本,特别是%timeit内置的magic命令测量值:s==1000 ms==1000*1000μs=1000*1000*1000 ns

设置:

1
2
x_list = [(i, i+1, i+2, i*2, i-9) for i in range(1000)]
i_list = list(range(1000))

内置功能:

1
2
3
4
5
6
7
8
9
10
%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功能:

1
2
3
4
5
6
7
8
9
10
%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。所以我认为把它加入比较是有用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
%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()


我认为最偏执的方法是使用列表理解,而不是使用mapfilter。原因是清单理解比mapfilter更清晰。

1
2
3
4
5
6
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允许过滤。