# Advanced Scientific Programming in Python

a Summer School by the G-Node, the Bernstein Center for Computational
Neuroscience Munich and the Graduate School of Systemic Neurosciences

# Decorators and context managers

## Materials

Use this to post questions, links, comments, and sometimes solutions to exercises.

## Exercises

### Exercise 1: Wrapping a function to do something on every invocation

Write a decorator which prints the arguments and the return value of the wrapped function.

```@logger
def g(x):
return 2 * x

@logger
def f(x, y):
return g(x) + g(y)```
```>>> f(5, 6)
f is called with args [5, 6] kwargs {}
g is called with args [5] kwargs {}
g returns 10
g is called with args [6] kwargs {}
g returns 12
f returns 22```

### Exercise 2: A decorator which times function execution

Write 'timeit' decorator which prints how long a function took to execute.

```import time

@timeit
def loooong():
time.sleep(5)
return 'ans'```
```>>> looong()
looong took 5.0323423s
ans```

### Exercise 3: A cache

Write a decorator which caches the results of some function. Store the results of every invocation. Every time the function is called with the same arguments, simply retrieve the value from storage. Otherwise call the function.

```>>> @cached
>>> def f(x): print('here')

>>> f(3)
here
>>> f(3)
>>>```

Test with:

```def factorial(n):
if n <= 1:
return 1
else:
return n * factorial(n-1)```

### Exercise 3a: Keeping state in decorators

Write a decorator which prints a warning the first time a given function is executed. This is a modification of `deprecate()` from previous exercise.

```@deprecate('do not use')
def f():
pass```
```>>> f()
f is deprecated, do not use
>>> f()
>>> f()```

The trick is how to store the state!

### Exercise 4: Returning a list of results

When a function returns a list of results, we might need to gather those results in a list:

```def lucky_numbers(n):
ans = []
for i in range(n):
if i % 7 != 0:
continue
if sum(int(digit) for digit in str(i)) % 3 != 0:
continue
ans.append(i)
return ans```

This looks much nicer when written as a generator. First convert lucky_numbers to be a generator.

Later, write `listize` decorator which gathers the results from a generator and returns a list and use it to wrap the new lucky_numbers().

Alternatively, write `arrayize` decorator which return the results in a numpy array.

### Exercise 5: A context manager to time execution

Before we wrote a decorator which would print how long a function took to execute. Now write a context manager which does the same thing.

```>>> with logtime_cm():
...     time.sleep(3)
Execution took 3.00001s```

### Exercise 6: A matplotlib context manager

This is synthesized from a real program that I use to analyze results and create graphs. Matplotlib figures can be plotted on screen, and they can also be saved to file with `figure.savefig()`.

Write a context manager which gives you a matplotlib figure object, and either saves the plot to a file or pops it up on screen, depending on a global parameter `SAVEFIGS` (in a real program this parameter would be settable by a commandline option).

```with save_or_plot('name') as f:
ax = f.gca()
ax.plot([0, 3, 2, 5])
ax.set_xlabel('x')
ax.set_ylabel('y')```

### Exercise 7: Checking exception raising

This example comes from unit testing. We want to make sure that we raise the right exceptions on errors.

Write a cm 'assert_raises' that

1. checks that an exception was raised
2. checks that the exception is of the right type
```>>> with assert_raises(ZeroDivisionError):
...     1 / 0

>>> with assert_raises(ZeroDivisionError):
...     0[0] / 0
Traceback (most recent call last):
...
AssertionError: expected ZeroDivisionError not AttributeError

>>> with assert_raises(ZeroDivisionError):
...     0 / 1
Traceback (most recent call last):
...
AssertionError: expected ZeroDivisionError exception```

### Exercise 8: Context manager to limit computation time

The OS call alarm can be used to interrupt a process:

```import signal
import time

def _handler(signum, frame):
print('_handler called for signal', signum)

oldhandler = signal.signal(signal.SIGALRM, _handler)
signal.alarm(3)
time.sleep(100)```

Write a context manager which limits the execution time to the given number of seconds:

```>>> with timelimit(5):
...     looong_computation()
RuntimeError         Traceback (most recent call last)
...