Skip to content

Commit c2db761

Browse files
committed
Start on approach: filter for multiples
1 parent 45af046 commit c2db761

File tree

4 files changed

+190
-2
lines changed

4 files changed

+190
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"introduction": {
3+
"authors": [
4+
"MatthijsBlom"
5+
]
6+
},
7+
"approaches": [
8+
{
9+
"uuid": "7dd85d5b-12bd-48a6-97fe-8eb7dd87af72",
10+
"slug": "filter-for-multiples",
11+
"title": "Filter for multiples",
12+
"blurb": "Use the built-in filter function to select the numbers that are multiples, then sum these.",
13+
"authors": [
14+
"MatthijsBlom"
15+
]
16+
}
17+
]
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# `filter` for multiples
2+
3+
```python
4+
def sum_of_multiples(limit, factors):
5+
is_multiple = lambda n: any(n % f == 0 for f in factors if f != 0)
6+
return sum(filter(is_multiple, range(limit)))
7+
```
8+
9+
Probably the most straightforward way of solving this problem is to
10+
11+
1. look at every individual integer between `0` and `limit`,
12+
2. check that it is a multiple of any of the given `factors`, and
13+
3. add it to the sum when it is.
14+
15+
16+
## Notable language features used in this solution
17+
18+
### Built-in function: `sum`
19+
20+
Adding all the numbers in a collection together is a very common operation.
21+
Therefore, Python provides the built-in function [`sum`][builtin-sum].
22+
23+
`sum` takes one argument, and requires that it be **iterable**.
24+
A value is iterable whenever it makes sense to use it in a `for` loop like this:
25+
26+
```python
27+
for _ in iterable_value: # 👈
28+
...
29+
```
30+
31+
The `list` is the most commonly used iterable data structure.
32+
Many other containers are also iterable, such as `set`s, `tuple`s, `range`s, and even `dict`s and `str`ings.
33+
Still other examples include iterators and generators, which are discuss below.
34+
35+
When given such a collection of numbers, `sum` will look at the elements one by one and add them together.
36+
The result is a single number.
37+
38+
```python
39+
numbers = range(1, 100 + 1) # 1, 2, …, 100
40+
sum(numbers)
41+
# ⟹ 5050
42+
```
43+
44+
Had the highlighted solution not used `sum`, it might have looked like this:
45+
46+
```python
47+
def sum_of_multiples(limit, factors):
48+
is_multiple = lambda n: any(n % f == 0 for f in factors if f != 0)
49+
total = 0
50+
for multiple in filter(is_multiple, range(limit)):
51+
total += total
52+
return total
53+
```
54+
55+
56+
### Built-in function: `filter`
57+
58+
Selecting elements of a collection for having a certain property is also a very common operation.
59+
Therefore, Python provides the built-in function [`filter`][builtin-filter].
60+
61+
`filter` takes two arguments.
62+
The first is a **predicate**.
63+
The second is the iterable the elements of which should be filtered.
64+
65+
A predicate is a function that takes one argument (of any particular type) and returns a `bool`.
66+
Such functions are commonly used to encode properties of values.
67+
An example is `str.isupper`, which takes a `str` and returns `True` whenever it is uppercase:
68+
69+
```python
70+
str.isupper("AAAAH! 😱") # ⟹ True
71+
str.isupper("Eh? 😕") # ⟹ False
72+
str.isupper("⬆️💼") # ⟹ False
73+
```
74+
75+
Thus, the function `str.isupper` represents the property of _being an uppercase string_.
76+
77+
Contrary to what you might expect, `filter` does not return a data structure like the one given as an argument:
78+
79+
```python
80+
filter(str.isupper, ["THUNDERBOLTS", "and", "LIGHTNING"])
81+
# ⟹ <filter object at 0x000002F46B107BE0>
82+
```
83+
84+
Instead, it returns an **iterator**.
85+
86+
An iterator is an object whose sole purpose is to guide iteration through some data structure.
87+
In particular, `filter` makes sure that elements that do not satisfy the predicate are skipped.
88+
It is a bit like a cursor that can move only to the right.
89+
90+
The main differences between containers (such as `list`s) and iterators are
91+
92+
- Containers can, depending on their contents, take up a lot of space in memory, but iterators are generally very small (regardless of how many elements they 'contain').
93+
- Containers can be iterated over multiple times, but iterators can be used only once.
94+
95+
To illustrate the latter difference:
96+
97+
```python
98+
is_even = lambda n: n % 2 == 0
99+
numbers = range(20) # 0, 1, …, 19
100+
even_numbers = filter(is_even, numbers) # 0, 2, …, 18
101+
sum(numbers) # ⟹ 190
102+
sum(numbers) # ⟹ 190
103+
sum(even_numbers) # ⟹ 90
104+
sum(even_numbers) # ⟹ 0
105+
```
106+
107+
Here, `sum` iterates over both `numbers` and `even_numbers` twice.
108+
109+
In the case of `numbers` everything is fine.
110+
Even after looping through the whole of `numbers`, all its elements are still there, and so `sum` can ask to see them again without problem.
111+
112+
The situation with `even_numbers` is move involved.
113+
To use the _cursor_ analogy: after going through all of `even_number`'s 'elements' &ndash; actually elements of `numbers` &ndash; the cursor has moved all the way to the right.
114+
It cannot move backwards, so if you wish to iterate over all even numbers then you need a new cursor.
115+
We say the the `even_numbers` iterator is _exhausted_. When `sum` asks for its elements again, `even_numbers` comes up empty and so `sum` returns `0`.
116+
117+
Had the highlighted solution not used `filter`, it might have looked like this:
118+
119+
```python
120+
def sum_of_multiples(limit, factors):
121+
is_multiple = lambda n: any(n % f == 0 for f in factors if f != 0)
122+
multiples = [candidate for candidate in range(limit) if is_multiple(candidate)]
123+
return sum(multiples)
124+
```
125+
126+
This variant stores all the multiples in a `list` before summing them.
127+
Such a list can potentially be very big.
128+
For example, if `limit = 1_000_000_000` and `factors = [1]` then `multiples` will be a list 8 gigabytes large!
129+
It is to avoid unnecessarily creating such large intermediate data structures that iterators are often used.
130+
131+
132+
### A function expression: `lambda`
133+
134+
...
135+
136+
137+
### Built-in function: `any`
138+
139+
...
140+
141+
142+
### A generator expression
143+
144+
...
145+
146+
147+
## Reflections on this approach
148+
149+
An important advantage of this approach is that it is very easy to understand.
150+
However, it suffers from potentially performing a lot of unnecessary work, for example when all `factors` are large, or when there are no `factors` at all.
151+
152+
<!-- TODO elaborate -->
153+
154+
155+
[builtin-sum]: https://docs.python.org/3/library/functions.html#sum "Built-in Functions: sum"
156+
[builtin-filter]: https://docs.python.org/3/library/functions.html#filter "Built-in Functions: filter"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
def sum_of_multiples(limit, factors):
2+
is_multiple = lambda n: any([n % f == 0 for f in factors if f != 0])
3+
return sum(filter(is_multiple, range(limit)))

exercises/practice/sum-of-multiples/.approaches/introduction.md

+13-2
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,16 @@ def sum_of_multiples(limit, factors):
1919
return sum(filter(is_multiple, range(limit)))
2020
```
2121

22-
Egregious performance when multiples are few.
22+
Probably the most straightforward way of solving this problem is to
2323

24-
...
24+
1. look at every individual integer between `0` and `limit`,
25+
2. check that it is a multiple of any of the given `factors`, and
26+
3. add it to the sum when it is.
27+
28+
An important advantage of this approach is that it is very easy to understand.
29+
However, it suffers from potentially performing a lot of unnecessary work, for example when all `factors` are large, or when there are no `factors` at all.
30+
31+
[Read more about this approach][filter-for-multiples].
2532

2633

2734
<!-- TODO improve section title -->
@@ -107,3 +114,7 @@ This approach saves on a lot of iteration, but is still vulnerable to excessive
107114
Fortunately it can be combined with the generator merging approach.
108115

109116
...
117+
118+
119+
120+
[filter-for-multiples]: https://exercism.org/tracks/python/exercises/sum-of-multiples/approaches/filter-for-multiples "Approach: filter for multiples"

0 commit comments

Comments
 (0)