Python Training at VMWare Pune - Day 1

Sep 27-29, 2017 Vikrant Patil

These notes are available online at http://notes.pipal.in/2017/vmware-pune-advpy

© Pipal Academy LLP

Day 1 | Day 2 | Day 3

Quick Recap

In [1]:
4 + 2
Out[1]:
6
In [2]:
4 ** 1000
Out[2]:
114813069527425452423283320117768198402231770208869520047764273682576626139237031385665948631650626991844596463898746277344711896086305533142593135616665318539129989145312280000688779148240044871428926990063486244781615463646388363947317026040466353970904996558162398808944629605623311649536164221970332681344168908984458505602379484807914058900934776500429002716706625830522008132236281291761267883317206598995396418127021779858404042159853183251540889433902091920554957783589672039160081957216630582755380425583726015528348786419432054508915275783882625175435528800822842770817965453762184851149029376
In [3]:
5 / 2
Out[3]:
2.5
In [4]:
5 // 2
Out[4]:
2
In [5]:
"There" "are" "string"
Out[5]:
'Therearestring'
In [7]:
"you" + "can " + "add" + "string"
Out[7]:
'youcan addstring'
In [8]:
 "you can multiply by integers" * 3 
Out[8]:
'you can multiply by integersyou can multiply by integersyou can multiply by integers'
In [9]:
"you can have ' inside a double quoted string"
Out[9]:
"you can have ' inside a double quoted string"
In [10]:
'you can " inside single quoted string'
Out[10]:
'you can " inside single quoted string'
In [11]:
multi = """
this 
is 
multi line
string
"""
In [12]:
print(multi)
this 
is 
multi line
string

In [13]:
digits = list(range(10))
In [14]:
digits
Out[14]:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
In [15]:
digits[0]
Out[15]:
0
In [16]:
digits[1:6] # new list containing items from 1st index till 6th (excluding)
Out[16]:
[1, 2, 3, 4, 5]
In [17]:
digits[1:] # everything from 1st index
Out[17]:
[1, 2, 3, 4, 5, 6, 7, 8, 9]
In [18]:
digits[:5] # strarting from zeroth till 5 th (excuding)
Out[18]:
[0, 1, 2, 3, 4]
In [19]:
digits[-1]
Out[19]:
9
In [20]:
digits[-2]
Out[20]:
8
In [21]:
digits[:-1] # all items except last
Out[21]:
[0, 1, 2, 3, 4, 5, 6, 7, 8]
In [22]:
digits[:]
Out[22]:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
In [23]:
digits[2:7:2]
Out[23]:
[2, 4, 6]
In [24]:
word = "madam"
In [25]:
word[0]
Out[25]:
'm'
In [27]:
word == word[::-1] # this is how I can check if given string is palindrome
Out[27]:
True

You can do lot of stuff in sigle statement using list comprehensions

In [30]:
numbers = list(range(10))
In [31]:
numbers
Out[31]:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
In [32]:
[n*n for n in numbers]
Out[32]:
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
In [33]:
[n*n*n for n in numbers]
Out[33]:
[0, 1, 8, 27, 64, 125, 216, 343, 512, 729]
In [34]:
[word.upper() for word in "Some sentence with some words in it".split() ]
Out[34]:
['SOME', 'SENTENCE', 'WITH', 'SOME', 'WORDS', 'IN', 'IT']
In [35]:
[i for i in range(20) if i%2 == 0]
Out[35]:
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

[dosomething(x) for x in items if somecondition(x)]

In [36]:
"it" in "not english"
Out[36]:
False
In [37]:
"hell" in "hello"
Out[37]:
True
In [39]:
person = {
    "name":"Alice",
    "email":"alice@wonder.land",
    "roles":["Admin", "Staff"]
}
In [40]:
person['name']
Out[40]:
'Alice'
In [41]:
[name for name in person.keys()]
Out[41]:
['name', 'email', 'roles']
In [42]:
[v for v in person.values()]
Out[42]:
['Alice', 'alice@wonder.land', ['Admin', 'Staff']]
In [44]:
[(k,v) for k,v in person.items()]
Out[44]:
[('name', 'Alice'),
 ('email', 'alice@wonder.land'),
 ('roles', ['Admin', 'Staff'])]

Example: a simple csv parser

