Pythonic vs Unpythonic

时间:2013-10-04 16:27:42

标签: python runtime

考虑这两个执行以下操作的函数:给定一个单词,它会生成一个列表,其中单词中的每个位置都被字母表中的每个字符替换

alphabet = 'abcdefghijklmnopqrstuvwxyz'

版本1(Pythonic):

def replace1_word( word ):
    return [word[:w]+alphabet[i]+word[w+1:] 
            for w in range(len(word)) 
            for i in range(len(alphabet))]

第2版(Unpythonic):

def replace1_word2( word ):
    res=[]
    for w in range(len(word)):
        for i in range(len(alphabet)):
            res.append( word[:w] + alphabet[i] + word[w+1:] )
    return res

我使用timeit模块运行1000次并测量时间,平均运行时间差异在0.028毫秒到0.040毫秒之间。

我的问题是第二版中代码的哪一部分/哪一行代价高昂?为什么?它们“似乎”以相同的方式工作,并以列表格式返回相同的结果。

6 个答案:

答案 0 :(得分:6)

  

我的问题是代码的哪一部分/哪一行在第二部分是代价高昂的   版本和为什么?他们“似乎”以同样的方式工作并返回   列表格式相同的结果。

不,他们不是。如果有疑问,请始终对其进行分析,它将为您提供每个操作的成本图。只是看下面的O / P它现在告诉你,第二个功能的成本是多少?

>>> cProfile.run("replace1_word2('foo bar baz')")
         313 function calls in 0.000 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.000    0.000 <pyshell#216>:1(replace1_word2)
        1    0.000    0.000    0.000    0.000 <string>:1(<module>)
       12    0.000    0.000    0.000    0.000 {len}
      286    0.000    0.000    0.000    0.000 {method 'append' of 'list' objects}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
       12    0.000    0.000    0.000    0.000 {range}


>>> cProfile.run("replace1_word('foo bar baz')")
         27 function calls in 0.000 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.000    0.000 <pyshell#220>:1(replace1_word)
        1    0.000    0.000    0.000    0.000 <string>:1(<module>)
       12    0.000    0.000    0.000    0.000 {len}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
       12    0.000    0.000    0.000    0.000 {range}
  

注意解释什么替换附加(或版本1如何   生成列表)

在Python中,函数调用会产生额外的开销。在前一种情况下,list.append被多次调用,而在列表理解的情况下,列表将作为单个表达式生成。所以从某种意义上说,对于具有循环结构的列表理解没有等效的符号。 List comprehension是Python中的一个强大工具,而不是循环的装饰语法。

<强>后记

如果你让我写一个函数来解决这个问题,我会以

结束
>>> from itertools import product
>>> from string import ascii_lowercase
>>> def replace1_word4(word):
         words = ('{}'.join([word[:w], word[w+1:]]) for w in range(len(word)))
         return [word.format(replace)
                 for  word, replace in product(words, ascii_lowercase)]

答案 1 :(得分:2)

它可能与列表推导的评估方式有关,而对于带有list.append的循环,它与收集器变量有关;如果您更改第二个代码段以使用yield,然后使用list()包装其结果,则性能会更接近第一个版本:

def replace1_word3(word):
    for w in range(len(word)):
        for i in range(len(alphabet)):
            yield word[:w] + alphabet[i] + word[w+1:]

基准:

In [18]: timeit replace1_word('foo bar baz ' * 100)
10 loops, best of 3: 38.7 ms per loop

In [19]: timeit replace1_word2('foo bar baz ' * 100)
10 loops, best of 3: 42.1 ms per loop

In [20]: timeit list(replace1_word3('foo bar baz ' * 100))
10 loops, best of 3: 39.7 ms per loop

剩下的差异可能归结于如何在列表理解中内部构建实际列表与yield =&gt;的性能。 generator =&gt; list()

P.S。 Abhijit的答案可以用更多技术术语解释为什么replace1_word更快。无论如何,看起来list.append是我猜测的罪魁祸首。

答案 2 :(得分:1)

谁在乎哪些被认为是“pythonic”而哪些不是? Python的主要设计目标和“pythonicness” - 关于易于阅读,不是,因为你的问题意味着使用timeit模块,关于特别高效的

在你的特殊情况下,我认为第二个例子更具可读性,但只需迭代字母表而不是遍历其索引就可以使它更具可读性:

def replace1_word2( word ):
    res=[]
    for w in range(len(word)):
        for letter in alphabet:
            res.append( word[:w] + letter + word[w+1:] )
    return res

此外,您可能不需要额外创建列表,yield关键字也可以:

def replace1_word2( word ):
    for w in range(len(word)):
        for letter in alphabet:
            yield word[:w] + letter + word[w+1:]

最后,虽然没有正式的指导方针,但很多人都在关注PEP8 styleguide。这可能是最有助于提高可读性和代码“pythonicness”的东西。在您的代码中,除了函数签名中的额外空格外,没有真正违反该样式指南的内容:

def replace1_word2( word ):  # no
def replace1_word2(word):  # yes

答案 3 :(得分:1)

也许更好的是这样的:

def replace1_word3( word ):
    return [word[:w]+alphabet[i]+word[w+1:] 
        for w,i in product(xrange(len(word)), xrange(len(alphabet)))]

但它并不比第一个版本快,因为它实际上做了同样的事情。

略有改进:

def replace1_word4( word ):
    return [word[:w]+i+word[w+1:] 
        for w,i in product(xrange(len(word)), alphabet)]

这有点不那么罗嗦 - 对于字母表,你不需要得到范围,然后是尊重;你可以直接使用这些值。但是,您可以在原始代码中进行相同的简化,并且可能获得相同的加速(单词是'pizzazzle':只有长度非常重要):

In [357]: %timeit replace1_word(word)
10000 loops, best of 3: 71.7 us per loop

In [358]: %timeit replace1_word2(word)
10000 loops, best of 3: 82.9 us per loop

In [359]: %timeit replace1_word3(word)
10000 loops, best of 3: 72.2 us per loop

In [360]: %timeit replace1_word4(word)
10000 loops, best of 3: 63.7 us per loop

答案 4 :(得分:0)

python中的循环可能很慢。由于for循环的额外解释器开销,嵌套循环版本是两者中较慢的一个。 https://wiki.python.org/moin/PythonSpeed/PerformanceTips

答案 5 :(得分:0)

我认为其中任何一个都不是Pythonic。好多了:

def replace1_word2( word ):
    res=[]
    for w, letter in enumerate(word):
        for alpha in alphabet:
            res.append(word[:w] + alpha + word[w+1:])
    return res