【问题标题】:Creating a generator for primes in the style of SICP以 SICP 风格创建素数生成器
【发布时间】:2020-07-04 15:23:54
【问题描述】:

在未来的课程中,我将学习一门使用 Python 的学科,重点是使用序列和生成器以及 Python 中的那些东西。

我一直在按照练习列表来练习这些部分。我被困在一个要求使用主要发电机的练习中。 到现在为止,我使用 Python 的还不是很多,但是我已经阅读并完成了 SICP 中的大部分练习。 在那里,他们展示了以下程序,该程序利用 Eratosthenes 的筛子生成一个惰性素数列表。

(define (sieve stream)
  (cons-stream
   (stream-car stream)
   (sieve (stream-filter
           (lambda (x)
             (not (divisible? x (stream-car stream))))
           (stream-cdr stream)))))

(define primes (sieve (integers-starting-from 2)))

在 Python 中,根据我的阅读,最接近的是生成器 所以我尝试将其翻译成以下内容。

import itertools
def sieve(seq):
    n = next(seq)
    yield n
    sieve(filter(lambda x: x % n != 0, seq))

def primes():
    return sieve(itertools.count(2))

print(list(itertools.islice(primes(),10)))

但它只打印[2]。 我认为这是因为对 sieve 的递归调用的结果只是被丢弃了,而不是像我最初预期的那样再次运行该函数。

为了解决这个问题,我尝试使用循环来代替:

def sieve(seq):
    def divisible(n):
        return lambda x: x % n != 0
    while True:
        n = next(seq)
        yield n
        seq = sieve(filter(divisible(n), seq))

在我可以生成前 9 个素数的情况下,这是可行的,但如果我要求第十个,则会引发 RecursionError

那么,我的问题是如何改进它以便能够计算更大的素数?

PS:https://stackoverflow.com/a/568618/6571467 中已经提出了筛生成器的实现,但它明确处理筛中的先前素数。而在惰性列表范式中,目标是从操作实际执行的顺序中抽象出来。

【问题讨论】:

  • 惰性范式依赖尾调用优化 (TCO) 来避免每次尝试向候选流中添加另一个过滤器时调用堆栈都增加。不幸的是,Python 没有实现 TCO。 (实际上,调用堆栈本身就是存储先前素数的东西,Python 不允许它无限制地增长。)
  • 请注意,尽管存在常见的误解,但这实际上并不是埃拉托色尼的筛子。 It's a highly inefficient form of trial division.
  • 相比之下,your link 中的筛子是一个真正的惰性筛子。
  • user2357112 支持 Monica 谢谢你的论文。我没有意识到在 SICP 中完成的过滤与原始筛子中的多重交叉有根本的不同。虽然我不明白显示的代码,因为我从来没有学过 Haskell,也不知道它们是做什么的。

标签: python python-3.x generator sicp lazy-sequences


【解决方案1】:

对于您的第一个版本,您可以使用 yield from 从递归调用中产生:

def sieve(seq):
    n = next(seq)
    yield n
    yield from sieve(filter(lambda x: x % n != 0, seq))

(或 for x in sieve(...): yield x 用于旧版本的 Python)

对于您的循环版本,删除递归调用,只需堆叠filters

def sieve(seq):
    while True:
        n = next(seq)
        yield n
        seq = filter(lambda x, n=n: x % n != 0, seq)

两个版本都适用于前(几乎)1000 个素数,然后还会导致最大递归错误(即使使用循环,因为您有一堆嵌套的 filter 函数),可以通过设置更高的递归限制,但并没有真正阻止——除非不使用递归或支持尾调用优化的语言。

或者,对于纯迭代解决方案,您可以存储看到的素数集并检查其中的any 是否是除数。 (这两种递归变体基本上也存储了这组素数,只是它们将其隐藏在嵌套的filter 调用堆栈中。)

def sieve(seq):
    ps = []
    while True:
        n = next(seq)
        if not any(n % p == 0 for p in takewhile(lambda p: p*p <= n, ps)):
            yield n
            ps.append(n)

所有三个版本都产生相同的结果,但“无递归”更快(很多):