In [45]:
%%file data.csv
A1,B1,C1
A2,B2,C2
A3,B3,C3
A4,B4,C5
Writing data.csv
In [46]:
open("data.csv").readlines()
Out[46]:
['A1,B1,C1\n', 'A2,B2,C2\n', 'A3,B3,C3\n', 'A4,B4,C5']
In [47]:
[line.strip().split(",") for line in open("data.csv").readlines() ]
Out[47]:
[['A1', 'B1', 'C1'],
 ['A2', 'B2', 'C2'],
 ['A3', 'B3', 'C3'],
 ['A4', 'B4', 'C5']]
In [50]:
with open("data.csv") as f:
    print([line.strip().split(",") for line in open("data.csv").readlines()])
[['A1', 'B1', 'C1'], ['A2', 'B2', 'C2'], ['A3', 'B3', 'C3'], ['A4', 'B4', 'C5']]

If you need to open a file for writing, then you must close it. Only on closing file handle you can be sure that contents are flushed to disk. with block is one nice way to close file handles automativally

In [52]:
with open("sample.txt", "w") as sample:
    sample.write("one\n")
    sample.write("two\n")
    sample.write("three\n")

Problem: creating a dictionary from two list

In [53]:
dict([(1,'a'), (2,'b'), (3, 'c')])
Out[53]:
{1: 'a', 2: 'b', 3: 'c'}
In [58]:
z = zip(range(5), ['0', '1', '2', '3', '4'])
In [59]:
list(z)
Out[59]:
[(0, '0'), (1, '1'), (2, '2'), (3, '3'), (4, '4')]
In [61]:
dict(zip(range(5), ['0', '1', '2', '3', '4']))
Out[61]:
{0: '0', 1: '1', 2: '2', 3: '3', 4: '4'}
In [62]:
digits = range(4)
In [63]:
strdigits = [str(n) for n in digits]
In [66]:
list(digits)
Out[66]:
[0, 1, 2, 3]
In [65]:
strdigits
Out[65]:
['0', '1', '2', '3']
In [67]:
for d, c in zip(digits, strdigits):
    print(d, c)
0 0
1 1
2 2
3 3

Functions

Positional arguments

In [69]:
def cylinder_volume(radius, height):
    return 3.14*radius*radius*height

These are called positional arguments

In [70]:
cylinder_volume(5, 10) #radius first and then height
Out[70]:
785.0
In [71]:
cylinder_volume(10, 5) # oops... it will wrong results and will be difficult to debug
Out[71]:
1570.0

How can we fix this?

In [72]:
cylinder_volume(radius=5, height=10)
Out[72]:
785.0
In [73]:
cylinder_volume(5, height=10)
Out[73]:
785.0
In [74]:
cylinder_volume(radius=5, 10)
  File "<ipython-input-74-ecfac9fa4968>", line 1
    cylinder_volume(radius=5, 10)
                             ^
SyntaxError: positional argument follows keyword argument

Default arguments

In [75]:
import os
In [76]:
def location(name, home="/home/vikrant/"):
    """
    returns virtualenv path on my machine
    """
    return os.path.sep.join([home, "usr", "local", name])
In [77]:
location("jupyter")
Out[77]:
'/home/vikrant//usr/local/jupyter'
In [78]:
location("jupyter", home= "/Users/vikrant")
Out[78]:
'/Users/vikrant/usr/local/jupyter'

Note that default values of arguments are defined at the time of function defination

In [79]:
import random

def number():
    return random.random()

def func(a, b=number()):
    print(a, b)
In [80]:
for i in range(5):
    func(i)
0 0.2288526901459801
1 0.2288526901459801
2 0.2288526901459801
3 0.2288526901459801
4 0.2288526901459801
In [81]:
def func(a, b=None):
    if not b:
        b = number()
    print(a,b)
In [83]:
for i in [4,2,7,3,9,10]:
    func(i)
4 0.7948288305278052
2 0.038413250627874373
7 0.5294486391817688
3 0.5260122409671809
9 0.9785418202796095
10 0.44011493748538544

You should also be very carefull when you pass default arguments that are mutable! basically always pass only immutable objects as default values.

In [84]:
def append(a, values=[]):
    values.append(a)
    return values
In [85]:
append("A")
Out[85]:
['A']
In [86]:
append("B")
Out[86]:
['A', 'B']
In [89]:
def append(a, values=None):
    if values is None:
        values = []
    values.append(a)
    return values
In [90]:
append("A")
Out[90]:
['A']
In [91]:
append("B")
Out[91]:
['B']
In [95]:
help(os.path.sep.join)
Help on built-in function join:

join(...) method of builtins.str instance
    S.join(iterable) -> str
    
    Return a string which is the concatenation of the strings in the
    iterable.  The separator between elements is S.

Only Named Arguments: With python3 there is sytanx to enforce few arguments with names only.

In [96]:
def sumation(values, *, initial=0):
    total = initial
    for v in values:
        total += v
    return total
In [97]:
sumation(range(10), initial=50)
Out[97]:
95
In [98]:
sumation(range(10), 50)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-98-8804fd3ebb8c> in <module>()
----> 1 sumation(range(10), 50)

TypeError: sumation() takes 1 positional argument but 2 were given
In [99]:
sum([1,2,3,4,5])
Out[99]:
15

If i want to build a genericsum function which works like this

genericsum(1,2)
3
genericsum(1,2,3,4,5)
genericsum(1.....)

Variable number of arguments : Ability to pass variable number of arguments to functions , makes the code scriptable

In [100]:
def func(*args):
    pass
In [101]:
func(1, 2, 3)
In [102]:
func(1)
In [103]:
func("hello", "test")
In [104]:
def func(*args):
    print(args)
In [105]:
func(1,2)
(1, 2)
In [106]:
func(1,2,3,4,5)
(1, 2, 3, 4, 5)
In [107]:
def genericsum(*args):
    return sum(list(args))
In [110]:
def genericsum(*args):
    s = 0
    for item in args:
        s += item
    return s
In [111]:
genericsum(1, 2)
Out[111]:
3
In [112]:
genericsum(1,2,4,5,6)
Out[112]:
18
In [113]:
def joinstrings(*args, sep=" "):
    total = args[0]
    for word in args[1:]:
        total = total + sep + word
    return total
In [114]:
joinstrings("Alanzo", "church", "thought", "about", "lambda", "calculas")
Out[114]:
'Alanzo church thought about lambda calculas'
In [116]:
def joinstrings(*args, sep=" "):
    return sep.join(args)
In [117]:
joinstrings("Alanzo", "church", "thought", "about", "lambda", "calculas")
Out[117]:
'Alanzo church thought about lambda calculas'
In [118]:
joinstrings("hello", "world", sep=",")
Out[118]:
'hello,world'
In [119]:
joinstrings("hello", "world",",")
Out[119]:
'hello world ,'

This is how you can pass variable number of named arguments

In [122]:
def make_parson(name, **kwargs):
    person = {"name": name}
    for key, value in kwargs.items():
        person[key] = value
    return person
    
In [124]:
p = make_parson(name="Haskell", surname="Curry", email="haskell@functional.expressions.com")
In [125]:
p['name']
Out[125]:
'Haskell'
In [126]:
p['email']
Out[126]:
'haskell@functional.expressions.com'
In [127]:
p
Out[127]:
{'email': 'haskell@functional.expressions.com',
 'name': 'Haskell',
 'surname': 'Curry'}
def f(a,v)
def f(a, v, *, name="x")
def f(*args)
def f(**kwargs)
In [130]:
genericsum(1, 2, 3, 4)
Out[130]:
10
In [131]:
def find_stats(*args):
    results = {'sum':genericsum(*args),
               'mean':genericsum(*args)/len(args),
               'max':max(args),
               'min':min(args)
        
    }
    return results
In [132]:
find_stats(1,2, 3,4, 5, 6, 7, 7, 8)
Out[132]:
{'max': 8, 'mean': 4.777777777777778, 'min': 1, 'sum': 43}
In [133]:
def timeseries(*values, **metadata):
    stats = find_stats(*values)
    ploting_data = {}
    ploting_data['value'] = values
    ploting_data['stats'] = stats
    
    for k, v in metadata.items():
        ploting_data[k] = v
        
    return ploting_data
In [134]:
timeseries(0, 0.01, 0.002, 0.0023, 0.0023,
           group = "cancer",
           observations = "raw intensity",
           experiment = "gene expression",
           gene = "RC311"
          )
Out[134]:
{'experiment': 'gene expression',
 'gene': 'RC311',
 'group': 'cancer',
 'observations': 'raw intensity',
 'stats': {'max': 0.01, 'mean': 0.00332, 'min': 0, 'sum': 0.0166},
 'value': (0, 0.01, 0.002, 0.0023, 0.0023)}

Functions as arguments and return values

Functions in python are ordinary in a sense that they are not different from any other data type. But this makes functions very extraordinary as compared to other langauges

In [135]:
def func(x):
    return x*x
