4

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

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(非pythonic):

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毫秒之间。

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

4

6 回答 6

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}

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

在 Python 中,函数调用有额外的开销。在前一种情况下,list.append多次调用,而在列表理解的情况下,列表作为单个表达式生成。所以从某种意义上说,对于具有循环结构的列表推导,没有等效的符号。列表推导是 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)]
于 2013-10-04T16:39:10.027 回答
2

谁在乎这些中的哪些被认为是“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 样式指南。这可能是最有助于代码的可读性和“pythonicness”的东西。在您的代码中,除了函数签名中的额外空格外,没有真正违反该样式指南:

def replace1_word2( word ):  # no
def replace1_word2(word):  # yes
于 2013-10-04T17:34:43.657 回答
2

它可能与如何评估列表推导与list.append使用收集器变量的 for 循环有关;如果您更改要使用的第二个片段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=> generator=>的性能list()

PS Abhijit 的回答可能会用更多技术术语解释为什么replace1_word更快。无论如何,这似乎list.append是我猜想的罪魁祸首。

于 2013-10-04T16:32:00.233 回答
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
于 2013-10-04T19:02:32.143 回答
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
于 2013-10-04T16:38:49.167 回答
0

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

于 2013-10-04T16:32:19.757 回答