Skip to content

Commit

Permalink
approach for nth prime (#3496)
Browse files Browse the repository at this point in the history
  • Loading branch information
safwansamsudeen committed Dec 18, 2023
1 parent 8ebf557 commit 0b78195
Show file tree
Hide file tree
Showing 6 changed files with 197 additions and 0 deletions.
21 changes: 21 additions & 0 deletions exercises/practice/nth-prime/.approaches/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"introduction": {
"authors": ["safwansamsudeen"]
},
"approaches": [
{
"uuid": "c97a3f8e-a97d-4e45-b44f-128bcffb2d3a",
"slug": "generator-fun",
"title": "Generator Fun",
"blurb": "Utilize Python library and generators",
"authors": ["safwansamsudeen"]
},
{
"uuid": "e989fdd2-3f39-4195-9d7c-120a6d6376b6",
"slug": "tracking",
"title": "Tracking",
"blurb": "Track previous prime values and return the nth one",
"authors": ["safwansamsudeen"]
}
]
}
51 changes: 51 additions & 0 deletions exercises/practice/nth-prime/.approaches/generator-fun/content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Generator Fun
The key of this approach is to not store the elements you do not need.

This is a code representation:
```python
from itertools import islice, count

def prime(number):
if number == 0:
raise ValueError('there is no zeroth prime')
gen = islice(filter(lambda counter: all(counter % test != 0 for test in range(2, int(counter ** 0.5) + 1)), count(2)), number)
for _ in range(number - 1): next(gen)
return next(gen)
```

Let's dissect it! `itertools.count` is like `range` without un upper bound - calling it returns a generator, and `for ... in count_obj` will result in an infinite loop.

Using a lambda expression, we `filter` out any numbers above two that are prime.
Doesn't this result in an infinite loop?
No - `filter` _also_ returns a generator object (which are [evaluated lazily][generator]), so while it's too will produce values infinitely if evaluated, it doesn't hang to program at the time of instantiation.

`itertools.islice` takes in a generator object and an end count, returning a generator object which _only evalutes until that end count_.

The next line exhausts all the values in the generator except the end, and we finally return the last element.

We can utilize the `functools.cache` decorator for greater speeds at higher values of `number`, so we take it out. The added bonus is that a very long line of code is cleant up.

```python
from itertools import islice, count
from functools import cache

@cache
def is_prime(counter):
return all(counter % test != 0 for test in range(2, int(counter ** 0.5) + 1))

def prime(number):
if number == 0:
raise ValueError('there is no zeroth prime')
gen = islice(filter(is_prime, count(2)), number)
for _ in range(number - 1): next(gen)
return next(gen)
```

~~~~exercism/note
Note that this that not create a list anywhere, and thus is both memory and time efficient.
~~~~

Read more on `itertools` on the [Python docs][itertools].

[itertools]: https://docs.python.org/3/library/itertools.html
[generator]: https://www.programiz.com/python-programming/generator
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from itertools import islice, count

def prime(number):
gen = islice(filter(lambda n: all(counter % test != 0 for test in range(2, int(counter ** 0.5) + 1)), count(2)), number)
for _ in range(number - 1): next(gen)
return next(gen)
46 changes: 46 additions & 0 deletions exercises/practice/nth-prime/.approaches/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Introduction
Nth Prime in Python is a very interesting exercise that can be solved in multiple ways.

## General guidance
Every approach has to a) validate the input, b) calculate the prime numbers until n - 1, and c) return the nth prime number.

As the previous numbers are calculated multiple times, it's advisable to extract that piece of code as a function and use `functools.cache` for greater speeds at higher numbers.

## Approach: Tracking
Using this approach, we manually track the primes/number of primes until the nth prime, after which we quit the loop and return the last number in the list/currently tracked prime.
There are many possible code implementations, such as this one:
```python
def prime(number):
if number == 0:
raise ValueError('there is no zeroth prime')
counter = 2
primes = [2]
while len(primes) < number:
counter += 1
if all(counter % test != 0 for test in primes):
primes.append(counter)
return primes[-1]
```

If you're interested in learning more about this approach (and discover a lesser known Python feature!), go [here][approach-tracking].

## Approach: Generator Fun
A far more idiomatic approach that utilizes Python's powerful standard library is to use `itertools` and generator expression related functions.

```python
from itertools import islice, count

def is_prime(n):
return not any(n % k == 0 for k in range(2, int(n ** 0.5) + 1))

def prime(number):
if number == 0:
raise ValueError('there is no zeroth prime')
gen = islice(filter(is_prime, count(2)), number)
for _ in range(number - 1): next(gen)
return next(gen)
```
If you'd like to understand this approach better, [read more][approach-generator-fun].

[approach-tracking]: https://exercism.org/tracks/python/exercises/nth-prime/approaches/tracking
[approach-generator-fun]: https://exercism.org/tracks/python/exercises/nth-prime/approaches/generator-fun
65 changes: 65 additions & 0 deletions exercises/practice/nth-prime/.approaches/tracking/content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Tracking
This approach includes building a list of all the previous primes until it reaches the nth number, after which it quits the loop and return the last number in the list.
```python
def prime(number):
if number == 0:
raise ValueError('there is no zeroth prime')
counter = 2
primes = [2]
while len(primes) < number:
counter += 1
if all(counter % test != 0 for test in range(2, counter)):
primes.append(counter)
return primes[-1]
```
Efficiency can be improved slightly by reducing the factor check to the square root of the number...
```python
...
if all(counter % test != 0 for test in range(2, int(counter ** 0.5) + 1)):
...
```
... or even better, checking whether a new number is merely not divisible by any of the primes (in which case it's a prime itself):
```python
...
if all(counter % test != 0 for test in primes):
...
```
Instead of building the list, it's more memory efficient to just save the number of primes found until now, and return the currently stored number when the nth prime is found.
However, this elongates the code.
```python
def prime(number):
if number == 0:
raise ValueError('there is no zeroth prime')
counter = 2
prime_count = 0
while True:
isprime = True
for test in range(2, int(counter ** 0.5) + 1):
if counter % test == 0:
isprime = False
break
if isprime:
prime_count += 1
if prime_count == number:
return counter
counter += 1
```

~~~~exercism/advanced
Tip: you can use `for... else` to make your code more idiomatic here.
```python
...
for test in range(2, int(counter ** 0.5) + 1):
if counter % test == 0:
break
else:
prime_count += 1
if prime_count == number:
return counter
```
The else block is executed if the `for` loop completes normally - that is, without `break`ing.
Read more on [for/else][for-else]
~~~~


[for-else]: https://book.pythontips.com/en/latest/for_-_else.html
8 changes: 8 additions & 0 deletions exercises/practice/nth-prime/.approaches/tracking/snippet.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
def prime(number):
counter = 2
primes = [2]
while len(primes) < number:
counter += 1
if all(counter % test != 0 for test in range(2, counter)):
primes.append(counter)
return primes[-1]

0 comments on commit 0b78195

Please sign in to comment.