处理列表和字典的惯用方式

3

最近我一直在了解“用Python的方式”来处理列表、字典、集合等。为此,我修改了一个计算前N个质数的函数,我们称之为“纯字典版”:

def findprimeuptoG(x):
    n_primes = {}

    for i in range(2, x):

        if i not in n_primes.values():
            n_primes[i]=i

        for k, v in n_primes.items():
            if i > v:
                n_primes[k] += k

    return sorted(n_primes)

这个函数的工作原理如下:
  1. 在字典中维护质数列表和这些质数的整数倍列表

  2. 这些倍数应该大于或等于某个整数i

  3. 如果一个数字不存在于现有质数的整数倍列表中,则它必须是一个质数,并被添加到质数列表中

  4. 2(最小的质数)开始,逐步增加i,直到x

  5. 返回质数列表

我已经使用列表、集合重写了这个函数几次,但这个版本似乎是最符合惯用法的。它很短,易于阅读。
如果有人能够友好地让我知道是否可以更清晰地编写此代码,请评论,我会很乐意阅读。
现在的问题是:这个函数的第一个版本不太干净,更像C语言风格:
def findprimeupto(x):
    primes = []
    n_primes = []

    for i in range(2, x):

        if i not in n_primes:
            primes.append(i)
            n_primes.append(i)

        for j in range(len(primes)):
            if i > n_primes[j]:
                n_primes[j] += primes[j]

    return primes

但是当我使用pypy编译器运行时,这个第一个版本绝对是最快的:

python3:

Primes up to: 100000

Algo: Clean version       , primes: 9592, time: 102.74523687362671

Algo: Dict, Set           , primes: 9592, time: 58.230621337890625

Algo: **FirstVersion**        , primes: 9592, time: 59.945680379867554

Algo: List of List[1]        , primes: 9592, time: 71.41077852249146

Algo: List of MutableNum  , primes: 9592, time: 292.3777365684509

Algo: **Pure Dict**           , primes: 9592, time: 56.381882667541504

pypy (版本2.3.1):

Primes up to: 100000

Algo: Clean version       , primes: 9592, time: 29.3849189281

Algo: Dict, Set           , primes: 9592, time: 85.8557658195

Algo: **FirstVersion**        , primes: 9592, time: 1.11557507515

Algo: List of List        , primes: 9592, time: 42.2394959927

Algo: List of MutableNum  , primes: 9592, time: 38.764893055

Algo: **Pure Dict**           , primes: 9592, time: 110.416568995

我知道'Pure Dict'版本的性能下降是由于我在循环中没有使用迭代器,尽管'FirstVersion'获得了惊人的速度提升。

由于我们的大部分代码可能最终会在生产中编译,因此我们应该以更类似C的方式而不是Python风格来编写代码吗?

编辑:

为了消除是否应该使用列表而不是字典的任何困惑,我提交了这个函数的另一个版本,我称之为'Clean版本'。这个版本没有直接访问列表的第N个元素,而是以我认为最符合Python风格的方式对列表进行迭代(顺便说一下,这个版本和同一代码的Lisp版本最相似:)

def findprimeuptoB(x):
    primes = []
    n_primes = []

    for i in range(2, x):

        if not (i in n_primes):
            primes.append(i)
            n_primes.append(i)

        new_n_primes = []

        for prime, n_prime in zip(primes, n_primes):
            if i > n_prime:
                new_n_primes.append(prime + n_prime)
            else:
                new_n_primes.append(n_prime)

        n_primes = new_n_primes

    return primes

以下是使用pypy3的结果,由于使用迭代器,“纯字典”的性能更好,但“第一版”仍然是绝对的胜者:质数上限:100000 算法: 简洁版本 , 质数: 9592, 时间: 12.010854959487915 算法: 字典,集合 , 质数: 9592, 时间: 13.721880912780762 算法: 第一版 , 质数: 9592, 时间: 1.1208839416503906 算法: 列表列表 , 质数: 9592, 时间: 8.978854894638062 算法: 可变数字列表 , 质数: 9592, 时间: 5.507040977478027 算法: 纯字典 , 质数: 9592, 时间: 15.005417108535767 - Arijan
1
你看起来正在尝试实现一个素数筛,但是做得不正确 - 可以参考例如http://en.wikipedia.org/wiki/Sieve_of_Eratosthenes。 - jonrsharpe
@jonrsharpe 我从未见过这种方法,也不理解它,但它似乎有效。我运行了他的代码100次,并得到了 [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97] - Two-Bit Alchemist
1
@Two-BitAlchemist 对不起,我应该表述得更清楚 - 这种方法并不是无效的,但并不完全是一个质数筛选器(质数筛算法要快得多)。 - jonrsharpe
无论如何,这似乎更多关于复杂性而不是数据结构。你说的第一种方法虽然不符合Pythonic规范,但在我看来非常干净,并且似乎使用了正确的数据结构。毕竟,这基于两个并发列表。它执行简单的任务(追加、添加)并且只运行两个循环,即两个for循环。你的字典实现需要不断地从字典中提取列表(值和项),然后对字典进行排序。字典是错误的数据结构,因为你要迭代它5倍才能让它做你想要的事情,基本上是这样。 - Two-Bit Alchemist
显示剩余3条评论
1个回答

2

如果你关心性能,"第一版本"是正确的选择。你可以使用cProfile来查看正在发生的情况。

参考资料,在pypy 2.5.0上,使用"第一版本"运行python -m cProfile -s cumulative x.py,我得到了以下结果:

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     1    0.000    0.000    0.727    0.727 x.py:1(<module>)
     1    0.724    0.724    0.727    0.727 x.py:29(findprimeupto)
 99999    0.002    0.000    0.002    0.000 {len}
 99999    0.001    0.000    0.001    0.000 {range}
 19184    0.001    0.000    0.001    0.000 {method 'append' of 'list' objects}
     1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

另一方面,使用“纯字典”,我得到:
ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     1    0.000    0.000   16.864   16.864 x.py:1(<module>)
     1    1.441    1.441   16.864   16.864 x.py:1(findprimeuptoG)
 99998   12.376    0.000   12.376    0.000 {method 'items' of 'dict' objects}
 99998    3.047    0.000    3.047    0.000 {method 'values' of 'dict' objects}
     1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
     1    0.000    0.000    0.000    0.000 {len}
     1    0.000    0.000    0.000    0.000 {range}

这段文字表明大部分时间都浪费在创建临时列表n_primes.items()n_primes.values()上。现在,有一个简单的解决方法:用它们各自的迭代器版本.iteritems().itervalues()替换.items().values()。然而,结果仍比列表版本慢得多,因为字典是比列表更复杂的结构,低级别的字典操作比等效的列表操作要慢得多。
ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     1    0.000    0.000    3.155    3.155 x.py:1(<module>)
     1    3.147    3.147    3.155    3.155 x.py:15(findprimeuptoH)
 99998    0.006    0.000    0.006    0.000 {method 'itervalues' of 'dict' objects}
 99998    0.002    0.000    0.002    0.000 {method 'iteritems' of 'dict' objects}
     1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
     1    0.000    0.000    0.000    0.000 {len}
     1    0.000    0.000    0.000    0.000 {range}

最后,“清洁版本”显然相当糟糕,因为它在每次迭代中都会创建一个新的n_primes列表。实际上,我花了21.795秒来计时。

结论:创建新容器(列表、字典等)非常慢,尽可能避免使用它们。此外,字典比列表慢。在这个问题中,你实际上不需要字典,所以应该使用列表。


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