列表理解与地图

有什么理由比列表理解更喜欢使用map() ,反之亦然? 它们中的一个通常比另一个效率更高,或者通常被认为比另一个更Python化吗?


#1楼

在某些情况下, map在微观map可能会更快(当您不是出于此目的而使用lambda时,而是在map和listcomp中使用相同的功能)。 在其他情况下,列表理解可能会更快,大多数(并非全部)pythonista用户认为列表更直接,更清晰。

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

当地图需要使用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

#2楼

我发现列表理解通常比map更能表达我要执行的操作-它们都可以完成操作,但是前者节省了试图了解什么可能是复杂的lambda表达式的精神负担。

在某个地方(我无法找到它)也有一次采访,其中Guido列出了lambda和函数功能,这是他最后悔接受Python的事情,因此您可以通过以下方式来论证它们是非Python的:的优点。


#3楼

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

即使它们不是“ Pythonic”的,您还是还是偏爱它们的一个客观原因是:
它们需要函数/ 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 )!


#4楼

这是一种可能的情况:

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

与:

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

我猜想zip()是一个不幸的和不必要的开销,如果您坚持使用列表推导而不是地图,则需要沉迷于此。 如果有人肯定或否定这一点,那就太好了。


#5楼

实际上, map和list的理解在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(和Python 2中的imapmap的返回值不是列表,而是一个迭代器!

与遍历列表不同,遍历迭代器时将消耗元素。 这就是为什么squares在最后一个print(list(squares))行中看起来是空的。

总结一下:

  • 在处理迭代器时,必须记住它们是有状态的,并且在遍历它们时会发生变化。
  • 列表更容易预测,因为它们仅在您显式对其进行更改时才会更改; 他们是容器
  • 还有一个好处:数字,字符串和元组甚至可以更容易预测,因为它们根本无法更改; 它们是价值

#6楼

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


#7楼

因此,由于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()


#8楼

我认为最Python的方式是使用列表理解而不是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

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


#9楼

我进行了一项快速测试,比较了三种调用对象方法的方法。 在这种情况下,时差可以忽略不计,并且与所讨论的功能有关(请参阅@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_comprehensionmap_comprehension技术)对于对象中两种类型的加法最快,尤其是对于较短的列表。

访问此pastebin以获取用于生成图和数据的源。


#10楼

案例

  • 常见情况 :几乎总是,您将要在python中使用列表推导,因为对于新手程序员来说,阅读代码会更加明显。 (这不适用于可能适用其他习惯用法的其他语言。)由于列表推导是python中用于迭代的事实上的标准,因此您对python程序员所做的工作甚至会更加明显。 他们是预期的
  • 较少见的情况 :但是,如果已经定义了函数 ,则使用map通常是合理的,尽管它被认为是“非pythonic”的。 例如, map(sum, myLists)[sum(x) for x in myLists]更为优雅/简洁。 您可以不必键入一个虚拟变量(例如sum(x) for x...sum(_) for _...sum(readableName) for readableName... ),从而获得优雅的sum(_) for _...两次,只是要迭代。 相同的参数适用于filterreduce以及itertools模块中的任何内容:如果您已经有了函数,可以继续进行一些函数式编程。 在某些情况下,这会提高可读性,而在其他情况下(例如,新手程序员,多个参数),则会失去可读性。但是,无论如何,代码的可读性在很大程度上取决于注释。
  • 几乎永远不会 :在执行函数编程时,您可能想将map函数用作纯抽象函数,在此情况下,您正在映射map或currying 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 -loop 。 从功能编程的角度来看,虽然不是那么优雅,但有时​​非局部变量使命令式编程语言(例如python)中的代码更清晰,因为人们已经非常习惯以这种方式阅读代码。 通常,当您仅执行任何不构建列表的复杂操作(例如列表理解和映射)(例如,求和或制作树等)时,for循环也是最有效的。就内存而言,它是高效的(不必在时间上,我希望在最坏的情况下,它是一个恒定的因素,除非出现一些罕见的病理性垃圾收集问题)。

“ Python主义”

我不喜欢“ pythonic”这个词,因为我发现pythonic在我眼中并不总是那么优雅。 但是,就样式而言, mapfilter以及类似的功能(例如非常有用的itertools模块)可能被认为是非Python的。

懒惰

就效率而言,就像大多数函数式编程构造一样, MAP可以是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...输入[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], ...的顺序求x[0], x[1], x[2], ...

但是,假设我们有一个要map的预制函数f ,并且我们通过立即强制使用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是在带有python 3。?。?的大约2010年英特尔工作站上执行的,而B和C是在带有python 3.2.1的大约2013年AMD工作站上执行的,具有截然不同的硬件。 结果似乎是,地图和列表理解的性能可比,这受其他随机因素的影响最大。 我们可以告诉的唯一的事情似乎是,奇怪的是,虽然我们期待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            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类在反汇编方面有点不透明,但是我们可以通过速度测试来确定。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值