>>> %timeit primes(sieve1, 900)
1 loop, best of 5: 297 ms per loop

>>> %timeit primes(sieve2, 900)
1 loop, best of 5: 185 ms per loop

>>> %timeit primes(sieve3, 900)
10 loops, best of 5: 35.4 ms per loop

(使用n.__rmod__ 而不是lambda x: x % n != 0 可以很好地提升基于filter 的速度,但它们仍然慢得多。)


附录,关于您的第二种方法,即使对于非常小的值也会导致递归错误:我仍然无法解决这个问题,但我是这样理解的:通过执行seq = sieve(filter(nondivisible(n), seq)) 而不仅仅是seq = filter(nondivisible(n), seq) ,“顶级”sieve 从下一级的sieve 获取next 值,依此类推,并且每个迭代都在每次迭代中添加 另一个 层筛子,导致在每次迭代中,sieve-stack 的高度为 double

【讨论】:

  • 对于所见素数,使用列表比使用集合更好。重复数据删除是不必要的,列表迭代更快,并且列表确保首先测试较小的素数。
  • @user2357112supportsMonica 同意,您可以使用takewhile 提前中止。
  • 奇怪的是,您的第二个版本比我的上一个版本运行得更远,因为它们几乎相同(两者都是在其环境中捕获 n 的函数)。来自Scheme,这些是python中让我感到困惑的事情。
  • @tigre200 我认为循环中的递归实际上会导致每个素数的堆叠筛子的数量加倍,请参阅我的编辑。
  • @tobias_k,这是一个非常出色的答案!
【解决方案2】:

在 Python 解决方案中,sieve 将是一个带有生成器的函数,它本身就是一个生成器,如下所示:

from itertools import count, islice

def sieve(gen):
    for n in gen:
        if n%2 == 0:
            yield n

def evens(start=2, limit=None):
    return islice(sieve(count(start)), limit)

for n in evens(limit=10):
    print(n)

2
4
6
8
10
12
14
16
18
20

...除了要生成素数,我们不仅要检查被 2 整除,还要检查每个给定点上小于n 的所有内容的整除性。我们可以通过意识到对于每对除数 a • b = n,其中一个除数 ≤ sqrt(n) 来做得更好。

Python 与 Scheme 的不同之处在于 Python 不鼓励递归,对递归深度的限制很小。 See this question.

所以,在sieve 内部循环是个好主意。

from itertools import count, islice
from math import sqrt

def sieve(gen):
    for n in gen:
        sqrt_n = int(sqrt(n))
        for d in count(2):
            if d > sqrt_n:
                yield n
                break
            if n % d == 0:
                break

def primes(start=2, limit=None):
    return islice(sieve(count(start)), limit)

for p in primes(limit=20):
    print(p)

我们可能会利用生成器的有状态特性。我们可以在生成器中保留一个先前生成的素数列表,以牺牲空间换取更快的运行时间。

def primes():
    primes = [2]
    yield 2
    for n in count(3):
        sqrt_n = int(sqrt(n))
        for p in primes:
            if p > sqrt_n:
                primes.append(n)
                yield n
                break
            if n % p == 0:
                break

这允许生成最多中等大小的素数:

In [56]: %time nth(primes(), 1000000)
CPU times: user 40.9 s, sys: 142 ms, total: 41.1 s
Wall time: 41.2 s
Out[56]: 15485863

如需真正智能的实现,请参阅Simple Prime Generator in Python

【讨论】:

  • 这并不能真正回答我的问题,因为我已经承认 Python 实现中的 Simple Prime Generator。保留素数列表是可行的,但我想探索不保留此类簿记信息的懒惰范式。我担心 chepner 可能是对的,python 堆栈限制排除了这种方法,但也许有人有一个聪明的解决方案。
  • 抱歉,没有注意到那个链接。
  • @tigre200:不过,您的方法仍然保留了该簿记信息。它只是将它隐藏在调用堆栈中。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-04-11
  • 2020-02-28
  • 1970-01-01
  • 2012-11-19
  • 2017-10-11
  • 2016-09-29
  • 1970-01-01
相关资源
最近更新 更多