In [1]:
%matplotlib inline

#%config PromptManager.in_template = '>>> '
#%config PromptManager.out_template = '    '
In [2]:
from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import HtmlFormatter
import IPython

class _merge_html:
    def __init__(self, *args):
        self._html = ''.join(x._repr_html_() for x in args)
    def _repr_html_(self):
        return self._html

def showfile(filename, *extras):
    with open(filename) as f:
        code = f.read()

    formatter = HtmlFormatter()
    html = IPython.display.HTML(
        '<style type="text/css">{}</style>{}'.format(
            formatter.get_style_defs('.highlight'),
            highlight(code, PythonLexer(), formatter)))
    link = IPython.display.FileLink(filename)
    extra = (IPython.display.FileLink(arg) for arg in extras)
    return _merge_html(link, html, *extra)

Generators, decorators, context managers

Zbigniew Jędrzejewski-Szmek

George Mason University

Objectives

  • show the use of generators, decorators, and context managers
  • explain the mechanism behind the magic
  • show how to write your own

Why?

  • nicer (minimalistic, declarative, elegant)
  • separation of concerns
  • less bugs, easier refactoring

Part I: generators

In [3]:
def countdown(n):
    while n > 0:
        yield n
        n -= 1
In [4]:
counter = countdown(3)
In [5]:
next(counter)
Out[5]:
3
In [6]:
next(counter)
Out[6]:
2
In [7]:
next(counter)
Out[7]:
1
In [8]:
next(counter)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-8-ada38ea300f6> in <module>()
----> 1 next(counter)

StopIteration: 

hide-input-cell

In [74]:
import networkx as nx
G=nx.DiGraph()
G.add_edge("generator function", "generator", label='call')
G.add_edge("generator", "item", label='next()')
pos = {"generator function":(0,3), "generator":(1,2), "item":(2,1)}
nx.draw(G, with_labels=True, pos=pos)
edges = nx.get_edge_attributes(G, 'label')

_ = nx.draw_networkx_edge_labels(G, pos=pos, edge_labels=edges)

Notes

  • A generator function returns a generator when executed
  • A generator "generates" items by calling yield
  • How to spot a generator function?
In [29]:
def not_a_generator():
    return
In [30]:
def a_generator():
    yield
    return
In [31]:
def also_a_generator():
    if False:
        yield
    return

Also: execute it!

In [32]:
showfile('files/example1.py', 'files/test_example1.py')
Out[32]:
files/example1.py
# Write a generator which takes a list (or any sequence)
# and returns items from the list in random order.

def randomized(seq):
    """Iterate over seq returning items in random order.

    Making a copy of the argument or mutating the argument is *not
    allowed*.

    >>> list(randomized('abcdefghi'))
    ['b', 'a', 'e', 'd', 'f', 'c', 'h', 'i', 'g']
    """
    ...
files/test_example1.py
In [75]:
!py.test-3.4 files/test_example1.py
============================= test session starts ==============================
platform linux -- Python 3.4.1 -- py-1.4.23 -- pytest-2.6.0
plugins: cov, instafail
collected 2 items 

files/test_example1.py F.

=================================== FAILURES ===================================
______________________________ test_completeness _______________________________

    def test_completeness():
>       assert set(randomized('abcdef')) == set('abcdef')
E       TypeError: 'NoneType' object is not iterable

files/test_example1.py:4: TypeError
====================== 1 failed, 1 passed in 0.04 seconds ======================

How does the execution proceed?

In [34]:
def generator_function():
    print('--start--')
    yield 1
    print('--middle--')
    yield 2
    print('--stop--')
In [35]:
generator = generator_function()
In [36]:
next(generator)
--start--

Out[36]:
1
In [37]:
next(generator)
--middle--

Out[37]:
2
In [38]:
next(generator)
--stop--

---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-38-1d0a8ea12077> in <module>()
----> 1 next(generator)

StopIteration: 

Generator objects

  • next(g) calls g.next() (Python 2) or g.__next__() (Python 3, 4,...)
  • g.send() enables bidirectional communication
