列表理解vs map

是否有理由更喜欢使用map()而不是列表理解?它们中的任何一个通常比另一个更有效还是被认为更Pythonic?

301003 次浏览

在某些情况下,map可能会更快(当你没有为此目的制作lambda,而是在map和listcomp中使用相同的函数时)。列表理解在其他情况下可能会更快,大多数(不是全部)Pythonista认为它们更直接、更清晰。

使用完全相同的函数时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

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

还有一次采访(我无法立即找到它),其中Guido列出了lambda和函数函数,这是他最后悔接受Python的事情,所以你可以认为它们是非Pythonic的。

这里有一个可能的案例:

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

对:

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

我猜zip()是一个不幸的和不必要的开销,如果你坚持使用列表理解而不是地图,你需要沉迷于它。如果有人澄清这一点,无论是肯定的还是否定的,那就太好了。

案件

  • 常见案例:几乎总是,你会想要在python中使用列表推导,因为对于阅读你代码的新手程序员来说,你所做的事情会更加明显。(这不适用于其他语言,其他习语可能适用。)你对python程序员所做的事情甚至会更加明显,因为列表推导是python中事实上的迭代标准;它们是预计
  • 不太常见的病例:但是如果你是map(sum, myLists)0,使用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]
  • 使用#0循环:当然,你也可以只使用for循环。虽然从函数式编程的角度来看没有那么优雅,但有时非局部变量会使命令式编程语言(如python)中的代码更清晰,因为人们非常习惯于以这种方式阅读代码。通常,当你只是做任何复杂的操作而不是构建列表时,for-循环也是最有效的,比如列表理解和映射已经过优化(例如求和或制作树等)-至少在内存方面是有效的(不一定是在时间方面,我认为最坏的情况下是一个常数因子,除非一些罕见的病态垃圾收集打嗝)。

“Pythonism”

我不喜欢“pythonic”这个词,因为我觉得pythonic在我眼里并不总是优雅的。尽管如此,mapfilter以及类似的功能(比如非常有用的itertools模块)可能在风格上被认为是非pythonic的。

懒惰

就效率而言,像大多数函数式编程结构一样,地图可以很懒实际上在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 negprint({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年使用python 3.?.?的英特尔工作站上执行的,B和C是在大约2013年使用python 3.2.1的AMD工作站上执行的,硬件截然不同。结果似乎是map和list理解在性能上是可比较的,这是受其他随机因素影响最强烈的。我们唯一能说的似乎是,奇怪的是,虽然我们期望列表理解[...]比生成器表达式(...)表现得更好,但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            06 LOAD_NAME                0 (xs)9 GET_ITER10 CALL_FUNCTION            113 RETURN_VALUE>>> listComp.co_consts(<code object <listcomp> at 0x2511a48, file "listComp", line 1>,)>>> dis.dis(listComp.co_consts[0])1           0 BUILD_LIST               03 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            121 LIST_APPEND              224 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            09 LOAD_NAME                1 (xs)12 GET_ITER13 CALL_FUNCTION            116 CALL_FUNCTION            119 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            118 YIELD_VALUE19 POP_TOP20 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            215 CALL_FUNCTION            118 RETURN_VALUE

似乎使用[...]语法比list(...)更好。可悲的是,map类对反汇编有点不透明,但我们可以通过速度测试来实现。

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

即使它们不是“Pythonic”,你也应该更喜欢它们的0个原因是:
它们需要函数/lambda作为参数,引入新的范围

我被这个咬了不止一次:

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*xsquares = 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函数传递给你的其余代码,你可能不必修改你的原始串行代码来让它并行运行(等)。

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

因为@AlexMartelli已经提到了,只有当你不使用lambda函数时,map()才比列表理解更快。

我会给你一些时间比较。

Python 3.5.2和CPython
我使用了木星笔记本,尤其是%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()

我认为最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_altOut[3]: True

如你所见,理解不需要map需要的额外lambda表达式。此外,理解还允许轻松过滤,而map需要filter来允许过滤。

我运行了一个快速测试,比较了调用对象方法的三种方法。在这种情况下,时间差可以忽略不计,这是一个有问题的函数的问题(参见@Alex Martelli的反应)。在这里,我查看了以下方法:

# map_lambdalist(map(lambda x: x.add(), vals))
# map_operatorfrom operator import methodcallerlist(map(methodcaller("add"), vals))
# map_comprehension[x.add() for x in vals]

我查看了整数(Pythonint)和浮点数(Pythonfloat)的列表(存储在变量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技术)对于对象中的两种添加类型都是最快的,尤其是对于较短的列表。

访问这个纸箱以获取用于生成绘图和数据的源。

我尝试了@alex-martelli的代码,但发现了一些差异

python -mtimeit -s "xs=range(123456)" "map(hex, xs)"1000000 loops, best of 5: 218 nsec per looppython -mtimeit -s "xs=range(123456)" "[hex(x) for x in xs]"10 loops, best of 5: 19.4 msec per loop

即使对于非常大的范围,map也需要相同的时间,而使用列表理解需要很多时间,这从我的代码中可以明显看出。因此,除了被认为是“unpythonic”之外,我没有遇到任何与使用map相关的性能问题。

输入图片描述

图片来源:专家

你可以自己看看列表理解和地图功能之间哪个更好

(与map函数相比,列表理解需要更少的时间来处理100万记录)

希望有帮助!祝你好运:)

我用灌流图(我的一个项目)对一些结果进行了计时。

正如其他人所指出的,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 perfplotimport 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慢,因为传递和返回参数,这就是我发现这篇文章的原因。

我相信使用map可以更具可读性和灵活性,特别是当我需要构建列表的值时。

如果你使用map,你实际上理解它。

def pair_list_items(*args):return args
packed_list = list(map(pair_list_items,lista, *listb, listc.....listn))

加上灵活性奖金。感谢所有其他答案,以及绩效奖金。