====== Decorators and context managers ======

===== Materials =====

https://scipy-lectures.github.io/advanced/advanced_python/index.html

== Public etherpad instance ==

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

https://beta.etherpad.org/p/aspp

===== 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.

<code python>
@logger         
def g(x):
    return 2 * x

@logger
def f(x, y):
    return g(x) + g(y)
</code>

<code>
>>> 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
</code>

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

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

<code python>
import time

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

  * https://docs.python.org/3/library/time.html#time.time
==== 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.

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

>>> f(3)
here
>>> f(3)
>>>
</code>

Test with:

<code python>
def factorial(n):
    if n <= 1:
        return 1
    else:
        return n * factorial(n-1)
</code>


==== 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.

<code python>
@deprecate('do not use')
def f():
    pass
</code>
<code>
>>> f()
f is deprecated, do not use
>>> f()
>>> f()
</code>

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:

<code python>
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
</code>

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.

<code>
>>> with logtime_cm():
...     time.sleep(3)
Execution took 3.00001s
</code>

==== 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).

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

  * http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.show
  * http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.savefig
  * http://matplotlib.org/api/backend_bases_api.html#matplotlib.backend_bases.FigureCanvasBase.set_window_title
==== 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
  - checks that an exception **was** raised
  - checks that the exception is of the right type

<code>
>>> 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
</code>

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

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

<code python>
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)
</code>

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

<code>
>>> with timelimit(5):
...     looong_computation()
RuntimeError         Traceback (most recent call last)
...
RuntimeError: over the deadline
</code>

Notes: signal.signal returns the **previous** handler that
was installed. It should be restored after our context manager
is done.

Some docs on the web:
  * http://stackoverflow.com/questions/8616630/time-out-decorator-on-a-multprocessing-function

  * https://docs.python.org/3/library/exceptions.html#RuntimeError
  * https://docs.python.org/3/library/signal.html#signal.alarm
  * https://docs.python.org/3/library/signal.html#signal.signal

  * http://linux.die.net/man/2/alarm