In [76]:
def bidirectional_generator_function():
    print('--start--')
    val = yield 1
    print('--got', val)
    print('--middle--')
    val = yield 2
    print('--got', val)
    print('--stop--')
In [77]:
generator = bidirectional_generator_function()
In [78]:
next(generator)
--start--

Out[78]:
1
In [79]:
generator.send('value')
--got value
--middle--

Out[79]:
2
In [80]:
next(generator)
--got None
--stop--

---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-80-1d0a8ea12077> in <module>()
----> 1 next(generator)

StopIteration: 

Careful about the first step

In [81]:
generator = bidirectional_generator_function()
generator.send('something')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-81-6de2b08dcc74> in <module>()
      1 generator = bidirectional_generator_function()
----> 2 generator.send('something')

TypeError: can't send non-None value to a just-started generator
In [82]:
generator = bidirectional_generator_function()
generator.send(None)
--start--

Out[82]:
1
In [83]:
showfile('files/example2.py', 'files/test_example2.py')
Out[83]:
files/example2.py
# Write a range replacement (adjrange, short for "adjustable range"),
# which can be prodded with .send() to change the step.
#
# This could be useful e.g. in a numerical integration routine, where
# we want to increase the step size in boring areas, and decrease in
# areas of high variability.

def adjrange(start, stop, step):
    """A range()/xrange() replacement with adjustable step.

    >>> x = adjrange(0, 7, 1)
    >>> next(g)
    0
    >>> next(g)
    1
    >>> g.send(2)
    >>> next(g)
    3
    >>> next(g)
    5
    >>> next(g)                              # doctest: +ELLIPSIS
    Traceback:
        ...
    StopIteration: ...
    """
    # fixme


# Bonus: fix the function so that adjrange(stop) works
files/test_example2.py

Handling exceptional circumstances

skip?

In [120]:
import math

def f(n):
    print('will call math.sqrt')
    ans = math.sqrt(n)
    print('math.sqrt has returned')
    return ans
    
def g(n):
    print('will call f')
    ans = 2 * f(n)
    print('f has returned')
    return ans

g(-1)
will call f
will call math.sqrt

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-120-5bdd1269a957> in <module>()
     13     return ans
     14 
---> 15 g(-1)

<ipython-input-120-5bdd1269a957> in g(n)
      9 def g(n):
     10     print('will call f')
---> 11     ans = 2 * f(n)
     12     print('f has returned')
     13     return ans

<ipython-input-120-5bdd1269a957> in f(n)
      3 def f(n):
      4     print('will call math.sqrt')
----> 5     ans = math.sqrt(n)
      6     print('math.sqrt has returned')
      7     return ans

ValueError: math domain error

Catching exceptions

In [153]:
def inverse(val):
    return 1/val

def g(val):
    try:
        return inverse(val)
    except Exception as e:
        print('caught exception', e.__class__.__name__, ':', e)
        return 0
In [157]:
print(g(5))
print(g(0))
0.2
caught exception ZeroDivisionError : division by zero
0

Doing something every time

In [154]:
def f(flag):
    try:
        if flag:
            no_such_func()
        return 11
    except Exception as e:
        print('caught exception', e.__class__.__name__, ':', e)
    finally:
        print('finally called')
In [155]:
f(False)
finally called

Out[155]:
11
In [156]:
f(True)
caught exception NameError : name 'no_such_func' is not defined
finally called

Explicitly raising exceptions

In [151]:
def sqrt(n):
    if n < 0:
        raise ValueError('no negativity please')
    return n ** 0.5
In [152]:
print(sqrt(5))
print(sqrt(-3))
2.23606797749979

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-152-02012ba21434> in <module>()
      1 print(sqrt(5))
----> 2 print(sqrt(-3))

<ipython-input-151-952e42f28e1c> in sqrt(n)
      1 def sqrt(n):
      2     if n < 0:
----> 3         raise ValueError('no negativity please')
      4     return n ** 0.5

ValueError: no negativity please

Exercise

  1. Write a function which uses random.random() to decide which exception to throw (e.g. ZeroDivisionError, ValueError, Exception, TypeError, ...).
  2. Write a try..except handler which catches multiple exception types using multiple except clauses and prints out the exceptions.

Sending exceptions into the generator

In [121]:
import math

def sqrt(n):
    try:
        print('will call math.sqrt')
        ans = math.sqrt(n)
        print('math sqrt has returned')
        return ans
    except ValueError:
        print('caught exception')
        return math.sqrt(-n) * 1j

print('+1:', sqrt(+1))
print('-1:', sqrt(-1))
will call math.sqrt
math sqrt has returned
+1: 1.0
will call math.sqrt
caught exception
-1: 1j

In [84]:
def generator_function():  # same as before
    print('--start--')
    yield 1
    print('--middle--')
    yield 2
    print('--stop--')
    
generator = generator_function()
next(generator)
--start--

Out[84]:
1
In [85]:
generator.throw(ZeroDivisionError)
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-85-5de9a8cb7d61> in <module>()
----> 1 generator.throw(ZeroDivisionError)

<ipython-input-84-c4e096b0e8ba> in generator_function()
      1 def generator_function():  # same as before
      2     print('--start--')
----> 3     yield 1
      4     print('--middle--')
      5     yield 2

ZeroDivisionError: 

.close() is used to destroy resources tied up in the generator

In [86]:
def generator_function():
    try:
        yield 'hello'
    except GeneratorExit:
        print('bye!')
In [87]:
g = generator_function()
next(g)
Out[87]:
'hello'
In [88]:
g.close()
bye!

In [89]:
showfile('files/example3.py')
Out[89]:
files/example3.py
# 'break' works on the innermost loop. If we want to break out
# of the outer loop, we often have to resort to flag variable.
# Rewrite the following example to use .close() to stop the
# outer loop.

from __future__ import print_function

def counter(n):
    i = 1
    while i <= n:
        yield i
        i += 1

def print_table():
    outer = counter(10)
    finished = False               # <---- get rid of this
    total, limit = 0, 100
    for i in outer:
        inner = counter(i)
        print(i, end=': ')
        for j in inner:
            print(i * j, end=' ')
            total += i * j
            if total >= limit:
                finished = True    # <----- and this
                break
        print()
        if finished:               # <----- and also this
            break                  #
    print('total:', total)

if __name__ == '__main__':
    print_table()
In [90]:
g = countdown(5)
g.gi_frame.f_locals
g.__sizeof__()
def gg(g):
    yield from g
ggg = gg(g)
list(ggg)
Out[90]:
[5, 4, 3, 2, 1]

Part II: decorators

    Summary
      This amazing feature appeared
      in the language almost apologetically
      and with concern that it might not
      be that useful.
    Bruce Eckel
  

  • Decorators give us a chance to modify a function after it is defined
  • Possible in Python since the beginning, but decorator syntax makes this easy
In [91]:
def deco(f):
    return f
In [92]:
@deco
def function():
    print('in function')
In [93]:
def function():                # old syntax before the introduction of @
    print('in function')
function = deco(function)

Decorators for class functions and properties

In [94]:
class A(object):
    def method(self, arg):
        return arg
a = A()
print(a.method(1))
1

In [95]:
class A(object):
    @classmethod
    def cmethod(cls, arg):
        return arg
a = A()
print(a.cmethod(2))
print(A.cmethod(3))
2
3

In [96]:
class A(object):
    @staticmethod
    def method(arg):
        return arg
a = A()
print(a.method(4))
print(a.method(5))
4
5

In [97]:
class A(object):
    @property
    def not_a_method(self):
        return 'value'
a = A()
print(a.not_a_method)
value

@property makes attributes read-only

In [98]:
class Square(object):
    def __init__(self, edge):
        self.edge = edge

    @property
    def area(self):
        """Computed area."""
        return self.edge ** 2

s = Square(2)
s.area
Out[98]:
4
In [99]:
s.area = 3
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-99-01915eb21e8a> in <module>()
----> 1 s.area = 3

AttributeError: can't set attribute

@property-ies can be also be read-write

In [100]:
class Square(object):
    def __init__(self, edge):
        self.edge = edge

    @property
    def area(self):
        "Compute area from edge"
        return self.edge ** 2

    @area.setter
    def area(self, area):
        "Set edge by computing from area"
        self.edge = area ** 0.5

s = Square(2)
s.area
Out[100]:
4
In [101]:
s.area = 9
s.edge
Out[101]:
3.0

The property triple: setter, getter, deleter

  • attribute access s.edge calls area.getx
  • set with @property
  • setting s.edge calls area.setx
  • set with @area.setter
  • deleting s.edge calls area.delx
  • set with @area.deleter

Implementing decorators

A decorator can be used to:

do stuff when a function is created

or replace a function (with a wrapper)

Do something when a function is defined

(this means that the decorator returns the argument it received)

In [102]:
FUNCTIONS_DEFINED_BY_ORDER = []

def remember_this_function(f):
    print('just decorating', f)
    FUNCTIONS_DEFINED_BY_ORDER.append(f)
    return f
In [103]:
@remember_this_function
def my_func():
    print('just doing my job')
just decorating <function my_func at 0x7ff48d17fd90>

In [104]:
my_func()
FUNCTIONS_DEFINED_BY_ORDER
just doing my job

Out[104]:
[<function __main__.my_func>]
In [105]:
showfile('files/decorator_attribute.py')
Out[105]:
files/decorator_attribute.py
# Sometimes we want to mark a function with an attribute.
# Define the decorator testthis which will set the
# attribute ._run_test on the function object. That
# function will then be tested. Run tests with:
#   python3 decorator_attribute.py
# or
#   python2 decorator_attribute.py

from __future__ import print_function

def testthis(func):
    ...

# This should be run as a test
def example_1():
    print('testing this')

# This should be run as a test
def example_2():
    print('testing that')

if __name__ == '__main__':
    print('running the tests')
    for name, value in dict(locals()).items():
        if getattr(value, '_run_test', False):
            print('running', name)
            value()

# To test run:
#   python3 decorator_attribute.py
# or
#   python2 decorator_attribute.py
# and check if the output contains
#   running example_1
#   running example_2
# No automatic test, sorry!

# Something like this done by
#   @unittest.skip, @unittest.expectedFailure,
#   @pytest.mark.skipif, @pytest.mark.xfail.

Replacing the function, a.k.a. doing something every time

Example: log the arguments and return value whenever a function is called

In [106]:
def logargs(f):
    def wrapper(*args, **kwargs):
        print('-- calling', f.__name__, 'with', args, 'and', kwargs)
        ans = f(*args, **kwargs)
        print('-- calling', f.__name__, 'results in', ans)
        return ans
    return wrapper
In [107]:
@logargs
def thinker(x, y, z):
    "This function ponders for a moment and then returns the result"
    print('let me think')
    return x + ' ' + y + ' ' + z

thinker('mala', 'kava', z='s mlijekom')
-- calling thinker with ('mala', 'kava') and {'z': 's mlijekom'}
let me think
-- calling thinker results in mala kava s mlijekom

Out[107]:
'mala kava s mlijekom'
In [108]:
showfile('files/deprecation.py')
Out[108]:
files/deprecation.py
# The decorator below is supposed to log a warning,
# but just once, not to annoy the user too much.

import math

def deprecate(f):
    """Log a warning when the function is called for the first time.

    >>> @deprecate
    ... def f():
    ...     pass
    >>> f()
    Function f is deprecated, please use something else!
    >>> f()
    >>> f()
    """
    ...
    return f

@deprecate
def square_root(x):
    return math.sqrt(x)

# To test this file, use:
#  py.test-3.4 --doctest-modules deprecation.py
# or
#  python3 -m doctest deprecation.py

The "docstring problem"

  • Our function is replaced by the wrapper which is "anonymous"
  • we lost the docstring
  • the attributes as missing
  • the signature is generic
In [109]:
print(thinker)
help(thinker)
<function logargs.<locals>.wrapper at 0x7ff48d16d620>
Help on function wrapper in module __main__:

wrapper(*args, **kwargs)


In [110]:
%%python3
def logargs(f):
    def wrapper(*args, **kwargs):
        print('-- calling', f.__name__, 'with', args, 'and', kwargs)
        ans = f(*args, **kwargs)
        print('-- calling', f.__name__, 'result is', ans)
        return ans
    return wrapper

@logargs
def f():
    1/0

f()
-- calling f with () and {}

Traceback (most recent call last):
  File "<stdin>", line 14, in <module>
  File "<stdin>", line 5, in wrapper
  File "<stdin>", line 12, in f
ZeroDivisionError: division by zero

Copy the docstring and other attributes of the original

  • __doc__
  • __module__ and __name__
  • __dict__
  • eval is required for the rest :(
  • module decorator compiles functions dynamically
In [111]:
import functools

def logargs(f):
    def wrapper(*args, **kwargs):
        print('-- calling', f.__name__, 'with', args, 'and', kwargs)
        ans = f(*args, **kwargs)
        print('-- calling', f.__name__, 'result is', ans)
        return ans
    return functools.update_wrapper(wrapper, f)
#     This ^^^^^^^^^^^^^^^^^^^^^^^^^       ^^^^ is new
In [112]:
@logargs
def f():
    "This function doesn't do anything really."
    print('here')

f()
-- calling f with () and {}
here
-- calling f result is None

In [113]:
print(f)
help(f)
<function f at 0x7ff49547a048>
Help on function f in module __main__:

f()
    This function doesn't do anything really.


Exercise n

Modify decorator from previous exercise to use functools.update_wrapper or functools.wraps.

Decorators which take arguments

In [114]:
import functools

def deprecated(message):
    def decorator(func):
        seen = False
        def wrapper(*args, **kwargs):
            nonlocal seen
            if not seen:
                print('{} is depracted: {}'.format(f.__name__, message))
                seen = True
            return func(*args, **kwargs)
        return functools.update_wrapper(wrapper, func)
    return decorator
In [115]:
@deprecated('use math.sqrt instead')
def mysqrt(n):
    """Compute the square root using Heron's method.
    
    Taken from http://stackoverflow.com/posts/12851282/revisions
    """
    if n == 0:
        return 0
    if n < 1:
        return mysqrt(n * 4) / 2
    if 4 <= n:
        return mysqrt(n / 4) * 2
    x = (n + 1) / 2
    for i in range(5):
        x = (x + n/x) / 2
    x = (x + n/x) / 2
    return x

print('5:', mysqrt(5))
print('3:', mysqrt(3))
f is depracted: use math.sqrt instead
5: 2.23606797749979
3: 1.7320508075688772

In [116]:
import functools
import math

def deprecated(message):
    "Create a decorator which will emit message."
    print('creating decorator')
    
    def decorator(func):
        "Decorate a function to emit the message on first call."
        print('decorating function', func.__name__)
        func.warning_seen = False
        def wrapper(*args, **kwargs):
            print('the wrapper for', func.__name__, 'is running')
            if not func.warning_seen:
                print('{} is depracted: {}'.format(f.__name__, message))
                func.warning_seen = True
            return func(*args, **kwargs)
        return functools.update_wrapper(wrapper, func)

    return decorator
In [117]:
@deprecated('use math.sqrt instead')
def mysqrt(n):
    print('mysqrt is running')
    return math.sqrt(n)
creating decorator
decorating function mysqrt

In [118]:
print('5:', mysqrt(5))
print('3:', mysqrt(3))
the wrapper for mysqrt is running
f is depracted: use math.sqrt instead
mysqrt is running
5: 2.23606797749979
the wrapper for mysqrt is running
mysqrt is running
3: 1.7320508075688772

Decorators work for classes too

  • same principle
  • much less exciting
In [119]:
def deco(f):
    print(f.__name__, 'just got defined')
    return f
    
@deco
class A(object):
    def __init__(self):
        print(self.__class__.__name__, 'object is created')

print('A:', A)
A just got defined
A: <class '__main__.A'>

In [147]:
a = A()
print('a:', a)
A object is created
a: <__main__.A object at 0x7ff48d1c55c0>

Example (skipped)

See Flask for a beautiful application of this mechanism.

Decorators — summary:

  • decorators work for functions and classes
  • decorators can return the original object or a replacement
  • decorators are usually implemented as functions, but could be classes
  • functools help preserve __doc__, __name__, __module__
  • nested functions are useful to hold state

Part III: context managers

How to make sure resources are freed?

In [127]:
import os

f = open('temporary_file', 'w')
try:
    f.write('blah blah')
    # do some processing using the temporary file
finally:
    os.unlink(f.name)

General pattern

  1. Acquire resources
  2. try to use the resources
  3. finally cleanup

Context managers

In [128]:
class Manager(object):
    def __enter__(self): return None
    def __exit__(self, *args): pass
    
def do_something(arg): pass
In [129]:
manager = Manager()
with manager as var:
    do_something(var)
In [130]:
manager = Manager()
var = manager.__enter__()
try:
    do_something(var)
finally:
    manager.__exit__(...)

Defining context managers using generator functions

In [131]:
import contextlib

@contextlib.contextmanager
def some_generator(*args):
    print('doing setup')
    try:
        yield 'value-of-variable'
    finally:
        print('doing cleanup')
        pass

A context manager defined using a generator function

In [132]:
import os

@contextlib.contextmanager
def temporary_file(name):
    f = open(name, 'w')
    try:
        yield f
    finally:
        os.unlink(f.name)
In [133]:
import time
time.time()
Out[133]:
1410261874.6944594
In [134]:
showfile('files/cm_timer.py')
Out[134]:
files/cm_timer.py
# We want to measure (and log) how long a chunk of code
# took to execute.
# 
# We can use time.time() function to get a timestamp right
# before and right after, and print out the difference.
#
# Implement logging the duration of the execution

from __future__ import print_function

import time
import random

def timeit():
    """Context manager which logs the duration of execution.

    >>> with timeit():                    # doctest: +ELLIPSIS
    ...     time.sleep(1)
    Calculations took 1...s

    >>> with timeit():                    # doctest: +ELLIPSIS
    ...     time.sleep(3)
    Calculations took 3...s
    """
    ...
    print('Calculations took XXXs')

def long_haul(*args):
    "Uses time.sleep() to simulate long calculations"
    time.sleep(5 * random.random())
    return sum(args)

if __name__ == '__main__':
    print('Will do calculations now')
    with timeit():
        ans = long_haul(1, 2, 3)
    print(ans)

# Please note:
# We could do it also in the traditional way
# using something like:
#
#   print('Will do calculations now')
#   start = time.time()
#   ans = long_haul()
#   duration = time.time() - start
#   print('Calculations took {}s'.format(duration))
#
# but this is harder to read, and leaves the
# 'start' and 'duration' names in the local scope.
#
# To test this file, use:
#  py.test-3.4 --doctest-modules cm_timer.py -v
# or
#  python3 -m doctest cm_timer.py

Nested try..finally calls

In [135]:
def buggy_code():
    raise Exception('boo boo')
    
def buggy_cleanup():
    print('buggy_cleanup running')
    raise Exception('bug in cleanup')
In [136]:
try:
    try:
        print('working')
        buggy_code()
        print('work done')
    finally:
        print('finalizer a')
        buggy_cleanup()
        print('cleanup done')
finally:
    print('finalizer b')
working
finalizer a
buggy_cleanup running
finalizer b

---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-136-40bfd9cbedb1> in <module>()
      6     finally:
      7         print('finalizer a')
----> 8         buggy_cleanup()
      9         print('cleanup done')
     10 finally:

<ipython-input-135-39547461b499> in buggy_cleanup()
      4 def buggy_cleanup():
      5     print('buggy_cleanup running')
----> 6     raise Exception('bug in cleanup')

Exception: bug in cleanup

Nested context managers

In [137]:
@contextlib.contextmanager
def cm(name):
    print('--entering', name)
    try:
        yield
    finally:
        print('--exiting', name)
        
with cm('outer'), cm('inner'):
    print('real stuff')
--entering outer
--entering inner
real stuff
--exiting inner
--exiting outer

Objects can be their own managers

Context manager must have __enter__() and __exit__().

  • an object has __enter__() and __exit__() methods
  • __enter__() returns self
In [138]:
f = open('some-file')
help(f.__enter__)
help(f.__exit__)
Help on built-in function __enter__:

__enter__(...) method of _io.TextIOWrapper instance

Help on built-in function __exit__:

__exit__(...) method of _io.TextIOWrapper instance


In [139]:
with open('/etc/fstab', 'rt') as f:
    with open('/tmp/g', 'wt') as g:
        g.write(f.read())
In [140]:
f.closed, g.closed
Out[140]:
(True, True)
In [141]:
with open('/etc/fstab', 'rt') as f, \
     open('/tmp/g', 'wt') as g:
        g.write(f.read())

Handling exceptions in the context manager

In [142]:
showfile('files/cm_assert_raises.py')
Out[142]:
files/cm_assert_raises.py
# This happens most often in testing. We would like to make sure that
# a block of code raises an exception of the certain type.
# If it does not raise an exception, *we* raise an error.
# If it raises an unexpected exception, we raise an error *too*.

# Write a context manager which does checks that an exception is
# raised properly and convert the example below. Make things
# less ugly too.

import math

def log_of_sqrt(n):
    return math.log(math.sqrt(n))

def assert_raises(exception_type):
    ...

def test_sqrt():
    try:
        log_of_sqrt(-5)
    except Exception as e:
        if isinstance(e, ValueError):  # what is Pythonic way
                                       # to check exception type?
            pass
        else:
            raise AssertionError('bad exception')
    finally:
        raise AssertionError('no expected exception')

# Note: this is implemented by
#  pytest.raises,
#  unittest.TestCase.assertRaises.

Summary

  • function/subroutine — inner block of code
  • decorator — function intro and outro
  • context manager — try..except..finally..else cleanup
  • generators & iterators & generator expressions — loop control
In [143]:
IPython.display.Image('files/tux-asleep-2.png')
Out[143]:
In [144]:
showfile('files/capture_stdout.py')
Out[144]:
files/capture_stdout.py
# Write a context manager which temporarily replaces
# sys.stdout with a io.StringIO() object and thus
# captures the output of print().

import contextlib

@contextlib.contextmanager
def capture_stdout():
    r"""Replace sys.stdout with String() for the block.

    >>> with capture_stdout() as out:
    ...     print('goo')
    >>> out.getvalue()
    'goo\n'
    """
    try:
        yield ...
    finally:
        pass

# To test this file, use:
#  py.test-3.4 --doctest-modules capture_stdout.py
# or
#  python3 -m doctest capture_stdout.py
In [145]:
showfile('files/test_example3.py')
Out[145]:
files/test_example3.py
import contextlib
import io

from example3 import print_table
from capture_stdout import capture_stdout

def test_sum():
    with capture_stdout() as output:
        print_table()
    assert 'sum: 115' in output.getvalue()
In [146]:
showfile('files/memoize.py', 'files/test_memoize.py', 'files/test_memoize_mutable.py')
Out[146]:
files/memoize.py
# Implement a cache, which will store the values
# returned by the function for specific inputs.

import functools

def memoize(func):
    ...
    return func

@memoize
def fibonacci(n):
    """Returns fibonnaci number n.

    See http://en.wikipedia.org/wiki/Fibonacci_number.

    >>> print(fibonacci.cache)
    {}
    >>> fibonacci(1)
    1
    >>> fibonacci(2)
    1
    >>> fibonacci(10)
    55
    >>> fibonacci.cache[10]
    55
    >>> fibonacci(40)
    102334155
    """
    assert n >= 0
    if n < 2:
        return n
    else:
        return fibonacci(n-1) + fibonacci(n-2)

# Test this with
#   py.test-3.4 -v test_memoize.py
# Once that passes, try
#   py.test-3.4 -v test_memoize_mutable.py
files/test_memoize.py
files/test_memoize_mutable.py