(a) The first improvement to this algorithm is given by the function `factors1()`

, below. Within IPython, this function can be written in the default text editor and initialized using the `%edit`

magic :

```
def factors1(n):
facs = set()
for i in range(1, int(math.sqrt(n)) + 1):
if not n % i:
facs |= {i, n // i}
return facs
```

By adding both the number `i`

which is found to be a factor of `n`

and `n // i`

(which is also a factor of `n`

) it is not necessary to test values of \texttt{i} greater than the square root of `n`

: such values will already have been included as `n // i`

. We use a `set()`

so that values for which `i`

and `n // i`

are equal are not duplicated in the output.
To time it, call it with some large(ish) integer using the `%timeit`

magic:

```
In [x]: %timeit factors1(123456789)
1000 loops, best of 3: 1.33 ms per loop
```

(b) The same algorithm can be coded using a generator expression:

```
def gen_factors2(n):
yield 1
for i in range(2, int(math.sqrt(n)) + 1):
if not n % i:
yield i
yield n // i
yield n
def factors2(n):
return set(gen_factors2(n))
```

but does not lead to a significantly different execution time:

```
In [131]: %timeit factors2(123456789)
1000 loops, best of 3: 1.27 ms per loop
```

(Generators are very *memory* efficient but cannot generally be expected to be *faster* than list comprehensions.)