Python Virtual Training For Arcesium - Module I - Day 3

Aug 10-14, 2020 Vikrant Patil

These notes are available online at http://notes.pipal.in/2020/arcesium_finop_batch1_module1/day3.html

© Pipal Academy LLP

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

We will be using jupyter hub from http://lab1.pipal.in for this training.

make use of notebook module1-day3.ipynb for today's session.

problems

  1. Net asset value , NAV is equal to fund's or company's total asset less it's liabilities. NAV is usually calculated per share value for MF, ETF or closed ended fund. Write a function NAV. Compute NAV for total assets of 25,00,00,000 , liabilities 30,00,000 and 1000 shares.
  2. In financial terms a -ve number is represented py round brackets around the number instaed of -ve sign. Write a function numeric_value which returns actual numeric value. For example a value "(1234)" should return -1234 and "1234" should return 1234.
  3. Have a look at following python code what will it print? can you correct it?

     def twice(x)
         print(2*x)
    
     print(twice(twice(3))

Solution 1

In [1]:
def NAV(total_assets, liabilities, sharecount):
    return (total_assets - liabilities)/sharecount
In [2]:
NAV(250000000, 3000000, 1000)
Out[2]:
247000.0
In [3]:
def NAV(x, y, z): ## functionality wise this will work.
    return (x-y)/z 

Solution 2

In [4]:
def numeric_value(strnum):
    numeric_str = strnum.replace("(", "-").replace(")","")
    return float(numeric_str)
In [5]:
numeric_value("(1212)")
Out[5]:
-1212.0
In [6]:
numeric_value("3433")
Out[6]:
3433.0

Solution 3

In [7]:
def twice(x):
    print(2*x)
In [8]:
twice(5)
10
In [9]:
twice(twice(5))
10
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-9-bb3a0f0c8472> in <module>
----> 1 twice(twice(5))

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

TypeError: unsupported operand type(s) for *: 'int' and 'NoneType'
In [10]:
x = twice(5)
10
In [11]:
print(x)
None
In [12]:
twice(None)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-12-011f23172eca> in <module>
----> 1 twice(None)

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

TypeError: unsupported operand type(s) for *: 'int' and 'NoneType'
In [13]:
def twice(x):
    return 2*x
In [14]:
twice(twice(4))
Out[14]:
16

Some guidelines to remember while writing functions

  • A reusable function is perfect black box. it works only on given inputs
  • A reusable function return the computed value
  • A reusable function doesn't make use of global variables.
  • A reusable function takes all that is required as function argument.

Style guidlines

  • Give meaningful names to your function
  • Give meaningful names to your variables
  • Write code to be read by humans first and then by computer.
In [17]:
x = 15
y = 13

def sumit():
    print(x+y)
In [18]:
x = 15
y = 13

def sum_it():
    print(x+y)
In [19]:
x = 15
y = 13

def sum_it(x, y):
    print(x+y)
In [20]:
x = 15
y = 13

def sum_it(x, y):
    return x+y
In [21]:
sum_it(2, 4)
Out[21]:
6
In [22]:
sum_it(5, 6)
Out[22]:
11
A black box with no input!

        +---------+
        |         |----->
        +---------+
In [24]:
help(input)
Help on method raw_input in module ipykernel.kernelbase:

raw_input(prompt='') method of ipykernel.ipkernel.IPythonKernel instance
    Forward raw_input to frontends
    
    Raises
    ------
    StdinNotImplentedError if active frontend doesn't support stdin.

In [25]:
sum([1, 2, 3, 45])
Out[25]:
51
In [26]:
sum(['a',"b", "d"])
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-26-2891fc73d694> in <module>
----> 1 sum(['a',"b", "d"])

TypeError: unsupported operand type(s) for +: 'int' and 'str'
In [28]:
numeric_value(34343) ## the function expects a string not an integer
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-28-fbbe0727a543> in <module>
----> 1 numeric_value(34343) ## the function expects a string not an integer

<ipython-input-4-3816bd2da295> in numeric_value(strnum)
      1 def numeric_value(strnum):
----> 2     numeric_str = strnum.replace("(", "-").replace(")","")
      3     return float(numeric_str)

AttributeError: 'int' object has no attribute 'replace'

Function Arguments

In [29]:
def say_hello(name, greeting):
    print(greeting, name + "!")
In [30]:
say_hello("vikrant", "hello")
hello vikrant!
In [31]:
say_hello("hello", "vikrant")
vikrant hello!
In [32]:
def compound_interest(P, r, n, t):
    return P*(1+r/n)**(n*t)
In [33]:
compound_interest(25000, 0.07, 4, 5)
Out[33]:
35369.454893894996
In [34]:
compound_interest(0.07, 25000, 4, 5)
Out[34]:
5.808821324493564e+74

If by mistake we give wrong order of paramenters, we might get in trouble for geting an answer which is wrong. How do we handle it! Python has a solution for it which is called as named arguments

In [35]:
compound_interest(r = 0.07, P = 25000, n = 4, t =5)
Out[35]:
35369.454893894996
In [36]:
compound_interest(P=25000, n = 4, r= 0.07, t = 5)
Out[36]:
35369.454893894996
In [37]:
compound_interest(25000, r = 0.07, t = 5, n=4)
Out[37]:
35369.454893894996
In [38]:
say_hello(greeting="Namaskar", name="Vikrant")
Namaskar Vikrant!

Default argument

In [39]:
def compound_interest(P, n, t, r=0.05): # default argument allows us to give default value for the argument
    return P*(1+r/n)**(n*t)
In [41]:
compound_interest(5000, 4, 5) # r is taken as 0.05 ...default value
Out[41]:
6410.186158542922
In [42]:
sum([0, 0, 0, 0, 0], start=5)
Out[42]:
5
In [43]:
sum([0, 0, 0, 0, 0])
Out[43]:
0

Namespace / scope and Functions

In [44]:
!python3 --version
Python 3.8.3
In [46]:
compound_interest
Out[46]:
<function __main__.compound_interest(P, n, t, r=0.05)>
In [53]:
 
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-53-3a710d2a84f8> in <module>
----> 1 z

NameError: name 'z' is not defined
In [50]:
def foo(x):
    x = x**2 + x + 1
    y = 2*x
    z = x+y
    return x, y, z # one can return multiple values from a function
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-50-34dc136ccff3> in <module>
----> 1 del x, y, z
      2 def foo(x):
      3     x = x**2 + x + 1
      4     y = 2*x
      5     z = x+y

NameError: name 'z' is not defined
In [54]:
a, b, c = foo(10)
In [55]:
x, y, z
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-55-e565797680e7> in <module>
----> 1 x, y, z

NameError: name 'x' is not defined

Variables defined at top level of program where main script starts are called as global variables. At this level globals and locals are same. Onece we call a function, as soon as function executaion starts , all variables cretaeted there are local to that function. Variables passed as argument to function are passed by reference. It means they refer same object, which is passed to function. Only diffence is they are called with diffrent name inside the function. As soon as the function call is over the namespace created for storing names of locals in function is deleted.

In [56]:
x
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-56-6fcf9dfbd479> in <module>
----> 1 x

NameError: name 'x' is not defined
In [57]:
def foo():
    x = 10
    print(x)
    
In [58]:
foo()
10
In [59]:
print(x)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-59-fc17d851ef81> in <module>
----> 1 print(x)

NameError: name 'x' is not defined
In [60]:
a = 10

def foo():
    print(a)
    
In [61]:
foo()
10
In [64]:
x = 5

def foo():
    y = x # we are trying to read from x
    x = 10 #unless you declare iniatially x as global you can not do reading and writing same time
    print(x, y)
foo()    
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-64-548f99fd80c2> in <module>
      5     x = 10 #unless you declare iniatially x as global you can not do reading and writing same time
      6     print(x, y)
----> 7 foo()

<ipython-input-64-548f99fd80c2> in foo()
      2 
      3 def foo():
----> 4     y = x # we are trying to read from x
      5     x = 10 #unless you declare iniatially x as global you can not do reading and writing same time
      6     print(x, y)

UnboundLocalError: local variable 'x' referenced before assignment
In [63]:
x = 5

def foo():
    global x
    x = 10

foo()
print(x)
10
In [65]:
x = 5

def foo():
    global x
    y = x
    x = 10
    print(x, y)

foo()
10 5

Problems

  1. what will this print?
    x = 10
    def foo():
        x = 20

    foo()
    print(x)
  1. ``` x = 10

    def foo():

     print(x)
    
    

    foo()


3.
x = 10

def foo():
    x = x+1

foo()
print(x)


4.
x = [1, 1, 1]

def appendzero(y):
    y = y + [0]

appendzero(x)
print(x)


5.
x = [1, 1, 1]

def appendzero(y):
    y.append(0)

appendzero(x)
print(x)

```

In [66]:
x = 10

def foo():
    x = x+1 ##<--- trying to read and write global

foo()
print(x)
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-66-4de87c253ec2> in <module>
      4     x = x+1 ##<--- trying to read and write global
      5 
----> 6 foo()
      7 print(x)

<ipython-input-66-4de87c253ec2> in foo()
      2 
      3 def foo():
----> 4     x = x+1 ##<--- trying to read and write global
      5 
      6 foo()

UnboundLocalError: local variable 'x' referenced before assignment
In [67]:
x = [1, 1, 1]

def appendzero(y):
    y = y + [0]

appendzero(x)
print(x)
[1, 1, 1]

Passing functions as arguments

Functions in python are just similar to any other data!

In [68]:
x = 1
In [69]:
y = x
In [70]:
print(x, y)
1 1
In [71]:
def foo():
    print("foobar")
In [72]:
foo
Out[72]:
<function __main__.foo()>
In [73]:
print(foo)
<function foo at 0x7f65f425fc10>
In [74]:
bar = foo
In [75]:
bar
Out[75]:
<function __main__.foo()>
In [76]:
foo()
foobar
In [77]:
bar()
foobar
In [78]:
def square(x):
    return x*x

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

def cube(x):
    return x*x*x

def sumofcubes(x, y):
    return cube(x) + cube(y)
In [79]:
def sumof(x, y, func):
    return func(x) + func(y)
In [80]:
sumof(3, 5, square)
Out[80]:
34
In [81]:
sumof(3, 5, cube)
Out[81]:
152

This idea of passing functions as arguments is very usefull and many python builtins make use of this idea. For example

In [82]:
words = ["ones", "two", "three", "four", "five", "six"]
In [84]:
max(words) ## gives max by ASCII order
Out[84]:
'two'
In [85]:
max(words, key=len)
Out[85]:
'three'
In [87]:
sorted(words) # by ASCII order
Out[87]:
['five', 'four', 'ones', 'six', 'three', 'two']
In [88]:
sorted(words, key=len)
Out[88]:
['two', 'six', 'ones', 'four', 'five', 'three']
In [89]:
min(words, key=len)
Out[89]:
'two'
In [90]:
max([1, 1, 333, 5, 333])
Out[90]:
333

Suppose we have dabase with following records. Record has name, value, gain.

In [99]:
records = [
    ("TATA", 200.0, 5.5),
    ("INFY", 2000.0, -5),
    ("RELIANCE", 1500, 50.0),
    ("HCL", 1200, 70.5)
]
In [93]:
max(records) # max by name, ASCII order
Out[93]:
('TATA', 200.0, 5.5)
In [94]:
def get_name(r):
    return r[0]

def get_value(r):
    return r[1]

def get_gain(r):
    return r[2]
In [96]:
max(records, key=get_gain) # arecord with max gain
Out[96]:
('HCL', 1200, 70.5)
In [98]:
max(records, key=get_value) # a record with max value
Out[98]:
('INFY', 2000.0, -5)
In [101]:
records[0]
Out[101]:
('TATA', 200.0, 5.5)
In [102]:
records[-1]
Out[102]:
('HCL', 1200, 70.5)
In [103]:
records[1:3]
Out[103]:
[('INFY', 2000.0, -5), ('RELIANCE', 1500, 50.0)]
In [104]:
records
Out[104]:
[('TATA', 200.0, 5.5),
 ('INFY', 2000.0, -5),
 ('RELIANCE', 1500, 50.0),
 ('HCL', 1200, 70.5)]
In [107]:
nums = [1, 2, 3, 33, 44, 55, 66, 6666, 777]

def digits(n):
    print("Computing digits for ", n)
    return len(str(n))
In [108]:
digits(5555)
Computing digits for  5555
Out[108]:
4
In [109]:
max(nums, key=digits)
Computing digits for  1
Computing digits for  2
Computing digits for  3
Computing digits for  33
Computing digits for  44
Computing digits for  55
Computing digits for  66
Computing digits for  6666
Computing digits for  777
Out[109]:
6666
In [110]:
records[0]
Out[110]:
('TATA', 200.0, 5.5)
In [111]:
records[1]
Out[111]:
('INFY', 2000.0, -5)
In [112]:
records[2]
Out[112]:
('RELIANCE', 1500, 50.0)
In [113]:
records[3]
Out[113]:
('HCL', 1200, 70.5)
In [114]:
def get_gain(r):
    print("Cmputing gain for", r)
    return r[2]
In [115]:
max(records, key=get_gain)
Cmputing gain for ('TATA', 200.0, 5.5)
Cmputing gain for ('INFY', 2000.0, -5)
Cmputing gain for ('RELIANCE', 1500, 50.0)
Cmputing gain for ('HCL', 1200, 70.5)
Out[115]:
('HCL', 1200, 70.5)
In [119]:
records[0][0] ## r[0]
Out[119]:
'TATA'
In [120]:
records[0][1] ## r[1]
Out[120]:
200.0
In [121]:
records[0][2] ## r[2]
Out[121]:
5.5
In [124]:
max(nums, digits) # key argument can not be passed by position
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-124-fed778a382be> in <module>
----> 1 max(nums, digits) # key argument can not be passed by position

TypeError: '>' not supported between instances of 'function' and 'list'
In [125]:
max(nums, key=digits) # key has to be named argument
Computing digits for  1
Computing digits for  2
Computing digits for  3
Computing digits for  33
Computing digits for  44
Computing digits for  55
Computing digits for  66
Computing digits for  6666
Computing digits for  777
Out[125]:
6666
In [126]:
max(key=digits, nums)
  File "<ipython-input-126-48e15e4c3aaf>", line 1
    max(key=digits, nums)
                    ^
SyntaxError: positional argument follows keyword argument

Functions that return function

In [127]:
def make_addder(x):
    
    def adder(y):
        return x+y
    
    return adder
In [128]:
adder5 = make_addder(5)
In [129]:
adder5
Out[129]:
<function __main__.make_addder.<locals>.adder(y)>
In [130]:
adder5(11)
Out[130]:
16
In [131]:
adder5(4)
Out[131]:
9
In [132]:
adder5(3)
Out[132]:
8
In [133]:
adder5(13)
Out[133]:
18
In [134]:
type(3)
Out[134]:
int
In [135]:
def make_logger(type_):
    
    def logger(msg):
        print("[" + type_.upper() + "]:", msg)
        
    return logger
In [138]:
info = make_logger("info")
error = make_logger("error")
warn = make_logger("warning")
In [139]:
info("This is just for yor information!")
[INFO]: This is just for yor information!
In [140]:
error("SOmwthing went wrong..error!")
[ERROR]: SOmwthing went wrong..error!
In [141]:
warn("I suspect, Something might be wrong!")
[WARNING]: I suspect, Something might be wrong!

lambda expressions

In [142]:
def add(x, y):
    return x+y
In [143]:
add = lambda x, y: x+y
In [144]:
add(2,3)
Out[144]:
5
In [146]:
max(records, key=lambda r: r[2]) # gain
Out[146]:
('HCL', 1200, 70.5)
In [147]:
max(records, key= lambda r: r[1]) #gain
Out[147]:
('INFY', 2000.0, -5)
In [148]:
def get_gain(r):
    return r[2]
In [ ]: