当需要使用`starmap`时,与`List Comprehension`相比有何优势?

8

在回答问题 Clunky calculation of differences between an incrementing set of numbers, is there a more beautiful way?时,我想出了两种解决方案,一种是使用List Comprehension,另一种是使用 itertools.starmap

对我来说,list comprehension的语法更加清晰、易读、简洁且更符合Python风格。但是既然 starmap在itertools中已经很常见了,我想知道它肯定有其存在的原因。

我的问题是,何时应该优先使用starmap而不是List Comprehension

注意:如果这只是一个风格问题,那么它肯定与“做一件事应该有一种——最好只有一种——明显的方法。”相矛盾。

头对头比较

易读性很重要。 --- LC

虽然这是个主观的问题,但对我来说,LCstarmap 更易读。 要使用 starmap,你需要导入 operator,或定义 lambda 或一些显式的 多变量 函数,并且额外导入 itertools

性能 --- LC

>>> def using_star_map(nums):
    delta=starmap(sub,izip(nums[1:],nums))
    return sum(delta)/float(len(nums)-1)
>>> def using_LC(nums):
    delta=(x-y for x,y in izip(nums[1:],nums))
    return sum(delta)/float(len(nums)-1)
>>> nums=[random.randint(1,10) for _ in range(100000)]
>>> t1=Timer(stmt='using_star_map(nums)',setup='from __main__ import nums,using_star_map;from itertools import starmap,izip')
>>> t2=Timer(stmt='using_LC(nums)',setup='from __main__ import nums,using_LC;from itertools import izip')
>>> print "%.2f usec/pass" % (1000000 * t1.timeit(number=1000)/100000)
235.03 usec/pass
>>> print "%.2f usec/pass" % (1000000 * t2.timeit(number=1000)/100000)
181.87 usec/pass

我认为按照你的方式进行比较是不公平的。两个函数都应该将差异保存到 deltas 中,因为此时 using_star_map 函数的可读性较低,因为它全部在一行中呈现。请将其更改为:deltas = starmap(sub,zip(nums[1:],nums)) sum(deltas)/float(len(nums)-1) - jamylak
@jamylak:感谢您指出这个问题。但不幸的是,它并没有改变性能差异。 - Abhijit
本来不是我们的重点,但我们也在谈论可读性。 - jamylak
您可能希望发布一个代码示例,它不需要大量修复就可以使用。 - Marcin
3个回答

13

我通常看到的区别是map()/starmap()在你只是对列表中的每个项目调用函数时最为适用。在这种情况下,它们会更清晰一些:

(f(x) for x in y)
map(f, y) # itertools.imap(f, y) in 2.x

(f(*x) for x in y)
starmap(f, y)

一旦你开始需要使用 lambdafilter,就应该切换到列表推导式/生成器表达式,但在只有一个函数的情况下,生成器表达式或列表推导式的语法对于这种情况来说感觉非常啰嗦。

它们是可以相互替换的,如果有疑问,就使用生成器表达式,因为通常更易读,但在简单的情况下 (map(int, strings), starmap(Vector, points)) 使用 map()/starmap() 有时会使阅读更容易。

示例:

一个我认为使用 starmap() 更易读的示例:

from collections import namedtuple
from itertools import starmap

points = [(10, 20), (20, 10), (0, 0), (20, 20)]

Vector = namedtuple("Vector", ["x", "y"])

for vector in (Vector(*point) for point in points):
    ...

for vector in starmap(Vector, points):
    ...

对于 map()

values = ["10", "20", "0"]

for number in (int(x) for x in values):
    ...

for number in map(int, values):
    ...

性能:

python -m timeit -s "from itertools import starmap" -s "from operator import sub" -s "numbers = zip(range(100000), range(100000))" "sum(starmap(sub, numbers))"                         
1000000 loops, best of 3: 0.258 usec per loop

python -m timeit -s "numbers = zip(range(100000), range(100000))" "sum(x-y for x, y in numbers)"                          
1000000 loops, best of 3: 0.446 usec per loop

构建一个namedtuple

python -m timeit -s "from itertools import starmap" -s "from collections import namedtuple" -s "numbers = zip(range(100000), reversed(range(100000)))" -s "Vector = namedtuple('Vector', ['x', 'y'])" "list(starmap(Vector, numbers))"
1000000 loops, best of 3: 0.98 usec per loop

python -m timeit -s "from collections import namedtuple" -s "numbers = zip(range(100000), reversed(range(100000)))" -s "Vector = namedtuple('Vector', ['x', 'y'])" "[Vector(*pos) for pos in numbers]"
1000000 loops, best of 3: 0.375 usec per loop

在我的测试中,当我们谈论使用简单函数(没有lambda)时,starmap()比等效的生成器表达式更快。自然地,除非性能是已知的瓶颈,否则可读性应该优先考虑。

lambda会影响性能,以下是一个例子,与前面的示例相同,但使用lambda代替operator.sub()

python -m timeit -s "from itertools import starmap" -s "numbers = zip(range(100000), range(100000))" "sum(starmap(lambda x, y: x-y, numbers))" 
1000000 loops, best of 3: 0.546 usec per loop

1
map(f, y) 相当于 [f(x) for x in y] 而不是 (f(x) for x in y),因为它不是一个生成器。它会立即执行。 - akaRem
2
@akaRem Lattyware始终使用Python 3。 - Marcin
@akaRem 抱歉,我在谈论Python 3.x - 在2.x中确实如此。已更新以澄清。 - Gareth Latty
@Abhijit 我增加了一个使用 starmap() 的示例用例。正如我所说,无论您何时需要 lambda,都已经超出了 map()/ starmap() 的设计用途,因此您应该切换到生成器表达式。 - Gareth Latty
我喜欢starmap的使用案例,我认为它是一个非常好的示例。 - jamylak
显示剩余2条评论

3

这主要是一种风格问题。选择阅读起来更容易的那个。

关于“只有一种方法可以做到这一点”,Sven Marnach友好地提供了这篇Guido引用

“你可能认为这违反了TOOWTDI,但正如我之前所说,那是一个善意的谎言(以及对Perl 2000年左右口号的俏皮回应)。能够表达意图(给人类读者)通常需要在多个形式之间进行选择,这些形式本质上做着相同的事情,但对读者来说却看起来不同。”

在性能瓶颈中,您可能希望选择运行最快的解决方案(在这种情况下,我想应该是基于starmap的解决方案)。

关于性能-starmap由于其析构而较慢;然而,在这里并不需要使用starmap:

from timeit import Timer
import random
from itertools import starmap, izip,imap
from operator import sub

def using_imap(nums):
    delta=imap(sub,nums[1:],nums[:-1])
    return sum(delta)/float(len(nums)-1)

def using_LC(nums):
    delta=(x-y for x,y in izip(nums[1:],nums))
    return sum(delta)/float(len(nums)-1)

nums=[random.randint(1,10) for _ in range(100000)]
t1=Timer(stmt='using_imap(nums)',setup='from __main__ import nums,using_imap')
t2=Timer(stmt='using_LC(nums)',setup='from __main__ import nums,using_LC')

在我的电脑上:

>>> print "%.2f usec/pass" % (1000000 * t1.timeit(number=1000)/100000)
172.86 usec/pass
>>> print "%.2f usec/pass" % (1000000 * t2.timeit(number=1000)/100000)
178.62 usec/pass

imap稍微快一些,可能是因为它避免了压缩/解构。


3
这是一个不错的彩蛋,其中有很多好的建议(“简单胜于复杂”),但仅因为它包含在解释器中,并不意味着它成为某种计算机科学定律。 - Marcin
4
引用 Guido van Rossum 的话:“你可能认为这违反了 TOOWTDI 原则,但正如我之前所说,那是个小谎言(同时也是对 Perl 在 2000 年左右的口号的一个俏皮回应)。能够表达意图(给人类读者看)通常需要在多种形式之间进行选择,这些形式本质上做相同的事情,但对读者来说看起来是不同的。” - Sven Marnach
2
@SvenMarnach甚至更简单的说法是,“实用性胜过纯洁性”。总会有不止一种方法来做某件事——这是语言强大的现实。Python之禅这句话的真正含义更多地是关于减少语言冗余和避免无意义的同义词。 - Gareth Latty
3
当然,这个概括性规则可以应用于解释任何不一致之处! :) - Sven Marnach
1
关于性能的一点说明:如果你实际上是在调用一个预先存在的函数,starmap 在我的 timeit 测试中似乎会快一点,但如果你必须定义一个 lambda 来完成它,生成器表达式几乎肯定会通过避免函数调用而获胜。 - Danica
显示剩余8条评论

2

关于Starmap..假设你有L = [(0,1,2),(3,4,5),(6,7,8),..]

生成器推导式看起来像这样:

(f(a,b,c) for a,b,c in L)

或者
(f(*item) for item in L) 

星图将会看起来像这样:
starmap(f, L)

第三个变量较轻且较短,但第一个非常明显,不需要思考它做了什么。
好的,现在我想编写更加复杂的内联代码...
some_result = starmap(f_res, [starmap(f1,L1), starmap(f2,L2), starmap(f3,L3)])

这行文字并不明显,但仍然容易理解。在生成器推导中,它看起来像这样:

some_result = (f_res(a,b,c) for a,b,c in [(f1(a,b,c) for a,b,c in L1), (f2(a,b,c) for a,b,c in L2), (f3(a,b,c) for a,b,c in L3)])

如您所见,这段文字过长、难以理解,且无法放在一行中,因为它超过了79个字符(PEP 8的规定)。即使是更短的变体也很糟糕:

some_result = (f_res(*item) for item [(f1(*item) for item in L1), (f(*item2) for item in L2), (f3(*item) for item in L3)])

太多字符.. 太多括号.. 太多噪音。

所以,星图在某些情况下是一个非常有用的工具。使用它可以编写更少、更简单易懂的代码。

编辑 添加了一些虚拟测试

from timeit import timeit
print timeit("from itertools import starmap\nL = [(0,1,2),(3,4,5),(6,7,8)]\nt=list((max(a,b,c)for a,b,c in L))")
print timeit("from itertools import starmap\nL = [(0,1,2),(3,4,5),(6,7,8)]\nt=list((max(*item)for item in L))")
print timeit("from itertools import starmap\nL = [(0,1,2),(3,4,5),(6,7,8)]\nt=list(starmap(max,L))")

输出 (Python 2.7.2)

5.23479851154
5.35265309689
4.48601346328

因此,在这里,星图甚至快了约15%。


我认为在您更复杂的情况下,starmap()和生成器表达式都不是一个好的解决方案。此时将代码展开(例如成为完整的生成器)会更易读。 - Gareth Latty

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接