Python Virtual Training For Arcesium - Module I - Day 3

Aug 17-21, 2020 Vikrant Patil

These notes are available online at http://notes.pipal.in/2020/arcesium_finop_batch2/module1-day3.html

© Pipal Academy LLP

Day 1 | Day 2 | Day 3 | Day 4 | Day 5

We will be using jupyter hub from http://lab2.pipal.in for this training. Use notebook with name module1-day3.ipynb for today's session.

Custom functions

In [1]:
def add(2, 3): # incorrect
    return 2+3
  File "<ipython-input-1-10e6cb680a4d>", line 1
    def add(2, 3): # incorrect
            ^
SyntaxError: invalid syntax

One can not have litterals in function definition

In [2]:
def add("a", "b"): # this string litteral , not allowd
    return a + b
  File "<ipython-input-2-093c18cda1b6>", line 1
    def add("a", "b"): # this string litteral , not allowd
            ^
SyntaxError: invalid syntax
In [3]:
def add("a", "b"): # this string litteral , not allowd
    return "a" + "b"
  File "<ipython-input-3-55bb57e01fb5>", line 1
    def add("a", "b"): # this string litteral , not allowd
            ^
SyntaxError: invalid syntax
In [4]:
def add(a, b):
    return a+b
In [5]:
def say_hello(name):
    print("Hello", name + "!")
In [6]:
None
In [7]:
x = add(2 , 3) ## I am using litterals while calling
In [8]:
x
Out[8]:
5
In [9]:
y = say_hello("python")
Hello python!
In [10]:
print(y)
None

Problems

  1. Net asset value or NAV is equal to fund's or comapny's total assets less its' liabilities. NAV is usually calculated per share value for MF, ETF or closed ended funds. Write a function NAV to compute NAV. Using this function compute NAV for tatl assets of 25,00,00,000 and liabilities 30,00,000 for 1000 shares.
  2. In financial terms a negative balance is represented with round brackets around the number instead of -ve sign. Write a function numeric_value which returns actual numeric value. FOr example a value (1234) should get -1234 as numeric values while "1234.5" will still get value as 1234.5
     >>> numeric_value("(123)")
     -123.0
     >>> numeric_value(123)
     123.0
  3. Have a look at this code, what will it print?

     def twice(x):
         print(2*x)
    
     print(twice(twice(3)))
In [12]:
def NAV(assets, liabilities, shares):
    return (assets-liabilities)/shares
In [13]:
assets
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-13-a10592701296> in <module>
----> 1 assets

NameError: name 'assets' is not defined
In [14]:
liabilities
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-14-976c251e93c3> in <module>
----> 1 liabilities

NameError: name 'liabilities' is not defined
In [15]:
NAV(250000000, 3000000, 1000)
Out[15]:
247000.0
In [16]:
def foo(x, y, z):
    s = x + y + z
    m = x*y*z
    
return s+m
  File "<ipython-input-16-936f54518490>", line 5
    return s+m
    ^
SyntaxError: 'return' outside function
In [17]:
def foo(x, y, z):
    s = x + y + z
    m = x*y*z
    
    return s+m
In [18]:
def foo(x, y, z):
    s = x + y + z
    m = x*y*z
    
    return s+m

foo(1, 2, 3)
Out[18]:
12
In [20]:
def foo(x, y, z):
    s = x + y + z
    m = x*y*z
    
    return s+m

    foo(1, 2, 3) # this statement has become part of function foo! because of indentation
In [21]:
int("(123)")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-21-a22709813951> in <module>
----> 1 int("(123)")

ValueError: invalid literal for int() with base 10: '(123)'
In [22]:
float("(123)")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-22-027431e8f067> in <module>
----> 1 float("(123)")

ValueError: could not convert string to float: '(123)'
In [23]:
"a string with - in it".replace("-", "_")
Out[23]:
'a string with _ in it'
In [24]:
"a string without hyphen".replace("-", "_")
Out[24]:
'a string without hyphen'
In [25]:
def numeric_value(formattednum):
    strnum = formattednum.replace("(", "-").replace(")","")
    return float(strnum)
In [26]:
numeric_value("(12232)")
Out[26]:
-12232.0
In [27]:
numeric_value("22324")
Out[27]:
22324.0
In [28]:
def twice(x):
    print(2*x)
In [29]:
twice(5)
10
In [30]:
twice(4)
8
In [31]:
print(twice(twice(3)))
6
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-31-908cb4a6e125> in <module>
----> 1 print(twice(twice(3)))

<ipython-input-28-8db4314a5c52> in twice(x)
      1 def twice(x):
----> 2     print(2*x)

TypeError: unsupported operand type(s) for *: 'int' and 'NoneType'
print(twice(twice(3)))
6
print(twice(None))
In [32]:
def twice(x):
    return 2*x
In [33]:
twice(twice(3))
Out[33]:
12
In [34]:
twice(twice(twice(6)))
Out[34]:
48

Styleguide for writing functions

A mathematical function which prints the result but does not return a value, is not usable.

  • A reusable function is perfect black box. it works only on given inputs.
  • A reusable function returns the computed value.
  • A reusable function does not make use of global variables!
  • A reuable function takes all that is required as argument.
In [35]:
n = 5
In [36]:
n
Out[36]:
5
In [37]:
def power_(x):
    return x**n ## not a good practice to use gloabl variables!
In [38]:
def power_(x, n):
    return x**n

**Additional guidelines

  • Give meaningful names to functions.
  • Give meaningful names to variables
  • Wrire code which is readable by humans first and then by computer!

Pitfalls

In [39]:
x = 10

def foo():
    print(x)

foo()
10
In [40]:
x = 10

def foo(y):
    y = 20
    
foo(x)
print(x)
10
In [41]:
x = 10

def foo():
    x = 20

foo()
print(x)
10
In [44]:
x = 10

def foo():
    x = x + 1
    
foo()
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-44-83c7eaeba6b9> in <module>
      4     x = x + 1
      5 
----> 6 foo()

<ipython-input-44-83c7eaeba6b9> in foo()
      2 
      3 def foo():
----> 4     x = x + 1
      5 
      6 foo()

UnboundLocalError: local variable 'x' referenced before assignment
In [43]:
x = 10

def foo():
    y = x + 1
    
foo()
    
In [45]:
x = 10

def foo(): # rhis definition
    x = x + 1
    
#foo() # this calling the function fooP
In [46]:
x = 10

def foo(): # rhis definition
    x = x + 1
    
foo() # this calling the function foo
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-46-a23883dfa483> in <module>
      4     x = x + 1
      5 
----> 6 foo() # this calling the function foo

<ipython-input-46-a23883dfa483> in foo()
      2 
      3 def foo(): # rhis definition
----> 4     x = x + 1
      5 
      6 foo() # this calling the function foo

UnboundLocalError: local variable 'x' referenced before assignment
In [47]:
[1, 1, 1] + [2]
Out[47]:
[1, 1, 1, 2]
In [48]:
x = [1, 1, 1]

def appendzero(y): # <----------here x and y are pointing to same list
    y = y + [0] #  we changed reference of y to somthing new
    
appendzero(x)
print(x)
[1, 1, 1]
statement        name      memory
x = [1, 1, 1]    x
def appendz(y)    \
    y = y + [0]    \ 
appendz(x)          \
                 appendz->code
                      \
                       \ [1, 1, 1]


statement           name      memory
                     y
                      \
y = y + [0]            \      
                        \
                         \
                             [1, 1, 1, 0]
In [50]:
x = [1, 1, 1]

def appendzero(y):
    y.append(0)
    
appendzero(x)
print(x)
[1, 1, 1, 0]
In [51]:
x = [1, 2, 3]
y = x
y = [2,3, 3]
In [ ]:
 

Function arguments

In [52]:
def compound_interest(P, r, n, t):
    return P*(1+r/n)**(n*t)
In [53]:
compound_interest(25000, 0.04, 4, 5)
Out[53]:
30504.750998699175
In [54]:
compound_interest(0.04, 25000, 4, 5)
Out[54]:
3.3193264711391796e+74

Python has a simple solution for it , called as named argument

In [66]:
def compound_interest(principle, rate, comp_freq, years):
    return principle*(1+rate/comp_freq)**(comp_freq*years)
In [56]:
compound_interest(rate=0.04, comp_freq=4, years=5, principle=25000)
Out[56]:
30504.750998699175
In [57]:
compound_interest(rate=0.04, principle= 25000, comp_freq=4, years=5)
Out[57]:
30504.750998699175
In [58]:
compound_interest(25000, comp_freq=4, years=5, rate=0.04)
Out[58]:
30504.750998699175

Default argument

In [67]:
def compound_interest(principle, years, rate=0.04, comp_freq=4):
    return principle*(1+rate/comp_freq)**(comp_freq*years)
In [68]:
compound_interest(25000, 5)
Out[68]:
30504.750998699175
In [69]:
compound_interest(25000, 5, rate=0.07)
Out[69]:
35369.454893894996

global statement

In [1]:
x = 10

def foo():
    global x
    x = 20
    
foo()
print(x)
20
In [2]:
x = [1, 1, 1]

def appendzero(y):
    y.append(0) ## here we are able to modify global y because it has got a method in it to modify itself.
    
appendzero(x)  
In [3]:
x = [1, 1, 1]

def appendzero():
    global x
    x = x + [0]
    
appendzero()
print(x)
[1, 1, 1, 0]

Passing functions as arguments

In [4]:
x
Out[4]:
[1, 1, 1, 0]
In [5]:
del x
In [6]:
x
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-6-6fcf9dfbd479> in <module>
----> 1 x

NameError: name 'x' is not defined
In [7]:
del foo
In [8]:
foo
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-8-f1d2d2f924e9> in <module>
----> 1 foo

NameError: name 'foo' is not defined
In [9]:
def foo():
    print("Hello foobar!")