In [136]:
func
Out[136]:
<function __main__.func>
In [137]:
a = zip([1, 2,3 ], [2, 3, 4])
In [138]:
a
Out[138]:
<zip at 0x7fb2c4002408>
In [139]:
type(a)
Out[139]:
zip
In [140]:
type(func)
Out[140]:
function
In [141]:
square = func
In [142]:
square
Out[142]:
<function __main__.func>
In [143]:
func(2)
Out[143]:
4
In [144]:
square(4)
Out[144]:
16
In [145]:
square(5), func(4)
Out[145]:
(25, 16)
In [148]:
%%file module.py

def square(x):
    return x*x

print(square(3))
Overwriting module.py
In [149]:
!python module.py
9
In [150]:
import module
9
In [151]:
%%file module1.py

def square(x):
    return x*x

print(square(3))
print(__name__)
Writing module1.py
In [152]:
!python module1.py
9
__main__
In [153]:
import module1
9
module1
In [161]:
%%file module2.py

def square(x):
    return x*x

def main():
    print(square(3))
    print(__name__)
    
if __name__ == "__main__": # __name__ is a special variable .. to identify if you are 
                           # running a main script or doing just import 
    main()
Overwriting module2.py
In [155]:
!python module2.py
9
__main__
In [156]:
import module2
In [157]:
_
Out[157]:
(25, 16)
In [158]:
3 * 4
Out[158]:
12
In [160]:
_ # this is special variable which stores results of last command executed
Out[160]:
12
In [162]:
func
Out[162]:
<function __main__.func>
In [163]:
square
Out[163]:
<function __main__.func>
In [164]:
square == func
Out[164]:
True
In [165]:
l = [1,2,3]
In [166]:
l2 = l
In [167]:
l is l2
Out[167]:
True
In [168]:
l3 = [1, 2, 3]
In [169]:
l is l3
Out[169]:
False
In [170]:
l == l3
Out[170]:
True
In [171]:
def fone():
    pass

def ftwo():
    pass
In [172]:
fone == ftwo
Out[172]:
False

just think about these functions

In [2]:
def sum_naturals(n):
    s = 0
    for num in range(1, n+1):
        s += num
    return s

def square(x):
    return x*x

def sum_squares(n):
    s = 0
    for num in range(1, n+1):
        s += square(num)
    return s

def cube(x):
    return x**3

def sum_cubes(n):
    s = 0
    for num in range(1, n+1):
        s += cube(num)
    return s

def sum_func(n, func):
    s = 0
    for num in range(1, n+1):
        s += func(num)
    return s
In [178]:
sum_squares(10)
Out[178]:
385
In [180]:
sum_func(10, square)
Out[180]:
385

lambda expression can be used to define annonymous functions on fly

In [181]:
f = lambda x: x*x
In [182]:
f(3)
Out[182]:
9

sum_func(10, lambda x: x*x)

problem : The series 8/(1*3), 8/(5*3), 8/(9*11) .... slowly converges to pi

In [3]:
sum_func(1000, lambda n: 8/((4*n-3)*(4*n-1)))
Out[3]:
3.141092653621038
In [184]:
def make_adder(x): 
    def adder(y):
        return x+y
    
    return adder
In [185]:
def f():
    pass
In [186]:
f
Out[186]:
<function __main__.f>
In [187]:
def f(x):
    return x+y
In [188]:
f(2)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-188-8ffcfab90d28> in <module>()
----> 1 f(2)

<ipython-input-187-9d3ce365bb87> in f(x)
      1 def f(x):
----> 2     return x+y

NameError: name 'y' is not defined
In [189]:
def make_adder(y):
    
    def adder(x):
        return x+y
    
    return adder
In [190]:
add5 = make_adder(5)
In [191]:
add5
Out[191]:
<function __main__.make_adder.<locals>.adder>
In [193]:
add5(4)
Out[193]:
9
In [194]:
add5(10)
Out[194]:
15
In [195]:
add3 = make_adder(3)
In [196]:
add3(14)
Out[196]:
17
In [197]:
def make_logger(prefix):
    def logger(*args):
        print(prefix, *args)
    
    return logger
In [198]:
info = make_logger("[INFO]: ")
In [199]:
warn = make_logger("[WARN]: ")
In [200]:
warn("Something went wrong")
[WARN]:  Something went wrong
In [201]:
info("Called some function")
[INFO]:  Called some function
In [202]:
record =[
    ("A", 40),
    ("B", 86),
    ("C", 48),
    ("D", 75)
]
In [203]:
max([2,3,4,5])
Out[203]:
5
In [204]:
max(record)
Out[204]:
('D', 75)
In [212]:
def get_marks(record):
    return record[1]
In [206]:
get_marks(record[0])
Out[206]:
40
In [207]:
max(record, key=get_marks)
Out[207]:
('B', 86)
In [209]:
max(["python", "java", "c++", "lisp", "smalltalk", "z++"])
Out[209]:
'z++'
In [210]:
max(["python", "java", "c++", "lisp", "smalltalk", "z++"], key=len)
Out[210]:
'smalltalk'
In [211]:
max(record, key=lambda r:r[1])
Out[211]:
('B', 86)
In [215]:
records = [
    ("A",90, 6.6),
    ("B", 100, 6.7),
    ("C", 110, 7.0),
    ("D", 95, 10.0)
]
# column0 -> name
# column1 -> systolic BP
# column2 -> blood sugar
In [216]:
def column(index):
    return lambda row:row[index]
In [217]:
max(records, key=column(1))
Out[217]:
('C', 110, 7.0)
In [218]:
max(records, key=column(2))
Out[218]:
('D', 95, 10.0)

problem:

  • Write a function compose which will take two functions f,g as arguments and return a function which will compute f(g(x))
>>> f = lambda x: x*x
>>> g = lamdda x: x-1
>>> fg = compose(f, g)
>>> fg(3)
4
>>> gf = compose(g, f)
>>> gf(3)
8
In [219]:
def compose(f, g):
    return lambda x: f(g(x))
In [221]:
f = lambda x: x*x
g = lambda x: x-1
In [222]:
fg = compose(f, g)
In [223]:
fg(3)
Out[223]:
4
In [224]:
gf = compose(g, f)
In [225]:
gf(3)
Out[225]:
8
In [226]:
words = "Lets print longest word in upper case from this statement".split()
In [227]:
words
Out[227]:
['Lets',
 'print',
 'longest',
 'word',
 'in',
 'upper',
 'case',
 'from',
 'this',
 'statement']
In [228]:
def longest_word(words):
    return max(words, key=len)
In [229]:
def uppercase(word):
    return word.upper()
