列表推导式 vs Map

2024-11-20 08:43:00
admin
原创
9
摘要:问题描述:是否有理由更喜欢使用map()而不是列表推导式,反之亦然?它们中的任何一个通常都比另一个更有效率,或者被认为通常更符合 Python 风格吗?解决方案 1:在某些情况下, map可能在微观上更快(当您不为此目的创建 lambda 时,但在 map 和列表推导中使用相同的函数)。在其他情况下,列表推导...

问题描述:

是否有理由更喜欢使用map()而不是列表推导式,反之亦然?它们中的任何一个通常都比另一个更有效率,或者被认为通常更符合 Python 风格吗?


解决方案 1:

在某些情况下, map可能在微观上更快(当您为此目的创建 lambda 时,但在 map 和列表推导中使用相同的函数)。在其他情况下,列表推导可能更快,并且大多数(并非全部)Pythonistas 认为它​​们更直接、更清晰。

使用完全相同的功能时,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

解决方案 2:

案例

  • 常见情况:几乎总是,你会想要在Python中使用列表推导,因为对于阅读代码的新手程序员来说,你正在做的事情会更加明显。(这不适用于其他语言,其他习语可能适用。)对于 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-loop:当然,您也可以只使用 for 循环。虽然从函数式编程的角度来看,它并不那么优雅,但有时​​非局部变量会使命令式编程语言(如 Python)中的代码更清晰,因为人们非常习惯以这种方式阅读代码。当您仅执行任何复杂的操作(而不是构建列表,如列表推导和映射)时,For 循环通常也是最有效的(例如求和或创建树等)——至少在内存方面是有效的(不一定在时间方面,我预计最坏的情况是常数因子,除非出现一些罕见的病态垃圾收集故障)。

“Python主义”

我不喜欢“pythonic”这个词,因为在我看来,pythonic 并不总是优雅的。尽管如此,map以及filter类似的函数(如非常有用的itertools模块)在风格上可能被认为是非 Pythonic 的。

懒惰

在效率方面,与大多数函数式编程构造一样,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            ^^^^^^^^^

因此,如果您不会使用所有数据,或者事先不知道需要多少数据,mappython3(以及 python2 或 python3 中的生成器表达式)将避免计算它们的值,直到最后一刻才需要。通常,这通常会超过使用 的任何开销map。缺点是,与大多数函数式语言相比,这在 python 中非常有限:只有当您按顺序从左到右访问数据时,您才能获得这种好处,因为 python 生成器表达式只能按 顺序进行评估x[0], x[1], x[2], ...

但是,假设我们有一个预先制作的函数,f我们希望,并且我们通过立即强制求值map来忽略 的惰性。我们得到了一些非常有趣的结果:map`list(...)`

% 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对于反汇编来说有点不透明,但我们可以通过速度测试来应付。

解决方案 3:

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

尽管它们不是“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)

那么一切都会好起来的。

您可能会说我在同一范围内使用相同的变量名是愚蠢的。

我没有。代码原本没问题——这两个xs 不在同一范围内。

只是在我内部块移到代码的不同部分后才出现问题(阅读:维护期间的问题,而不是开发期间的问题),这是我没有想到的。

是的,如果你从不犯这个错误,那么列表推导会更优雅。

但从个人经验(以及看到其他人犯同样的错误)来看,我已经看到这种情况发生了很多次,我认为当这些错误潜入你的代码时,你必须经历的痛苦是不值得的。

结论:

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

附注:

不要忘记考虑使用imapifilter(在itertools)它们是否适合您的情况!

解决方案 4:

实际上,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返回一个普通的列表,就像两种语言中的列表推导一样。关键在于,map在 Python 3(和imapPython 2)中,返回的值不是列表 - 而是一个迭代器!

与迭代列表不同,迭代迭代器时会消耗元素。这就是squares最后print(list(squares))一行看起来为空的原因。

总结一下:

  • 处理迭代器时,您必须记住它们是有状态的,并且在遍历它们时它们会发生变异。

  • 列表更可预测,因为它们只在你明确改变它们时才会改变;它们是容器

  • 另外还有一个好处:数字、字符串和元组更加可预测,因为它们根本无法改变;它们是

解决方案 5:

以下是一种可能的情况:

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

相对:

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

我猜如果你坚持使用列表推导而不是映射,zip() 是一个不幸且不必要的开销,你需要沉迷其中。如果有人能澄清这一点,无论是肯定的还是否定的,那就太好了。

解决方案 6:

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

解决方案 7:

我发现列表推导通常比它们更能表达我想要做的事情map- 它们都可以完成任务,但是前者节省了试图理解可能是一个复杂lambda表达式的脑力负担。

在某个地方还有一篇采访(我没能一下子找到它),其中 Guido 列出了lambdas 和函数式函数,这是他最后悔接受 Python 的东西,因此你可以据此争辩说它们不符合 Python 风格。

解决方案 8:

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

正如@AlexMartelli 已经提到的,map()仅当您不使用函数时才比列表理解更快lambda

我将向您展示一些时间比较。

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

解决方案 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]

vals我查看了整数 (Python int) 和浮点数 (Python ) 的列表(存储在变量中),以增加列表大小。考虑float以下虚拟类: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获取用于生成图表和数据的来源。

解决方案 10:

我使用perfplot(我的一个项目)对一些结果进行了计时。

正如其他人所指出的,map实际上只返回一个迭代器,因此它是一个恒定时间操作。当通过实现迭代器时list(),它与列表推导相当。根据表达式,任何一个都可能略有优势,但几乎不重要。

请注意,在 NumPy 中类似的算术运算x ** 2快得多,特别是当输入数据已经是 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()

解决方案 11:

我尝试了@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 所花费的时间也相同,而使用列表推导式则需要花费大量时间,这一点从我的代码中可以看出来。因此,除了被认为“不符合 Python 风格”之外,我还没有遇到与使用 map 相关的任何性能问题。

解决方案 12:

绩效衡量

在此处输入图片描述

图片来源: Experfy

你可以亲自看看列表理解和map函数哪个更好。

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

解决方案 13:

我认为最 Pythonic 的方式是使用列表推导而不是mapand filter。原因是列表推导比mapand更清晰filter

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允许过滤。

解决方案 14:

我的用例:

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 可以更具可读性和灵活性,特别是当我需要构造列表的值时。

如果你使用了地图,那么当你阅读它时你实际上就理解了它。

def pair_list_items(*args):
    return args

packed_list = list(map(pair_list_items,
                       lista, *listb, listc.....listn))

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

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用