That's not the sieve of Eratosthenes
It's rather prime trial division.
Your filter for 2 lets 1/2 of all numbers through, your filter for 3 lets 2/3 of all remaining numbers through, etc. So 1/2 * 2/3 * 4/5 * 6/7 = 22%
of all numbers make it through your filters for 2, 3, 5 and 7, so your filter for 11 will have to check 22% of all numbers. The real sieve of Eratosthenes treats 11 by marking off every 11th number, i.e., only 9% of all numbers. Less than half of your percentage. And yours gets worse for the larger primes. For example your filters for the primes from 2 to 89 let ~12% of all numbers through, so your filter for 97 checks ~12% of all numbers. While the real sieve of Eratosthenes treats 97 by marking off only about 1% of all numbers.
So that's why it's slow. It's doing way too much work.
An improvement
You can make it a bit faster by not stacking filters but simply doing the trial divisions at the current number, using the primes you're collecting anyway:
from itertools import count
def primes(n):
primes = []
sieve = (x for x in count(2) if all(x % p for p in primes))
for _ in range(n):
primes.append(next(sieve))
return primes
Or using islice
:
from itertools import count, islice
def primes(n):
primes = []
sieve = (x for x in count(2) if all(x % p for p in primes))
primes += islice(sieve, n)
return primes
Demo:
>>> primes(25)
[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]
The real thing
But here's a genuine lazy one based on the paper mentioned by trentcl. I add each prime to the list of its next multiple. When the list of the next x
is empty, it's thus a prime. Otherwise, I move the primes from the list to their next multiples.
from itertools import count
from collections import defaultdict
def primes(n):
marked = defaultdict(list)
primes = []
for x in count(2):
if x in marked:
for p in marked.pop(x):
marked[x + p].append(p)
else:
primes.append(x)
if len(primes) == n:
return primes
marked[x * x].append(x)
Benchmark 1
Times for n = 10000
(benchmark code at the end of the answer):
9.330 s original
3.502 s improved1
3.499 s improved2
0.065 s genuine
That said, your original and my improved version can be made much faster by using fewer filters (only up to the square root of the current number), as I've done in my later answer.
To infinity and beyond
As Eric Duminil showed, it's trivial to make the latter an infinite generator, which also makes it shorter. No need to build a result list and check whether you're done, just yield the primes:
infinite:
def primes():
marked = defaultdict(list)
for x in count(2):
if x in marked:
for p in marked.pop(x):
marked[x + p].append(p)
else:
yield x
marked[x * x].append(x)
The other solutions can similarly be turned into infinite generators, but meh, they're no good anyway. Plus in my improved1
and improved2
, the primes
list is not just the result but is also used for filtering, so I need to build it anyway.
As infinite generator, it's more versatile. You can still get a list of the first n if you want to:
>>> list(islice(primes(), 20))
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71]
You can also get all primes up to a desired limit:
>>> *itertools.takewhile(72 .__gt__, primes()),
(2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71)
Or if you neither know how many you need nor how big you need but you need "enough" somehow, for example enough so that their product exceeds a million:
>>> result, product = [], 1
>>> it = primes()
>>> while product < 10**6:
p = next(it)
result.append(p)
product *= p
>>> result
[2, 3, 5, 7, 11, 13, 17, 19]
Or if you just want the millionth prime:
>>> next(islice(primes(), 999999, None))
15485863
Or if you have a prime and want to know the "how manyth" prime it is:
>>> from operator import indexOf
>>> indexOf(primes(), 15485863) + 1
1000000
Or if you have a prime and want to know the next larger one (granted, there are more efficient ways to do that):
>>> it = primes()
>>> 89 in it and next(it)
97
Note that in the last few cases, you don't want a list of primes at all, so building one is wasted memory.
Speaking of memory, though... the generator (and also my version) do take a lot of memory. Here are memory measurements and benchmarks for up to n=1,000,000 for the above "infinite" solution and the solutions I'll show below:
| time for the first... | tracemalloc for 1,000,000
| 10,000 100,000 1,000,000 | size peak
------------+------------------------------+--------------------------
infinite | 0.068 s 0.971 s 12.450 s | 231,866,275 312,793,599
no_lists | 0.035 s 0.574 s 8.065 s | 143,870,959 226,554,527
lazier | 0.033 s 0.456 s 6.016 s | 36,500,174 36,535,742
recursive | 0.032 s 0.473 s 6.116 s | 72,169 109,109
twostep | 0.015 s 0.217 s 2.897 s | 71,986 108,926
The memory usage was measured like this:
import tracemalloc
tracemalloc.start()
it = primes()
prime = next(islice(it, 9999999, None))
size, peak = tracemalloc.get_traced_memory()
So the above "infinite" solution takes hundreds of megabytes to generate the first million primes, i.e., hundreds of bytes per prime. Quite a lot. Let's see why, by doing global marked
first thing in the primes()
function so we can inspect it afterwards. We'll see that almost a million composites are marked and the dictionary takes 84 MB:
>>> it = primes()
>>> next(islice(it, 999999, None))
15485863
>>> from sys import getsizeof
>>> len(marked), getsizeof(marked)
(999919, 83886176)
The lists of primes total 88 MB, and almost all of them contain only a single prime marking the composite:
>>> sum(map(getsizeof, marked.values()))
87992872
>>> from collections import Counter
>>> Counter(map(len, marked.values()))
Counter({1: 999845, 2: 69, 3: 4, 4: 1})
The marked composites take 32 MB and the marking primes take 28 MB:
>>> sum(map(getsizeof, marked))
31983680
>>> from itertools import chain
>>> sum(map(getsizeof, chain.from_iterable(marked.values())))
27999972
Together that's indeed 232 MB as reported by tracemalloc in the table above:
>>> 84 + 88 + 32 + 28
232
Since most lists of marking primes just contain a single prime anyway, let's store only the first prime at the composite. If more primes want to mark that composite, then instead move them ahead to their next multiple:
no_lists:
def primes():
marked = {}
for x in count(2):
if x in marked:
p = marked.pop(x)
while (x := x + p) in marked:
pass
marked[x] = p
else:
yield x
marked[x * x] = x
As the above table showed, this made it faster and saved a lot of memory.
In my other answer, I showed that prematurely setting marked[x * x]
as soon as you discover a new prime x
is wasteful. Let's rewrite the better version from there as an infinite generator as well. Here m
is the next marking prime, and it will start marking as soon as we reach m2
=m
2.
lazier:
def primes():
yield 2
primes = [2]
markers = iter(primes)
marked = {}
m = next(markers)
m2 = m * m
for x in count(3):
if x == m2:
while (x := x + m) in marked:
pass
marked[x] = m
m = next(markers)
m2 = m * m
elif x in marked:
p = marked.pop(x)
while (x := x + p) in marked:
pass
marked[x] = p
else:
yield x
primes.append(x)
This brought it down to under 37 MB, as marked
got much smaller. However, it creates a primes
list again, in order to update m
to the next next marking prime.
Can we get the best of both worlds? Not prematurely enter marked[x * x]
and not build a primes
list? Yes we can!
What do we use the primes list for? Just as a more slowly used stream of primes. But that's what we're writing! A generator of primes. So we can use it recursively. Our main generator iterator of primes will use a slower generator iterator of primes. But where does that slower one get its marker primes from? From yet another even more slowly progressing one, if needed. And so on. A potentially infinite stack of infinite iterators.
recursive:
(also see the addendum at the end of the answer for better versions of this)
def primes():
yield 2
marked = {}
markers = primes()
m = next(markers)
m2 = m * m
for x in count(3):
if x == m2:
while (x := x + m) in marked:
pass
marked[x] = m
m = next(markers)
m2 = m * m
elif x in marked:
p = marked.pop(x)
while (x := x + p) in marked:
pass
marked[x] = p
else:
yield x
Let's see the levels by storing each iterator's marked
in a global levels
list by doing levels.append(marked)
right after marked = {}
. Then:
>>> levels = []
>>> millionth = next(islice(primes(), 999999, None))
>>> for marked in levels:
print(len(marked), max(marked.values()))
546 3931
18 61
4 7
2 3
1 2
We see that there are five levels of iterators. The top-level iterator, the one we're using directly, has 546 marking primes, the largest being 3931. That makes sense, as the next larger prime is 3943, which starts marking at 39432, which is larger than the millionth prime:
>>> it = primes()
>>> 3931 in it and next(it)
3943
>>> 3943**2
15547249
>>> millionth
15485863
The next-lower level so far only needed to yield the 18 primes up to 61. Which is correct, as the next-larger prime 67 will only start marking at 672=4489, well above the prime 3943 needed next by the top-level iterator. And so on, until the fifth and lowest level only needed its initial yield 2
so far.
With only a few hundred primes in all levels combined, it's no wonder this solution takes faaar less memory than our first "infinite" one, about 3000 times less, only around 100 KB. And its advantage will increase for larger numbers of primes, as the earlier solutions need O(n) memory while the recursive ones take something like O(sqrt(n)) memory (not sure how the density of primes affects it).
Last but not least, here's a little variation where I handle the prime 2 separately and then move everything twice as fast, which makes the overall solution about twice as fast (again see the table above):
twostep:
def primes():
yield 2
yield 3
marked = {}
markers = primes()
next(markers)
m = next(markers)
m2 = m * m
for x in count(5, 2):
if x == m2:
marked[m2 + 2*m] = 2*m
m = next(markers)
m2 = m * m
elif x in marked:
step = marked.pop(x)
while (x := x + step) in marked:
pass
marked[x] = step
else:
yield x
Code for benchmark 1:
from timeit import repeat
from itertools import count, islice
from collections import defaultdict
def original(n):
sieve = count(2)
primes = []
nprime = 0
for _ in range(n):
nprime = next(sieve)
primes.append(nprime)
sieve = filter(eval(f"lambda x: x % {nprime} != 0"), sieve)
return primes
def improved1(n):
primes = []
sieve = (x for x in count(2) if all(x % p for p in primes))
for _ in range(n):
primes.append(next(sieve))
return primes
def improved2(n):
primes = []
sieve = (x for x in count(2) if all(x % p for p in primes))
primes += islice(sieve, n)
return primes
def genuine(n):
marked = defaultdict(list)
primes = []
for x in count(2):
if x in marked:
for p in marked.pop(x):
marked[x + p].append(p)
else:
primes.append(x)
if len(primes) == n:
return primes
marked[x * x].append(x)
funcs = original, improved1, improved2, genuine
n = 10000
expect = funcs[0](n)
for func in funcs[1:]:
print(func(n) == expect, func.__name__)
for _ in range(3):
for func in funcs:
t = min(repeat(lambda: func(n), number=1))
print('%.3f s ' % t, func.__name__)
print()
Addendum - better recursive generators
I came up with a nicer way later, don't want to rewrite the answer using this now but want to share. This goes through the primes of the underlying generator in an outer loop and from one prime's square to the next prime's square in an inner loop. It's less code and makes the structure of the procedure clearer.
def primes():
yield 2
marks = {}
i = 2
for p in primes():
for i in range(i+1, p*p):
if i not in marks:
yield i
else:
m = marks.pop(i)
j = i + m
while j in marks:
j += m
marks[j] = m
marks[p*p] = p
And a version with the paper's way of potentially storing several primes at the same common multiple:
def primes():
yield 2
marks = defaultdict(set)
i = 2
for p in primes():
for i in range(i+1, p*p):
if i not in marks:
yield i
else:
for m in marks.pop(i):
marks[i+m].add(m)
marks[p*p] = {p}
lambda
without relying oneval
; you just need to prebindnprime
as a defaulted argument:sieve = filter(lambda x, nprime=nprime: x % nprime != 0, sieve)
. That said, if you need a Python defined function (def
orlambda
) to usefilter
, you're better off just using an equivalent listcomp or genexpr (sadly, harder here due to late binding), or finding a way to make it actually use a built-in implemented in C. In this case, you could just dosieve = filter(nprime.__rmod__, sieve)
with the same effect (givennprime
and allsieve
elements areint
). \$\endgroup\$itertools
to dowheel2357 = cycle([2, 4, 2, 4, 6, 2, 6, 4, 2, 4, 6, 6, 2, 6, 4, 2, 6, 4, 6, 8, 4, 2, 4, 2, 4, 8, 6, 4, 6, 2, 4, 6, 2, 6, 6, 4, 2, 4, 6, 2, 6, 4, 2, 4, 2, 10, 2, 10])
andprimes = chain([2, 3, 5, 7], sieve(accumulate(wheel2357, initial=11)))
.. \$\endgroup\$