In [12]:
foo # a variable with name as function name is created
Out[12]:
<function __main__.foo()>
In [13]:
foo()
Hello foobar!
In [14]:
x = 10
y = x
In [21]:
bar = foo # aliasing
In [16]:
bar
Out[16]:
<function __main__.foo()>
In [17]:
foo
Out[17]:
<function __main__.foo()>
In [18]:
foo()
Hello foobar!
In [19]:
bar()
Hello foobar!
In [24]:
def square(x):
    return x*x

def sumofsquares(x, y):
    return square(x) + square(y)


def cube(x):
    return x**3

def sumofcubes(x, y):
    return cube(x) + cube(y)
In [25]:
def sumof(x, y, func):
    return func(x) + func(y)
In [27]:
sumof(5, 6, square)
Out[27]:
61
In [28]:
sumof(5, 6, cube)
Out[28]:
341
In [29]:
sumofsquares(5, 6)
Out[29]:
61
In [30]:
sumofcubes(5, 6)
Out[30]:
341
In [31]:
help(max)
Help on built-in function max in module builtins:

max(...)
    max(iterable, *[, default=obj, key=func]) -> value
    max(arg1, arg2, *args, *[, key=func]) -> value
    
    With a single iterable argument, return its biggest item. The
    default keyword-only argument specifies an object to return if
    the provided iterable is empty.
    With two or more arguments, return the largest argument.

In [36]:
words = ["one", "twelve", "three", "four", "sixty five"]
In [37]:
max(words) ##ASCII order
Out[37]:
'twelve'
In [38]:
max(words, key=len)
Out[38]:
'sixty five'
In [39]:
words = ["Z", "YY", "XXX", "MMMM", "AAAAAAA"]
In [40]:
max(words)
Out[40]:
'Z'
In [45]:
max(words, key=len) # a word with max length
Out[45]:
'AAAAAAA'
In [42]:
min(words)
Out[42]:
'AAAAAAA'
In [44]:
min(words, key=len) # a word with minimu length
Out[44]:
'Z'
In [46]:
records = [
    ("rupali", 21, 9.5),
    ("rupa",   20, 9.8),
    ("alice",  16, 9.6),
    ("elsa",   23, 9.2)
]
In [48]:
max(records) # by name ASCII order
Out[48]:
('rupali', 21, 9.5)
In [49]:
def get_age(row):
    return row[1]

def get_score(row):
    return row[2]
In [50]:
row = ("rupali", 21, 9.5)
In [51]:
row[0] # name
Out[51]:
'rupali'
In [52]:
row[1] # age
Out[52]:
21
In [55]:
row[2] #score
Out[55]:
9.5
In [56]:
max(records, key=get_age)
Out[56]:
('elsa', 23, 9.2)
In [57]:
max(records, key=get_score)
Out[57]:
('rupa', 20, 9.8)
In [58]:
min(records, key=get_age)
Out[58]:
('alice', 16, 9.6)
In [59]:
sorted(records, key=get_age)
Out[59]:
[('alice', 16, 9.6), ('rupa', 20, 9.8), ('rupali', 21, 9.5), ('elsa', 23, 9.2)]
In [60]:
sorted(records, key=get_score)
Out[60]:
[('elsa', 23, 9.2), ('rupali', 21, 9.5), ('alice', 16, 9.6), ('rupa', 20, 9.8)]
In [61]:
sorted(records, key=get_age, reverse=True)
Out[61]:
[('elsa', 23, 9.2), ('rupali', 21, 9.5), ('rupa', 20, 9.8), ('alice', 16, 9.6)]
In [62]:
sorted(records, key=get_age)
Out[62]:
[('alice', 16, 9.6), ('rupa', 20, 9.8), ('rupali', 21, 9.5), ('elsa', 23, 9.2)]

Functions returning functions

In [63]:
def make_adder(x):
    
    def adder(y):
        return x+y
    
    return adder
In [64]:
adder5 = make_adder(5)
In [65]:
adder5
Out[65]:
<function __main__.make_adder.<locals>.adder(y)>
In [66]:
adder5(7)
Out[66]:
12
In [67]:
adder5(11)
Out[67]:
16
In [68]:
adder5(15)
Out[68]:
20
In [69]:
adder9 = make_adder(9)
In [70]:
adder9
Out[70]:
<function __main__.make_adder.<locals>.adder(y)>
In [71]:
adder9(5)
Out[71]:
14

lambda expression

In [72]:
def get_age(r):
    return r[1]
In [73]:
get_age = lambda r : r[1]
In [74]:
max(records, key=lambda r:r[1])
Out[74]:
('elsa', 23, 9.2)
In [77]:
def get_age(r):
    print("*"*5, r)
    return r[1]
In [78]:
max(records, key=get_age)
***** ('rupali', 21, 9.5)
***** ('rupa', 20, 9.8)
***** ('alice', 16, 9.6)
***** ('elsa', 23, 9.2)
Out[78]:
('elsa', 23, 9.2)
In [ ]: