%matplotlib inline
#%config PromptManager.in_template = '>>> '
#%config PromptManager.out_template = ' '
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)
Zbigniew Jędrzejewski-Szmek
George Mason University
def countdown(n):
while n > 0:
yield n
n -= 1
counter = countdown(3)
next(counter)
3
next(counter)
2
next(counter)
1
next(counter)
--------------------------------------------------------------------------- StopIteration Traceback (most recent call last) <ipython-input-8-ada38ea300f6> in <module>() ----> 1 next(counter) StopIteration:
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)
yielddef not_a_generator():
return
def a_generator():
yield
return
def also_a_generator():
if False:
yield
return
Also: execute it!
showfile('files/example1.py', 'files/test_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']
"""
...
!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 ======================
def generator_function():
print('--start--')
yield 1
print('--middle--')
yield 2
print('--stop--')
generator = generator_function()
next(generator)
--start--
1
next(generator)
--middle--
2
next(generator)
--stop--
--------------------------------------------------------------------------- StopIteration Traceback (most recent call last) <ipython-input-38-1d0a8ea12077> in <module>() ----> 1 next(generator) StopIteration:
next(g) calls g.next() (Python 2) or g.__next__() (Python 3, 4,...)g.send() enables bidirectional communicationdef bidirectional_generator_function():
print('--start--')
val = yield 1
print('--got', val)
print('--middle--')
val = yield 2
print('--got', val)
print('--stop--')
generator = bidirectional_generator_function()
next(generator)
--start--
1
generator.send('value')
--got value --middle--
2
next(generator)
--got None --stop--
--------------------------------------------------------------------------- StopIteration Traceback (most recent call last) <ipython-input-80-1d0a8ea12077> in <module>() ----> 1 next(generator) StopIteration:
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
generator = bidirectional_generator_function()
generator.send(None)
--start--
1
showfile('files/example2.py', 'files/test_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
skip?
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
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
print(g(5))
print(g(0))
0.2 caught exception ZeroDivisionError : division by zero 0
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')
f(False)
finally called
11
f(True)
caught exception NameError : name 'no_such_func' is not defined finally called
def sqrt(n):
if n < 0:
raise ValueError('no negativity please')
return n ** 0.5
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
random.random() to decide which exception to throw (e.g. ZeroDivisionError, ValueError, Exception, TypeError, ...).try..except handler which catches multiple exception types using multiple except clauses and prints out the exceptions.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
def generator_function(): # same as before
print('--start--')
yield 1
print('--middle--')
yield 2
print('--stop--')
generator = generator_function()
next(generator)
--start--
1
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 generatordef generator_function():
try:
yield 'hello'
except GeneratorExit:
print('bye!')
g = generator_function()
next(g)
'hello'
g.close()
bye!
showfile('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()
g = countdown(5)
g.gi_frame.f_locals
g.__sizeof__()
def gg(g):
yield from g
ggg = gg(g)
list(ggg)
[5, 4, 3, 2, 1]
Summary
This amazing feature appeared
in the language almost apologetically
and with concern that it might not
be that useful.
Bruce Eckel
def deco(f):
return f
@deco
def function():
print('in function')
def function(): # old syntax before the introduction of @
print('in function')
function = deco(function)
class A(object):
def method(self, arg):
return arg
a = A()
print(a.method(1))
1
class A(object):
@classmethod
def cmethod(cls, arg):
return arg
a = A()
print(a.cmethod(2))
print(A.cmethod(3))
2 3
class A(object):
@staticmethod
def method(arg):
return arg
a = A()
print(a.method(4))
print(a.method(5))
4 5
class A(object):
@property
def not_a_method(self):
return 'value'
a = A()
print(a.not_a_method)
value
@property makes attributes read-onlyclass Square(object):
def __init__(self, edge):
self.edge = edge
@property
def area(self):
"""Computed area."""
return self.edge ** 2
s = Square(2)
s.area
4
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-writeclass 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
4
s.area = 9
s.edge
3.0
property triple: setter, getter, deleters.edge calls area.getx@propertys.edge calls area.setx@area.setters.edge calls area.delx@area.deleterA decorator can be used to:
do stuff when a function is created
or replace a function (with a wrapper)
(this means that the decorator returns the argument it received)
FUNCTIONS_DEFINED_BY_ORDER = []
def remember_this_function(f):
print('just decorating', f)
FUNCTIONS_DEFINED_BY_ORDER.append(f)
return f
@remember_this_function
def my_func():
print('just doing my job')
just decorating <function my_func at 0x7ff48d17fd90>
my_func()
FUNCTIONS_DEFINED_BY_ORDER
just doing my job
[<function __main__.my_func>]
showfile('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.
Example: log the arguments and return value whenever a function is called
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
@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
'mala kava s mlijekom'
showfile('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
print(thinker)
help(thinker)
<function logargs.<locals>.wrapper at 0x7ff48d16d620> Help on function wrapper in module __main__: wrapper(*args, **kwargs)
%%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
__doc____module__ and __name____dict__eval is required for the rest :(decorator compiles functions dynamicallyimport 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
@logargs
def f():
"This function doesn't do anything really."
print('here')
f()
-- calling f with () and {}
here
-- calling f result is None
print(f)
help(f)
<function f at 0x7ff49547a048>
Help on function f in module __main__:
f()
This function doesn't do anything really.
nModify decorator from previous exercise to use functools.update_wrapper or functools.wraps.
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
@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
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
@deprecated('use math.sqrt instead')
def mysqrt(n):
print('mysqrt is running')
return math.sqrt(n)
creating decorator decorating function mysqrt
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
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'>
a = A()
print('a:', a)
A object is created a: <__main__.A object at 0x7ff48d1c55c0>
See Flask for a beautiful application of this mechanism.
Decorators — summary:
functools help preserve __doc__, __name__, __module__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
try to use the resourcesfinally cleanupclass Manager(object):
def __enter__(self): return None
def __exit__(self, *args): pass
def do_something(arg): pass
manager = Manager()
with manager as var:
do_something(var)
manager = Manager()
var = manager.__enter__()
try:
do_something(var)
finally:
manager.__exit__(...)
import contextlib
@contextlib.contextmanager
def some_generator(*args):
print('doing setup')
try:
yield 'value-of-variable'
finally:
print('doing cleanup')
pass
import os
@contextlib.contextmanager
def temporary_file(name):
f = open(name, 'w')
try:
yield f
finally:
os.unlink(f.name)
import time
time.time()
1410261874.6944594
showfile('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
try..finally callsdef buggy_code():
raise Exception('boo boo')
def buggy_cleanup():
print('buggy_cleanup running')
raise Exception('bug in cleanup')
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
@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
Context manager must have __enter__() and __exit__().
__enter__() and __exit__() methods__enter__() returns selff = 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
with open('/etc/fstab', 'rt') as f:
with open('/tmp/g', 'wt') as g:
g.write(f.read())
f.closed, g.closed
(True, True)
with open('/etc/fstab', 'rt') as f, \
open('/tmp/g', 'wt') as g:
g.write(f.read())
showfile('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.
try..except..finally..else cleanupIPython.display.Image('files/tux-asleep-2.png')
showfile('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
showfile('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()
showfile('files/memoize.py', 'files/test_memoize.py', 'files/test_memoize_mutable.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