In [231]:
longest_upper = compose(uppercase, longest_word)
In [232]:
longest_upper(words)
Out[232]:
'STATEMENT'
  • Problem: Write a function zip_with which will take function f as argument and return a function which zips two lists into single list by combining element by element with f

  • problem: Write a generic polynomial generator, which takes coefficients as aregument and returns a function which calculates polynomial function for given argument.

      P2 = make_polynomial(1,1,1) #  P(x) = x^2 + x + 1
      P2(2) #compute P(x) for for x = 2
      7
  • Bonus problem: A number x is called a fixed point of a function f if x satisfies the equation f(x) = x. For some functions f we can locate a fixed point by beginning with an initial guess and applying f repeatedly, f(x),f(f(x), f(f(f(x), .... Write a python function to find fixed point of a function. it should take function as argument, initial guess and accuracy (default = 0.0001) for equating f(x) = x
    fixed_point(math.cos, 1, 0.0001)
    0.7390547907469174
In [233]:
def zip_with(f):
    def func(list1, list2):
        return [f(x,y) for x,y in zip(list1, list2)]
    
    return func
In [234]:
for x,y in zip([1,2,3], ['a', 'b', 'c']):
    print(x,y)
1 a
2 b
3 c
In [239]:
def add(x, y):
    return x+y

vector_adder = zip_with(add)
In [240]:
vector_adder([1,2,3,4], [5,6,7,8])
Out[240]:
[6, 8, 10, 12]
In [241]:
list_multiplier = zip_with(lambda x, y: x*y)
In [242]:
list_multiplier([1,2,3,4], [5,6,7,8])
Out[242]:
[5, 12, 21, 32]
In [243]:
string_con = zip_with(add)
In [244]:
string_con(["Hello", "Goodbye"], ["python", "C++"])
Out[244]:
['Hellopython', 'GoodbyeC++']
In [251]:
def usual_way_polynomial(coeffs, x):
    """
    computes value of polynomial given coefficients and x
    >>> usual_way_polynomial([1,1,1], 2) # computes x^2 + x + 1
    7
    """
    s = 0
    for i, c in enumerate(reversed(coeffs)):
        s += c*x**i
    
    return s

def make_polynomial(*coeffs):
    """
    make a polynomial as function given coefficients
    >>> P2 = make_polynomial([1,1,1])
    >>> P2(2)
    7
    """
    def poly(x):
        s = 0
        for i, c in enumerate(reversed(coeffs)):
            s += c*x**i
    
        return s

    return poly
    #return lambda x: sum([c*x**i for c,i in enumerate(reversed(coeffs))])
    
    
In [252]:
usual_way_polynomial([1,1,1], 2)
Out[252]:
7
In [253]:
P2 = make_polynomial(1, 2, 1)
In [254]:
P2(3)
Out[254]:
16
In [ ]:
def fixed_point(f, guess, tollerance=0.00001):
    """
    computes fixed point of a function
    """
    prev = f(guess)
    current = f(prev)
    
    while abs(prev-current) >tollerance:
        prev, current = current, f(current)
        
    return current
In [5]:
import math
fixed_point(math.cos, 1)
Out[5]:
0.7390822985224024
In [6]:
math.cos(0.7390822985224024)
Out[6]:
0.7390870426953322

Testing

In [255]:
%%file sq1.py

def square(x):
    return x*x

def test():
    print(square(3))
    
if __name__ == "__main__":
    test()
Writing sq1.py
In [256]:
!python sq1.py
9
In [257]:
%%file sq2.py
def square(x):
    return x*x

def test():
    if square(3) == 9:
        print("Passed")

if __name__ == "__main__":
    test()
Writing sq2.py
In [258]:
!python sq2.py
Passed
In [263]:
%%file sq3.py

def square(x):
    return x*x

def test():
    assert square(3) == 9
    assert square(0) == 0
    assert square(-3) == 9
    assert square(-1) == 0

if __name__ == "__main__":
    test()
Overwriting sq3.py
In [264]:
!python sq3.py
Traceback (most recent call last):
  File "sq3.py", line 12, in <module>
    test()
  File "sq3.py", line 9, in test
    assert square(-1) == 0
AssertionError

py.test is a third party library which makes testing very easy. to install it

pip3 install pytest
In [265]:
!pytest sq3.py
============================= test session starts ==============================
platform linux -- Python 3.6.1, pytest-3.0.7, py-1.4.33, pluggy-0.4.0
rootdir: /home/vikrant/trainings/2017/vmware-pune-advpy, inifile:
collected 1 items 

sq3.py F

=================================== FAILURES ===================================
_____________________________________ test _____________________________________

    def test():
        assert square(3) == 9
        assert square(0) == 0
        assert square(-3) == 9
>       assert square(-1) == 0
E       assert 1 == 0
E        +  where 1 = square(-1)

sq3.py:9: AssertionError
=========================== 1 failed in 0.05 seconds ===========================
In [266]:
%%file weekday.py
import datetime

def now():
    return datetime.datetime.now()

def weekday():
    t = now()
    return t.strftime("%A")

if __name__ == "__main__":
    print(weekday())
Writing weekday.py
In [267]:
!python weekday.py
Wednesday
In [369]:
%%file test_weekday.py
import weekday
import datetime

def test_weekday(monkeypatch):
    faketime = 2010, 1, 1
    
    def fakenow():
        return datetime.datetime(*faketime)
    
    monkeypatch.setattr(weekday, "now", fakenow)
    
    faketime = 2010, 1, 1
    assert weekday.weekday() == "Friday"
    
    faketime = 2010, 1, 2
    assert weekday.weekday() == "Saturday"
    
Overwriting test_weekday.py
In [272]:
!pytest test_weekday.py
============================= test session starts ==============================
platform linux -- Python 3.6.1, pytest-3.0.7, py-1.4.33, pluggy-0.4.0
rootdir: /home/vikrant/trainings/2017/vmware-pune-advpy, inifile:
collected 1 items 

test_weekday.py .

=========================== 1 passed in 0.01 seconds ===========================
In [273]:
import datetime
In [274]:
datetime.datetime(2010, 1, 1)
Out[274]:
datetime.datetime(2010, 1, 1, 0, 0)
In [275]:
faketime = 2010, 1, 1
In [276]:
faketime
Out[276]:
(2010, 1, 1)
In [279]:
datetime.datetime(*faketime)
Out[279]:
datetime.datetime(2010, 1, 1, 0, 0)

Decorators

Whats the easiest and surest way of debuging?

In [281]:
def add(x, y):
    print("add ", x, y)
    return x+y
In [284]:
dir(add)
Out[284]:
['__annotations__',
 '__call__',
 '__class__',
 '__closure__',
 '__code__',
 '__defaults__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__get__',
 '__getattribute__',
 '__globals__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__kwdefaults__',
 '__le__',
 '__lt__',
 '__module__',
 '__name__',
 '__ne__',
 '__new__',
 '__qualname__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__']
In [294]:
def sub(x, y):
    return x-y

def mult(x, y):
    return x*y

def fib(n):
    if n in [1, 2]:
        return 1
    else:
        return fib(n-1) + fib(n-2)
    
In [305]:
def debug(f):
    def wrapper(*args):
        print(f.__qualname__, args)
        return f(*args)
    
    return wrapper
In [296]:
sub(3,4)
Out[296]:
-1
In [297]:
subnew = debug(sub)
In [298]:
subnew(3,4)
sub (3, 4)
Out[298]:
-1
In [299]:
sub = debug(sub)
In [300]:
sub(4,5)
sub (4, 5)
Out[300]:
-1
In [301]:
fib = debug(fib)
In [302]:
fib(5)
fib (5,)
fib (4,)
fib (3,)
fib (2,)
fib (1,)
fib (2,)
fib (3,)
fib (2,)
fib (1,)
Out[302]:
5

There is syntactic sugar for this!

In [304]:
@debug
def fib(n):
    if n in [1, 2]:
        return 1
    else:
        return fib(n-1)+ fib(n-2)

Whats the achivement?

  • Debugging code is at one place
  • It becomes easy to change later
  • User of decorator need not worry about the changes

Lets do some advanced debuging

In [309]:
%%file trace.py
import os

level = 0

def log(*args):
    if os.getenv("DEBUG") == "true":
        print(*args)
    
def trace(f):
    
    def g(*args):
        global level
        log("| "*level + "|-- " +f.__qualname__, args)
        level += 1
        value = f(*args)
        level -= 1
        log("| "*level + "|-- " + "return", value)
        return value
    
    return g
        
Overwriting trace.py
In [310]:
%%file sum.py
from trace import trace

@trace
def square(x):
    return x*x

#square = trace(square)
@trace
def sum_of_squares(x, y):
    return square(x) + square(y)

if __name__ == "__main__":
    sum_of_squares(3, 4)
Overwriting sum.py
In [312]:
!DEBUG=true python sum.py
|-- sum_of_squares (3, 4)
| |-- square (3,)
| |-- return 9
| |-- square (4,)
| |-- return 16
|-- return 25
In [313]:
!python sum.py
In [314]:
%%file fib.py
import sys
from trace import trace

@trace
def fib(n):
    if n in [1, 2]:
        return 1
    else:
        return fib(n-1) + fib(n-2)
    
def main():
    n = int(sys.argv[1])
    fib(n)
    
if __name__ == "__main__":
    main()
Writing fib.py
In [319]:
!DEBUG=true python fib.py 6
|-- fib (6,)
| |-- fib (5,)
| | |-- fib (4,)
| | | |-- fib (3,)
| | | | |-- fib (2,)
| | | | |-- return 1
| | | | |-- fib (1,)
| | | | |-- return 1
| | | |-- return 2
| | | |-- fib (2,)
| | | |-- return 1
| | |-- return 3
| | |-- fib (3,)
| | | |-- fib (2,)
| | | |-- return 1
| | | |-- fib (1,)
| | | |-- return 1
| | |-- return 2
| |-- return 5
| |-- fib (4,)
| | |-- fib (3,)
| | | |-- fib (2,)
| | | |-- return 1
| | | |-- fib (1,)
| | | |-- return 1
| | |-- return 2
| | |-- fib (2,)
| | |-- return 1
| |-- return 3
|-- return 8

Problem : Write a function depricated that prints a warning message thet the function is depricated everytime it is called

@depricated
def square(x):
    return x*x

print(square(4)
WARNING: function square is depricated
16

Problem: Write a decorator function with_retries that continues to retry 5 times if there is any exception raised in original function.

from urllib.request import urlopen
@with_retries
def wget(url):
    return urlopen(url).read()

wget("http://google.com/nourl")
Failed to download, retrying..
Failed to download, retrying..
Failed to download, retrying..
Failed to download, retrying..
Failed to download, retrying..
Giving up!
In [320]:
def depricated(f):
    
    def wrapper(*args):
        print("WARNING: function "+ f.__qualname__ + "is depricated.")
        return f(*args)
    
    return wrapper

@depricated
def square(x):
    return x*x
In [321]:
square(4)
WARNING: function squareis depricated.
Out[321]:
16
In [322]:
def f():
    print("f")
In [323]:
x = f()
f
In [324]:
x
In [325]:
print(x)
None
In [335]:
def with_retries(f):
    
    def wrapper(*args):
        for i in range(5):
            try:
                return f(*args)
            except Exception as e:
                print(e, "Retrying")
        print("Giving up!")
        
    return wrapper

from urllib.request import urlopen

@with_retries
def wget(url):
    return urlopen(url).read()
In [331]:
wget("http://lksdfdsf.sdfsfs.in")
<urlopen error [Errno -2] Name or service not known> retrying
<urlopen error [Errno -2] Name or service not known> retrying
<urlopen error [Errno -2] Name or service not known> retrying
<urlopen error [Errno -2] Name or service not known> retrying
<urlopen error [Errno -2] Name or service not known> retrying
Giving up!

Lets look closely at fib!

In [336]:
!DEBUG=true python fib.py 5
|-- fib (5,)
| |-- fib (4,)
| | |-- fib (3,)
| | | |-- fib (2,)
| | | |-- return 1
| | | |-- fib (1,)
| | | |-- return 1
| | |-- return 2
| | |-- fib (2,)
| | |-- return 1
| |-- return 3
| |-- fib (3,)
| | |-- fib (2,)
| | |-- return 1
| | |-- fib (1,)
| | |-- return 1
| |-- return 2
|-- return 5

Can we improve this?

  • If we can cache the the values if function is called with some specific argument.
  • You can write fin in iterative fashion than recursive!
In [345]:
%%file memoize.py

def memoize(func):
    cache = {}
    
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    
    return wrapper
Overwriting memoize.py
In [351]:
import imp
In [352]:
imp.reload(memoize)
Out[352]:
<module 'memoize' from '/home/vikrant/trainings/2017/vmware-pune-advpy/memoize.py'>
In [353]:
from memoize import memoize
In [355]:
@memoize
def square(x):
    print("square", x)
    return x*x
In [356]:
square(2)
square 2
Out[356]:
4
In [357]:
square(4)
square 4
Out[357]:
16
In [358]:
square(2)
Out[358]:
4
In [359]:
%%file fib1.py
import sys
from trace import trace
from memoize import memoize

@memoize
def fib(n):
    if n in [1, 2]:
        return 1
    else:
        return fib(n-1) + fib(n-2)
    
def main():
    n = int(sys.argv[1])
    fib(n)
    
if __name__ == "__main__":
    main()
Writing fib1.py
In [360]:
!time -p python fib.py 30
real 13.16
user 13.15
sys 0.00
In [361]:
!time -p python fib1.py 30
real 0.02
user 0.02
sys 0.00
In [362]:
#f = decorator2(decorator1(f))
In [364]:
%%file fib2.py
import sys
from memoize import memoize
from trace import trace


@memoize           # fib = memoize(trace(fib))  ^
@trace             # fib = trace(fib)           |
def fib(n):        #                            |
    if n in [1, 2]:
        return 1
    else:
        return fib(n-1) + fib(n-2)
    
def main():
    n = int(sys.argv[1])
    fib(n)
    
if __name__ == "__main__":
    main()
Overwriting fib2.py
In [368]:
!DEBUG=true python fib2.py 10 # with memoize
|-- fib (10,)
| |-- fib (9,)
| | |-- fib (8,)
| | | |-- fib (7,)
| | | | |-- fib (6,)
| | | | | |-- fib (5,)
| | | | | | |-- fib (4,)
| | | | | | | |-- fib (3,)
| | | | | | | | |-- fib (2,)
| | | | | | | | |-- return 1
| | | | | | | | |-- fib (1,)
| | | | | | | | |-- return 1
| | | | | | | |-- return 2
| | | | | | |-- return 3
| | | | | |-- return 5
| | | | |-- return 8
| | | |-- return 13
| | |-- return 21
| |-- return 34
|-- return 55
In [367]:
!DEBUG=true python fib.py 5
|-- fib (5,)
| |-- fib (4,)
| | |-- fib (3,)
| | | |-- fib (2,)
| | | |-- return 1
| | | |-- fib (1,)
| | | |-- return 1
| | |-- return 2
| | |-- fib (2,)
| | |-- return 1
| |-- return 3
| |-- fib (3,)
| | |-- fib (2,)
| | |-- return 1
| | |-- fib (1,)
| | |-- return 1
| |-- return 2
|-- return 5
In [ ]: