8

在回答增加一组数字之间差异的笨拙计算问题时,有没有更漂亮的方法?,我想出了两种解决方案,一种使用 itertools.starmap List Comprehension,另一种使用itertools.starmap

对我来说,list comprehension语法看起来更清晰、更易读、更简洁、更 Pythonic。但是由于星图在itertools中很好用,我想知道,这一定是有原因的。

我的问题是什么时候starmap可以优先考虑List Comprehension

注意如果它是风格问题,那么它肯定是矛盾的There should be one-- and preferably only one --obvious way to do it.

头对头比较

可读性很重要。---LC

这又是一个感知问题,但对我LC来说比starmap. 要使用starmap,您需要导入operator、定义lambda或一些显式multi-variable函数,但需要额外导入 from 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
4

3 回答 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同时,您应该切换到列表 comp/generator 表达式,但在它是单个函数的情况下,对于列表理解的生成器表达式,语法感觉非常冗长。

它们可互换的,如果有疑问,请坚持使用生成器表达式,因为它通常更具可读性,但在简单的情况下(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

在我的测试中,我们谈论的是使用简单函数 (no 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
于 2012-05-04T12:12:36.927 回答
3

这在很大程度上是一种风格。选择您认为更具可读性的那个。

关于“只有一种方法可以做到”,Sven Marnach 好心地提供了这个Guido 报价

“你可能认为这违反了 TOOWTDI,但正如我之前所说,这是一个善意的谎言(也是对 2000 年左右 Perl 口号的厚颜无耻的回应)。能够(向人类读者)表达意图通常需要在执行基本相同但对读者看起来不同的多种形式之间进行选择。”</p>

在性能热点中,您可能想要选择运行速度最快的解决方案(我猜在这种情况下将是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出来的速度有点快,可能是因为它避免了压缩/解构。

于 2012-05-04T12:05:57.957 回答
2

关于星图.. 假设你有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)])

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

所以。Starmap 在某些情况下是一个非常有用的工具。有了它,您可以编写更少的代码,更易于理解。

编辑添加了一些虚拟测试

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%。

于 2012-05-04T12:59:55.297 回答