Pages

Friday, September 29, 2017

PYTHON PROGRAMMING LANGUAGE

Debugging : 
#In interactive mode
python3 -i script.py import pdb
pdb.pm()
# This will take you to code and will show the line of error

# Launch debugger inside code
import pdb; pdb.set_trace()

## Program Structure
Variable name in python should always start with either _ or letter

Indentation should be consistent . It can be of 2 spaces or 3 spaces or 4 spaces
Usually program are indented by 4 spaces.

## Formatted printing
name = 'Cisco'
shares = 100
price = 32.86
print('%10s %10d %10.2f' % (name, shares, price))

## values right aligned 
print('{:>10s} {:>10d} {:>10.2f}'.format(name, shares, price))

# values left aligned
print('{:<10s d="" f="" format="" name="" p="" price="" shares="">
# for header
print('{:>10s} {:>10s} {:>10s}'.format('Company','Shares','Price'))

# We can also redirect output to filter
out = open('schedule.txt', 'w') # open file for writing
## tell output to go to that file
print('{:<10s d="" f="" file="out)</p" format="" name="" price="" shares="">out.close() # close the file

# Now if we run the program, output will be redirected to file schedule.txt

## Text processing and files
# deal with .csv file
f = open('/Users/nawlekha/Desktop/pyATS/portfolio.csv', 'r')
print(f)
data = f.read() # this will read whole content of file at once
print(data)
f.close()

name,date,shares,price
AA,06/11/07,100,32.2
IBM,13/05/17,50,91.1
CAT,23/09/06,150,83.44
MSFT,01/02/06,200,51.23
GR,31/10/06,95,40.3
MSFT,09/07/06,50,65.2
IBM,17/05/07,100,70.04
or
f = open('/Users/nawlekha/Desktop/pyATS/portfolio.csv', 'r')
for line in f: # read data line by line
  print(line)
f.close()

AA,06/11/07,100,32.2
IBM,13/05/17,50,91.1
CAT,23/09/06,150,83.44
MSFT,01/02/06,200,51.23
GR,31/10/06,95,40.3
MSFT,09/07/06,50,65.2
IBM,17/05/07,100,70.04

## Other way of working with file using with statement
with open('/Users/nawlekha/Desktop/pyATS/portfolio.csv', 'r') as f:
  data = f.read()
print(data)

## this will automatically take care of closing file

name,date,shares,price
AA,06/11/07,100,32.2
IBM,13/05/17,50,91.1
CAT,23/09/06,150,83.44
MSFT,01/02/06,200,51.23
GR,31/10/06,95,40.3
MSFT,09/07/06,50,65.2
IBM,17/05/07,100,70.04

# Working on string 
c = 'hello'
d = 'world'
print(c + d) ## concatenate string
'helloworld'

# To remove white space at beginning or end of line
line = '\n"Cisco", "2017.08.07", 100,32.88\n'
print(line.strip())
print(line)
"Cisco", "2017.08.07", 100,32.88

# Remember string modification doesn't change original string. We need to save the result to have effect.
line = line.strip()
print(line)
"Cisco", "2017.08.07", 100,32.88
print(line.replace('"','-'))
-Cisco-, -2017.08.07-, 100,32.88
print(line)
"Cisco", "2017.08.07", 100,32.88

parts = line.split(',') ## result is list
print(parts)
['"Cisco"', ' "2017.08.07"', ' 100', '32.88']
parts[0]
'"Cisco"'
parts[1]
' "2017.08.07"'
parts[2]
' 100'
parts[0] = parts[0].strip('"') # to strip double quote
'Cisco'
parts[2] = int(parts[2])
100
parts[3] = float(parts[3])
32.88
print(parts[2] * parts[3])
3288.0000000000005

## Reading file and doing some calculation
total = 0.0
with open('/Users/nawlekha/Desktop/pyATS/portfolio.csv', 'r') as f:
  headers = next(f) # skip a single line input
  for line in f:
    line = line.strip() # strip whitespace
    parts = line.split(',')
    parts[0] = parts[0].strip('"')
    parts[1] = parts[1].strip('"')
    parts[2] = int(parts[2])
    parts[3] = float(parts[3])
    total += parts[2]*parts[3]
    print(parts)
 
print('Total cost:', total)

['AA', '06/11/07', 100, 32.2]
['IBM', '13/05/17', 50, 91.1]
['CAT', '23/09/06', 150, 83.44]
['MSFT', '01/02/06', 200, 51.23]
['GR', '31/10/06', 95, 40.3]
['MSFT', '09/07/06', 50, 65.2]
['IBM', '17/05/07', 100, 70.04]
Total cost: 44629.5

# if csv file have date in  comma separated format like "June 11, 2017" then we will have an issue. To open such .csv file, we can use import csv module

# we can also import library module csv to read csv files
import csv # to read , separted value files
f = open('/Users/nawlekha/Desktop/pyATS/portfolio.csv', 'r')
rows = csv.reader(f)
for row in rows: # this will take care of splitting rows apart and eliminating quotes
  print(row)

['name', 'date', 'shares', 'price']
['AA', '06/11/07', '100', '32.2']
['IBM', '13/05/17', '50', '91.1']
['CAT', '23/09/06', '150', '83.44']
['MSFT', '01/02/06', '200', '51.23']
['GR', '31/10/06', '95', '40.3']
['MSFT', '09/07/06', '50', '65.2']
['IBM', '17/05/07', '100', '70.04']

## Defining and using Simple functions:
def add(x,y):
  result = x + y
  return result

print(add(4,5))

#print(x) #Everything that happens in function stays inside function. x and y are local to a function

print(add(x=10, y=15))

print(add(y=15, x=10))

print(add(5, y=20))
     
## Moving a script into function
import csv
def portfolio_cost(filename):
  '''
  computes calculation
  '''
  total = 0.0
  with open('/Users/nawlekha/Desktop/pyATS/portfolio.csv', 'r') as f:
    rows = csv.reader(f)
    headers = next(rows) # skip header rows
    for row in rows:
      row[2] = int(row[2])
      row[3] = float(row[3])
      total += row[2]*row[3]
  return total 
 
total = portfolio_cost('/Users/nawlekha/Desktop/pyATS/portfolio.csv')
print('Total cost:', total)

Total cost: 44629.5

#glob module
import glob # to match filenames with specific patterns
files = glob.glob('/Users/nawlekha/Desktop/pyATS//portfolio*.csv')
print(files)
## this will print all files portfolio1, 2 and so on..
for filename in files:
  print(filename, portfolio_cost(filename))

['/Users/nawlekha/Desktop/pyATS/portfolio.csv', '/Users/nawlekha/Desktop/pyATS/portfolio1.csv']
/Users/nawlekha/Desktop/pyATS/portfolio.csv 44629.5
/Users/nawlekha/Desktop/pyATS/portfolio1.csv 44629.5

## Data and Exception Handling
import csv
def portfolio_cost(filename):
  '''
  computes calculation
  '''
  total = 0.0
  with open('/Users/nawlekha/Desktop/pyATS/missing.csv', 'r') as f:
    rows = csv.reader(f)
    headers = next(rows) # skip header rows
    for row in rows:
      try:
        row[2] = int(row[2])
        row[3] = float(row[3])
      except ValueError:
        print('Bad row:', row) # if find error skip to next row
        continue
      total += row[2]*row[3]
  return total 

total = portfolio_cost('/Users/nawlekha/Desktop/pyATS/missing.csv')
print('Total cost:', total)

Bad row: ['CAT', '23/09/06', '', '83.44']
Bad row: ['MSFT', '09/07/06', 'N/A', '65.2']
Total cost: 28853.5
or
  except ValueError as err: # this will store exception in variable err
      print('Reason:', err) ## this will print exact error
   
Bad row: ['CAT', '23/09/06', '', '83.44']
Reason: invalid literal for int() with base 10: ''
Bad row: ['MSFT', '09/07/06', 'N/A', '65.2']
Reason: invalid literal for int() with base 10: 'N/A'
Total cost: 28853.5
or

# we can also print exact row number where error is seen

import csv
def portfolio_cost(filename):
  '''
  computes calculation
  '''
  total = 0.0
  with open('/Users/nawlekha/Desktop/pyATS/missing.csv', 'r') as f:
    rows = csv.reader(f)
    headers = next(rows) # skip header rows
    rowno = 0
    for row in rows:
      rowno += 1
      try:
        row[2] = int(row[2])
        row[3] = float(row[3])
      except ValueError as err:
        print('Row:',rowno, 'Bad row:', row) # if find error skip to next row
        print('Row:',rowno, 'Reason:', err)
        continue
      total += row[2]*row[3]
  return total   

total = portfolio_cost('/Users/nawlekha/Desktop/pyATS/missing.csv')
print('Total cost:', total)

Row: 3 Bad row: ['CAT', '23/09/06', '', '83.44']
Row: 3 Reason: invalid literal for int() with base 10: ''
Row: 6 Bad row: ['MSFT', '09/07/06', 'N/A', '65.2']
Row: 6 Reason: invalid literal for int() with base 10: 'N/A'
Total cost: 28853.5
or

# we can also use enumerate which gives extra counter with loops. Using it, we don't have to initialize data and increment it 

import csv
def portfolio_cost(filename):
  '''
  computes calculation
  '''
  total = 0.0
  with open('/Users/nawlekha/Desktop/pyATS/missing.csv', 'r') as f:
    rows = csv.reader(f)
    headers = next(rows) # skip header rows
    for rowno, row in enumerate(rows, start=1): # start means start of count
      try:
        row[2] = int(row[2])
        row[3] = float(row[3])
      except ValueError as err:
        print('Row:',rowno, 'Bad row:', row) # if find error skip to next row
        print('Row:',rowno, 'Reason:', err)
        continue
      total += row[2]*row[3]
  return total     

total = portfolio_cost('/Users/nawlekha/Desktop/pyATS/missing.csv')
print('Total cost:', total)

Row: 3 Bad row: ['CAT', '23/09/06', '', '83.44']
Row: 3 Reason: invalid literal for int() with base 10: ''
Row: 6 Bad row: ['MSFT', '09/07/06', 'N/A', '65.2']
Row: 6 Reason: invalid literal for int() with base 10: 'N/A'
Total cost: 28853.5 
 
## Function design considerations
import csv
def portfolio_cost(filename):
  '''
  computes calculation
  '''
  total = 0.0
  with open('/Users/nawlekha/Desktop/pyATS/missing.csv', 'r') as f:
    rows = csv.reader(f)
    headers = next(rows) # skip header rows
    for rowno, row in enumerate(rows, start=1): # start means start of count
      try:
        row[2] = int(row[2])
        row[3] = float(row[3])
      except ValueError as err:
        print('Row:',rowno, 'Bad row:', row) # if find error skip to next row
        print('Row:',rowno, 'Reason:', err)
        continue
      total += row[2]*row[3]
  return total       

total = portfolio_cost('/Users/nawlekha/Desktop/pyATS/missing.csv')
print('Total cost:', total)

Row: 3 Bad row: ['CAT', '23/09/06', '', '83.44']
Row: 3 Reason: invalid literal for int() with base 10: ''
Row: 6 Bad row: ['MSFT', '09/07/06', 'N/A', '65.2']
Row: 6 Reason: invalid literal for int() with base 10: 'N/A'
Total cost: 28853.5

##, unfortunately, we don't know what type of error code will generate. One thing we can do is catch all errors as:
    except Exception as err: ## catches all errors but this is not good practice
    so its better to avoid using this
 
# another concern

import csv
def portfolio_cost(filename, errors = 'warn'):
  '''
  computes calculation
  '''
  total = 0.0

  with open('/Users/nawlekha/Desktop/pyATS/missing.csv', 'r') as f:
    rows = csv.reader(f)
    headers = next(rows) # skip header rows
    for rowno, row in enumerate(rows, start=1): # start means start of count
      try:
        row[2] = int(row[2])
        row[3] = float(row[3])
      except ValueError as err:
        if errors == 'warn':
          print('Row:',rowno, 'Bad row:', row) # if find error skip to next row
          print('Row:',rowno, 'Reason:', err)
        continue
      total += row[2]*row[3]
  return total

total = portfolio_cost('/Users/nawlekha/Desktop/pyATS/missing.csv', 'silent') ## this will skip print statement
print('Total cost:', total)

Total cost: 28853.5
or
total = portfolio_cost('Data/missing.csv', errors = 'silent') # for better readable

other way to force user to call this way is by using * in def function
def portfolio_cost(filename, *,errors = 'warn'):
# while calling, now use has to use keyword style
total = portfolio_cost('/Users/nawlekha/Desktop/pyATS/missing.csv',  errors ='silent')

# what if there are multiple values for errors
import csv
def portfolio_cost(filename, *, errors = 'warn'):
  '''
  computes total shares*price for a CSV file with name,date, shares,price data
  '''
  if errors not in { 'warn', 'silent', 'raise' }:
      raise ValueError("errors must be one of 'warn', 'silent', 'raise'")
  total = 0.0
  with open('/Users/nawlekha/Desktop/pyATS/missing.csv', 'r') as f:
    rows = csv.reader(f)
    headers = next(rows) # skip header rows
    for rowno, row in enumerate(rows, start=1): # start means start of count
      try:
        row[2] = int(row[2])
        row[3] = float(row[3])
      except ValueError as err:
        if errors == 'warn':
          print('Row:',rowno, 'Bad row:', row) # if find error skip to next row
          print('Row:',rowno, 'Reason:', err)
        elif errors == 'raise':
          raise # reraise last Exception
        else:
          pass
        continue
      total += row[2]*row[3]

total = portfolio_cost('/Users/nawlekha/Desktop/pyATS/missing.csv',  errors ='ignore')
print('Total cost:', total)
   
# if we enter bad value it will print errors must be one of 'warn', 'silent', 'raise'    
Traceback (most recent call last):
  File "/Users/nawlekha/Desktop/pyATS/test.py", line 27, in
    total = portfolio_cost('/Users/nawlekha/Desktop/pyATS/missing.csv',  errors ='ignore')
  File "/Users/nawlekha/Desktop/pyATS/test.py", line 7, in portfolio_cost
    raise ValueError("errors must be one of 'warn', 'silent', 'raise'")
ValueError: errors must be one of 'warn', 'silent', 'raise'

###Data structure and manipulation

#Bulit in data types

#tuple
t = ('AA', '2017-08-07', 100, 32.2) # this is called packing of values
print(len(t))
4
t[0]
'AA'
t[1]
'2017-08-07'
t[2]*t[3]
3220.0000000000005

# we can do unpacking of values as;
name, date , shares, price = t
print(name)
print(date)
print(shares)
print(price)

AA
2017-08-07
100
32.2

# tuple are also immutable. Once it is created we can't change them
t[2] = 50

Traceback (most recent call last):
  File "/Users/nawlekha/Desktop/pyATS/test.py", line 15, in
    t[2] = 50
TypeError: 'tuple' object does not support item assignment

# list
names = ['IBM', 'YAHoo', 'Cisco', 'CAT']
# we can change the list items
print(names)
names.append('IBM')
names.insert(1, 'FB')
print(names)
names[2] = 'HPE'
print(names)
# basically in list all items are of same type

['IBM', 'YAHoo', 'Cisco', 'CAT']
['IBM', 'FB', 'YAHoo', 'Cisco', 'CAT', 'IBM']
['IBM', 'FB', 'HPE', 'Cisco', 'CAT', 'IBM']

# set 
distinct_names = {'YAHoo', 'IBM', 'IBM', 'Cisco', 'Yahoo', 'Cisco'}
# eliminates duplicate items
print(distinct_names)

{'Yahoo', 'Cisco', 'YAHoo', 'IBM'}

#one use of set is we can convert list to set to eliminate dups 
set(names)
{'CAT', 'IBM', 'Cisco', 'YAHoo'}

#another use is to test membership
print('IBM' in distinct_names)
print('AA' in distinct_names)

True
False

# Dictionary: mapping between key and values
prices = {
  'IBM': 91.2,
  'MSFT': 45.23,
  'Cisco': 36.5
}

print(prices['IBM'])
91.2
# we can also re-assign new values
prices['IBM'] = 87.23
print(prices)
{'IBM': 87.23, 'MSFT': 45.23, 'Cisco': 36.5}

# we can also test membership
print('IBM' in prices)
True

# we can have list of tuples
items = [('AA', 100, 32.2), ('MSFT', 50, 91.2)]

[('AA', 100, 32.2), ('MSFT', 50, 91.2)]

# for complicated Dictionary
prices = {
  'IBM' : {'current': 91.23, 'high': 94.23, 'low': 91.10}
}

print(prices)
print(prices['IBM']['low'])

{'IBM': {'current': 91.23, 'high': 94.23, 'low': 91.1}}
91.1

## Building data structure from a file

# one way to read data from data structure
import csv
def read_portfolio(filename, *,errors = 'warn'):
  # read a CSV file with name, date, shares, price data into a list

  if errors not in {'warn', 'silent', 'raise'}:
     raise ValueError("errors must be one of 'warn', 'silent', 'raise'")

  portfolio = [] # list of records
  with open('/Users/nawlekha/Desktop/pyATS/portfolio.csv', 'r') as f:
    rows = csv.reader(f)
    headers = next(rows) # skip header rows
    for rowno, row in enumerate(rows, start=1): # start means start of count
      try:
        row[2] = int(row[2])
        row[3] = float(row[3])
      except ValueError as err:
        if errors == 'warn':
          print('Row:',rowno, 'Bad row:', row) # if find error skip to next row
          print('Row:',rowno, 'Reason:', err)
        elif errors == 'raise':
          raise # re-raise last Exception
        else:
          pass
        continue
      record = tuple(row)
      portfolio.append(record)
  return portfolio
 
portfolio = read_portfolio('/Users/nawlekha/Desktop/pyATS/portfolio.csv')
print(portfolio)
print(len(portfolio))
## this should return list of tuples. files is converted into rows
len(portfolio)
# each entry is row of tuples

[('AA', '06/11/07', 100, 32.2), ('IBM', '13/05/17', 50, 91.1), ('CAT', '23/09/06', 150, 83.44), ('MSFT', '01/02/06', 200, 51.23), ('GR', '31/10/06', 95, 40.3), ('MSFT', '09/07/06', 50, 65.2), ('IBM', '17/05/07', 100, 70.04)]
7

# Still we can find the total
total = 0.0
for holding in portfolio:
  total += holding[2]*holding[3] #shares* price

print('Total cost:', total)

Total cost: 44629.5

#or better way is 
for name, date, shares, price in portfolio:
  total += shares*price
print('Total cost:', total)

Total cost: 44629.5

# if file has many rows and columns then its better to use dictionary rather than tuple in code 
import csv
def read_portfolio(filename, *,errors = 'warn'):
  # read a CSV file with name, date, shares, price data into a list

  if errors not in {'warn', 'silent', 'raise'}:
     raise ValueError("errors must be one of 'warn', 'silent', 'raise'")

  portfolio = [] # list of records
  with open('/Users/nawlekha/Desktop/pyATS/portfolio.csv', 'r') as f:
    rows = csv.reader(f)
    headers = next(rows) # skip header rows
    for rowno, row in enumerate(rows, start=1): # start means start of count
      try:
        row[2] = int(row[2])
        row[3] = float(row[3])
      except ValueError as err:
        if errors == 'warn':
          print('Row:',rowno, 'Bad row:', row) # if find error skip to next row
          print('Row:',rowno, 'Reason:', err)
        elif errors == 'raise':
          raise # re-raise last Exception
        else:
          pass
        continue
      record = {
          'name': row[0],
          'date': row[1],
          'shares': row[2],
          'price': row[3]
      }
      portfolio.append(record)
  return portfolio # now it will return list of dictionary
 
portfolio = read_portfolio('/Users/nawlekha/Desktop/pyATS/portfolio.csv')

total = 0.0
for holding in portfolio:
  total += holding['shares']*holding['price']

print('Total cost:', total)

Total cost: 44629.5

# Using dictionary we can re-use code in other programming language as well
# we can port  code to java as
import jason
data = jason.dumps(portfolio)
print(data)
# if we want data back into python again
port = jason.loads(data)
print(port)

# Data manipulation
# find all shares names from the list
names = []
for holding in portfolio:
  names.append(holding['name'])
print(names)

['AA', 'IBM', 'CAT', 'MSFT', 'GR', 'MSFT', 'IBM']

# find all holding having share more than 100
more100 = []
for holding in portfolio:
  if holding['shares'] > 100:
    more100.append(holding) 
print(more100)

[{'name': 'CAT', 'date': '23/09/06', 'shares': 150, 'price': 83.44}, {'name': 'MSFT', 'date': '01/02/06', 'shares': 200, 'price': 51.23}]

# List comprehension
total = sum([holding['shares']*holding['price'] for holding in portfolio])
# here we are passing list of shares and price to sum
print(total)

44629.5

names = [holding['name'] for holding in portfolio]
print('Names:', names)
Names: ['AA', 'IBM', 'CAT', 'MSFT', 'GR', 'MSFT', 'IBM']

more100 = [holding for holding in portfolio if holding['shares'] > 100]
print('Shares:', more100)
Shares: [{'name': 'CAT', 'date': '23/09/06', 'shares': 150, 'price': 83.44}, {'name': 'MSFT', 'date': '01/02/06', 'shares': 200, 'price': 51.23}]

# if we want only name of stock
more100 = [holding['name'] for holding in portfolio if holding['shares'] > 100]
print('Stock:', more100)
Stock: ['CAT', 'MSFT']

# Now lets find share price comparison
names = [holding['name'] for holding in portfolio] # this has duplicate
print('Names:', names)

#unique_names = set(names) or
unique_names = {holding['name'] for holding in portfolio}
print('Unique:', unique_names)
print(type(unique_names))

namestr = ','.join(unique_names)
print('Namestr:', namestr)
print(type(namestr))

Names: ['AA', 'IBM', 'CAT', 'MSFT', 'GR', 'MSFT', 'IBM']
Unique: {'CAT', 'IBM', 'GR', 'MSFT', 'AA'}

Namestr: CAT,IBM,GR,MSFT,AA


import urllib.request
u = urllib.request.urlopen('http://finance.yahoo.com/d/quotes.csv?s={}&f=l1'.format(namestr))
data = u.read()
# this will return prices of stocks
pricedata = data.split()
for name, price in zip(unique_names, pricedata): # to do pairing of stock name and price
  print(name, '=', price)

# to have same in dict format
prices = dict(zip(unique_names, pricedata))
prices['IBM']

## dictionary comprehension
prices = {name:float(price) for name, price in zip(unique_names, pricedata)}
print(prices)

current_value = 0.0
for holding in portfolio:
  current_value += holding['shares']* prices[holding['name']]
or
current_value = sum([holding['shares']* prices[holding['name']] for holding in portfolio])
change = current_value - total

## Sorting and Grouping
portfolio.sort()
# this will throw error becoz it doesn't know on what basis to sort
Traceback (most recent call last):
  File "/Users/nawlekha/Desktop/pyATS/test.py", line 65, in
    portfolio.sort()
TypeError: '<' not supported between instances of 'dict' and 'dict'

def holding_name(holding):
  return holding['name']

print(portfolio[0])

portfolio.sort(key=holding_name)

for holding in portfolio:
  print(holding) # now return is dict with sorted names

{'name': 'AA', 'date': '06/11/07', 'shares': 100, 'price': 32.2}
{'name': 'CAT', 'date': '23/09/06', 'shares': 150, 'price': 83.44}
{'name': 'GR', 'date': '31/10/06', 'shares': 95, 'price': 40.3}
{'name': 'IBM', 'date': '13/05/17', 'shares': 50, 'price': 91.1}
{'name': 'IBM', 'date': '17/05/07', 'shares': 100, 'price': 70.04}
{'name': 'MSFT', 'date': '01/02/06', 'shares': 200, 'price': 51.23}
{'name': 'MSFT', 'date': '09/07/06', 'shares': 50, 'price': 65.2}

# Instead of defining function we can do sort based on lambda
portfolio.sort(key = lambda holding: holding['name'])

a = lambda x: 10*x
a(10) # will return 100

# for min and mix also, we can use lambda
print(min(portfolio, key = lambda holding: holding['price’]))
print(max(portfolio, key = lambda holding: holding['price’]))

{'name': 'AA', 'date': '06/11/07', 'shares': 100, 'price': 32.2}
{'name': 'IBM', 'date': '13/05/17', 'shares': 50, 'price': 91.1}

## Group data
import itertools
for price, items in itertools.groupby(portfolio, key=lambda holding: holding['price']):
  print('Price:', price)
  for it in items:
    print(' ', it)
 
Price: 32.2
  {'name': 'AA', 'date': '06/11/07', 'shares': 100, 'price': 32.2}
Price: 83.44
  {'name': 'CAT', 'date': '23/09/06', 'shares': 150, 'price': 83.44}
Price: 40.3
  {'name': 'GR', 'date': '31/10/06', 'shares': 95, 'price': 40.3}
Price: 91.1
  {'name': 'IBM', 'date': '13/05/17', 'shares': 50, 'price': 91.1}
Price: 70.04
  {'name': 'IBM', 'date': '17/05/07', 'shares': 100, 'price': 70.04}
Price: 51.23
  {'name': 'MSFT', 'date': '01/02/06', 'shares': 200, 'price': 51.23}
Price: 65.2
  {'name': 'MSFT', 'date': '09/07/06', 'shares': 50, 'price': 65.2}
 
# lets take data and build dictionary on it
by_name = {name: list(items)
          for name, items in itertools.groupby(portfolio, key=lambda holding: holding['holding_name'])
       
by_name['IBM']

## Library functions and import
x = 42

def spam():
    print('x is', x)

def run():
    print('calling spam')
    spam()

print('Running')
run()

Save this code in python file simple.py

# Now we want to import this module simple
>>> import simple # this will fetch the file and execute completely
Running
calling spam
x is 42

#Now every content of that module is accessible 

>>> simple

>>> simple.x
42
>>> simple.spam

>>> simple.spam()
x is 42
>>>

# we have inbuilt module as well
>>> import math
>>> math.cos(2)
-0.4161468365471424
>>> math.sqrt(2)
1.4142135623730951
>>>

Another variation of import:

>>> from simple import run
In this case we will get same output but we will see only run() function. we will not have access to x and spam()
>>> run()
calling spam
x is 42

What this actually does is:
import simple
run = simple.run # copy
run()

>>> from simple import spam, x
>>> spam()
x is 42
>>> x
42
>>>

# if we want to change the value of x we need to import simple and change it
>>> x = 13
>>> x
13
>>> spam()
x is 42
>>>
>>>
>>> import simple
>>> simple.x
42
>>> simple.x = 37
>>> spam()
x is 37

## another aspect of import is it is one time operation. First-time import will execute the code but not second time.
>> import simple
Running
calling spam
x is 42

>>> import simple ## no output. Python caches all of the modules already loaded and it will never reload again.
>>>

# to find path of module

>>> import sys
>>> sys.modules['simple']


Note: if you change some code in module. You need to quit python or delete cache and import again to see the changes

>>> import sys
>>> sys.path
['/Users/nawlekha/Desktop/pyATS', '/Library/Frameworks/Python.framework/Versions/3.6/bin', '/Library/Frameworks/Python.framework/Versions/3.6/lib/python36.zip', '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6', '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/lib-dynload', '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages']

Python will look for source code in these paths one by one. If it is not there it will throw error module not found

If module is in different directory. we can update env : PYTHONPATH or append that dir as : sys.path.append(‘..’) and perform import

x = 42

def spam():
    print('x is', x)

def run():
    print('calling spam')
    spam()

# it is not wise to expose these commented lines so will move under If statement
#print('Running')
#run()

#doing this python program will work as script
if __name__ == '__main__': # to check the name of the module and check if you are the main program or not
    print('Running')
    run()

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3 simple.py
Running
calling spam
x is 42
nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$

Now if you import simple and run we won’t see that code

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3
Python 3.6.1 (v3.6.1:69c0db5050, Mar 21 2017, 01:21:04)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import simple
>>>

Every single module knows its own name. There is global variable called __name__ defined in python and if you are the main program or you are in interactive shell your name is main

>>> import simple
>>> __name__
'__main__'

>>> simple.__name__
'simple'

Note : code under if statement will execute if you are running it as main program. 
if you are using import statement, that code will not execute

>>> simple.spam()
x is 42

## Writing a General Purpsose CSV Parsing Module

Instead of writing function to read .csv file we can do

>>> f = open("/Users/nawlekha/Desktop/pyATS/portfolio.csv", 'r')
>>> import csv
>>> rows = csv.reader(f)
>>> headers = next(rows)
>>> headers
['name', 'date', 'shares', 'price']
>>> row = next(rows)
>>> row
['AA', '06/11/07', '100', '32.2']
>>> types = [str, str, int, float]
>>> int('42')
42
>>> float('4.2')
4.2
>>> types
[, , , ]
>>> row
['AA', '06/11/07', '100', '32.2']
>>> for func, val in zip(types, row):
print(func, val)

AA
06/11/07
100
32.2
>>>

>>> converted = [func(val) for func, val in zip(types, row)]
>>> converted
['AA', '06/11/07', 100, 32.2]
>>> dict(zip(headers, converted))
{'name': 'AA', 'date': '06/11/07', 'shares': 100, 'price': 32.2}
>>>

>>> import reader
>>> portfolio = reader.read_csv('/Users/nawlekha/Desktop/pyATS/portfolio.csv', [str, str, int, float])
>>> portfolio
[{'name': 'AA', 'date': '06/11/07', 'shares': 100, 'price': 32.2}, {'name': 'IBM', 'date': '13/05/17', 'shares': 50, 'price': 91.1}, {'name': 'CAT', 'date': '23/09/06', 'shares': 150, 'price': 83.44}, {'name': 'MSFT', 'date': '01/02/06', 'shares': 200, 'price': 51.23}, {'name': 'GR', 'date': '31/10/06', 'shares': 95, 'price': 40.3}, {'name': 'MSFT', 'date': '09/07/06', 'shares': 50, 'price': 65.2}, {'name': 'IBM', 'date': '17/05/07', 'shares': 100, 'price': 70.04}]
>>>

Now lets use this:
change our port.py file and use reader module

import reader
def read_portfolio(filename, *, errors='warn'):
    '''
    read a csv file with name,date, shares, price data
    '''
    return reader.read_csv(filename, [str, str, int, float], errors=errors)
portfolio = read_portfolio('/Users/nawlekha/Desktop/pyATS/portfolio.csv')
total = 0.0
for holding in portfolio:
    total += holding['shares']*holding['price']

print('Total cost:', total)

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3 port.py
Total cost: 44629.5

# still we got output but using reader module

If somebody want to run above code as main program then we will modify code as:

import reader
def read_portfolio(filename, *, errors='warn'):
    '''
    read a csv file with name,date, shares, price data
    '''
    return reader.read_csv(filename, [str, str, int, float], errors=errors)

if __name__ == ‘__main__’
    portfolio = read_portfolio('/Users/nawlekha/Desktop/pyATS/portfolio.csv')
    total = 0.0
    for holding in portfolio:
        total += holding['shares']*holding['price']

    print('Total cost:', total)


nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3 port.py
Total cost: 44629.5
nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$

## Making a package
How to create package ?
First create a directory and move some files to it. Create empty __init__.py file

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ mkdir portie
nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ mv port.py reader.py portie
nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ ls portie
port.py    reader.py
nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$
nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$
nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ touch portie/__init__.py

# we are trying to isolate our program module with others programming module

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3
Python 3.6.1 (v3.6.1:69c0db5050, Mar 21 2017, 01:21:04)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import portie.reader
>>> port = portie.reader.read_csv('/Users/nawlekha/Desktop/pyATS/portfolio.csv', [str, str, int, float])
>>> print(port)
[{'name': 'AA', 'date': '06/11/07', 'shares': 100, 'price': 32.2}, {'name': 'IBM', 'date': '13/05/17', 'shares': 50, 'price': 91.1}, {'name': 'CAT', 'date': '23/09/06', 'shares': 150, 'price': 83.44}, {'name': 'MSFT', 'date': '01/02/06', 'shares': 200, 'price': 51.23}, {'name': 'GR', 'date': '31/10/06', 'shares': 95, 'price': 40.3}, {'name': 'MSFT', 'date': '09/07/06', 'shares': 50, 'price': 65.2}, {'name': 'IBM', 'date': '17/05/07', 'shares': 100, 'price': 70.04}]
>>>

# some of the module might not work properly with package as shown below. This is because there might be other standard reader module somewhere.
>>> import portie.port
Traceback (most recent call last):
  File "", line 1, in
  File "/Users/nawlekha/Desktop/pyATS/portie/port.py", line 32, in
    import reader
ModuleNotFoundError: No module named 'reader'
>>>

One way to fix it is . Open port.py file and change “import reader” to “import portie.reader “. 
or 
change “import reader” to “from . import reader” # package relative import from same directory

## use of __init__.py file
when we import any module, this file will get execute.

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS/portie$ vi __init__.py

print('loading portie')

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3
Python 3.6.1 (v3.6.1:69c0db5050, Mar 21 2017, 01:21:04)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import portie.port
loading portie
>>>

## one use of this file is to perform initialization steps. Another use it to load symbols from sub-modules like.

In init file:
nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS/portie$ vi __init__.py

print('loading portie')
from .port import read_portfolio
from .reader import read_csv

# we can use those function without knowing what their sub-modules are

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3
Python 3.6.1 (v3.6.1:69c0db5050, Mar 21 2017, 01:21:04)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import portie
loading portie
>>> portie.read_portfolio('/Users/nawlekha/Desktop/pyATS/portfolio.csv')
[{'name': 'AA', 'date': '06/11/07', 'shares': 100, 'price': 32.2}, {'name': 'IBM', 'date': '13/05/17', 'shares': 50, 'price': 91.1}, {'name': 'CAT', 'date': '23/09/06', 'shares': 150, 'price': 83.44}, {'name': 'MSFT', 'date': '01/02/06', 'shares': 200, 'price': 51.23}, {'name': 'GR', 'date': '31/10/06', 'shares': 95, 'price': 40.3}, {'name': 'MSFT', 'date': '09/07/06', 'shares': 50, 'price': 65.2}, {'name': 'IBM', 'date': '17/05/07', 'shares': 100, 'price': 70.04}]
>>>

## Classes and Objects
to create object using class statement. Class is a convenient way to define data structure and to attach method to carry out operations on data.
instead of doing below calculation using dictionary, we can use class

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3
Python 3.6.1 (v3.6.1:69c0db5050, Mar 21 2017, 01:21:04)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> holding = {'name': 'AA', 'date': '2007-06-11', 'shares': 100, 'price': 32}
>>> holding[name]
Traceback (most recent call last):
  File "", line 1, in
NameError: name 'name' is not defined
>>> holding['name']
'AA'
>>> holding['shares']
100

>>> def cost(holding):
...     return holding['shares']*holding['price']
...
>>> cost(holding)
3200
>>>

## Understanding attribute access
class Holding(object): # object is compulsory for python 2 and it is optional in python 3
  def __init__(self, name, date, shares, price):
    self.name = name
    self.date = date
    self.shares = shares
    self.price = price
 
  def cost(self):
    return self.shares*self.price
 
  def sell(self, nshares):
    self.shares -= nshares

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3 -i holding.py
>>> >>> h = Holding('AA', '2017-08-17', 100, 35.5)

three operations we can do with class
#get attributes
>>> h.name
'AA'
>>> h.price
35.5
>>> h.shares
100

# set attributes
h.shares = 50
print(h.shares)

# delete attributes
del h.shares

# we can also set new attributes like time but this can leads to some problem
h.time = '10 am'
print(h.time)

# for get and set attributes alternatively we can used built in function
print(getattr(h, 'name')) # h.name
>>> getattr(h, 'name')
'AA'

setattr(h, 'shares', 50) # h.shares = 50
print(h.shares)

delattr(h, 'shares')

# call method
>>> h.cost()
3550.0

>>> print('%10s %10d %10.2f' % (h.name, h.shares, h.cost()))
        AA        100    3550.00

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3 -i holding.py
>>> portfolio = read_portfolio('/Users/nawlekha/Desktop/pyATS/portfolio.csv')
>>> portfolio
[<__main__ .holding="" 0x1022964a8="" at="" object="">, <__main__ .holding="" 0x102296438="" at="" object="">, <__main__ .holding="" 0x102296518="" at="" object="">, <__main__ .holding="" 0x102296588="" at="" object="">, <__main__ .holding="" 0x1022965f8="" at="" object="">, <__main__ .holding="" 0x102296668="" at="" object="">, <__main__ .holding="" 0x1022966d8="" at="" object="">]
>>>
>>> total = 0.0
>>> for h in portfolio:
...     total += h.shares * h.price
...
>>> total
96525.0
>>>

# we can do list comprehension as well

>>> names = [h.name for h in portfolio]
>>> names
['AA', 'IBM', 'CAT', 'MSFT', 'GR', 'MSFT', 'IBM']
>>>
>>> output_columns = ['name', 'shares', 'price']
>>> for colname in output_columns:
...     print(colname, '=', getattr(h, colname))
...
name = AA
shares = 100
price = 35.

>>> portfolio = read_portfolio('/Users/nawlekha/Desktop/pyATS/portfolio.csv')
>>> import table
>>> table.print_table(portfolio, ['names', 'shares'])
{:>10s}.format(colname)
Traceback (most recent call last):
  File "", line 1, in
  File "/Users/nawlekha/Desktop/pyATS/table.py", line 10, in print_table
    print('{:>10s}'.format(str(getattr(obj, colname))), end= '')
AttributeError: 'Holding' object has no attribute 'names'

>>> import importlib
>>> importlib.reload(table)

>>> table.print_table(portfolio, ['name', 'shares'])

Class methods and Alternate Constructors:
=================================
One way to create object is by using classname . What if we want to create object in different way.
for that we are creating a file dateobj.py

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3
Python 3.6.1 (v3.6.1:69c0db5050, Mar 21 2017, 01:21:04)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import dateobj
>>> d = dateobj.Date(2012, 12, 21)
>>> d.year
2012

Let’s create object in diff way

say we have

s = ‘2007-06-11’
let’s make date from it, one way is

>>> s = '2007-06-11'
>>> parts = s.split('-')
>>> parts
['2007', '06', '11']
>>> d = dateobj.Date(int(parts[0]), int(parts[1]), int(parts[2]))
>>> d.year
2007

We can also create a function that can return date from string

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3
Python 3.6.1 (v3.6.1:69c0db5050, Mar 21 2017, 01:21:04)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import dateobj
>>> d = dateobj.date_from_string('2007-06-11')
>>> d

>>> d.year
2007

One issue of this is date_from_string function is kind of detached from class

One way to solve this problem is using constructor @classmethod

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3 -i dateobj.py
>>> d = Date.from_string('2007-06-11')
>>> d
<__main__ .date="" 0x102285cf8="" at="" object="">
or
>>> e = Date(2012, 12 ,21)
>>>

this way we are using class to create a date. method is attached to class.

let's create another constructor as well

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3 -i dateobj.py
>>> d = Date(2012, 12, 21)
>>> e = Date.from_string('2007-06-11')
>>> f = Date.today()
>>> f.year
2017
>>> f.month
8
>>> f.day
14
>>>

## Inheritance concepts (to use program extensively)

class Parent(object):
  def __init__(self, value):
    self.value = value
 
  def spam(self):
    print('Parent.spam', self.value)
 
  def grok(self):
    print('Parent.grok')
    self.spam()
 
nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3 -i inherit.py
>>> p = Parent(42)
>>> p.value
42
>>> p.spam()
Parent.spam 42
>>> p.gork
Traceback (most recent call last):
  File "", line 1, in
AttributeError: 'Parent' object has no attribute 'gork'
>>> p.grok()
Parent.grok
Parent.spam 42
>>>

class Child1(Parent): # inherit attributes from Parent class
  def yow(self): # we can add new method in Child class. Add something to original code. This is one benefit
    print('Child1.yow')
 
c = Child1(42)
print(c.value)

print(c.spam())
print(c.grok())
print(c.yow())

>>> class Child1(Parent):
...     def yow(self):
...             print('Child1.yow')
...
>>> c = Child1(42)
>>> c.value
42
>>> c.spam()
Parent.spam 42
>>> c.grok()
Parent.grok
Parent.spam 42
>>> c.yow()
Child1.yow
>>>

## Another benefit is we can redefine the existing classmethod
class Child2(Parent):
  def spam(self):
    print('Child2.spam', self.value)
 
c2 = Child2(42)
print(c2.spam())

print(c2.grok()) # now this will print Child2.spam 42 instead of Parent.spam 42

>>> class Child2(Parent):
...     def spam(self):
...             print('Child2.spam', self.value)
...
>>> c2 = Child2(42)
>>> c2.spam()
Child2.spam 42
>>> c2.grok()
Parent.grok
Child2.spam 42
>>>

## We can also discard current/existing method and call original method
class Child3(Parent):
  def spam(self):
    print('Child3.spam')
    super().spam() ## This invokes original spam() method

c3 = Child3(42)
print(c3.spam())
## output is
#Child3.spam
#Parent.spam 42

print(c3.grok())

>>>
>>> class Child3(Parent):
...     def spam(self):
...             print('Child3.spam')
...             super().spam()
...
>>> c3 = Child3(42)
>>> c3.spam()
Child3.spam
Parent.spam 42
>>> c3.grok()
Parent.grok
Child3.spam
Parent.spam 42
>>>

## 4th benefit is we can add new attribute to object
class Child4(Parent):
  def __init__(self, value, extra):
    self.extra = extra
    super().__init__(value)
 
c4 = Child4(42, 37)
print(c4.value)

print(c4.extra)

## Another benefit is we can have more than one Parent
class Parent2(object):
  def yow(self):
    print('Parent2.yow')
 
class Child5(Parent, Parent2): # we can inherit from more than one Parent. Kind of multiple inheritances
  pass

c5 = Child5(42)
print(c5.grok())
print(c5.spam())
print(c5.yow())

>>> class Child4(Parent):
...     def __init__(self, value, extra):
...             self.extra = extra
...             super().__init__(value)
...
>>> c4 = Child4(42, 37)
>>> c4.value
42
>>> c4.extra
37
>>>
>>> class Parent2(object):
...     def yow(self):
...             print('Parent2.yow')
...
>>>
>>> class Child5(Parent, Parent2):
...     pass
...
>>> c5 = Child5(42)
>>> c5.grok()
Parent.grok
Parent.spam 42
>>> c5.spam()
Parent.spam 42
>>> c5.yow()
Parent2.yow
>>>

## Building a extensible library:
nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3 -i holding.py
>>> portfolio = read_portfolio("/Users/nawlekha/Desktop/pyATS/portfolio.csv")
>>> portfolio
[<__main__ .holding="" 0x102296470="" at="" object="">, <__main__ .holding="" 0x102296400="" at="" object="">, <__main__ .holding="" 0x1022964e0="" at="" object="">, <__main__ .holding="" 0x102296550="" at="" object="">, <__main__ .holding="" 0x1022965c0="" at="" object="">, <__main__ .holding="" 0x102296630="" at="" object="">, <__main__ .holding="" 0x1022966a0="" at="" object="">]
>>> table.print_table(portfolio, ['name', 'price'])
Traceback (most recent call last):
  File "", line 1, in
NameError: name 'table' is not defined
>>> import table
>>> table.print_table(portfolio, ['name', 'price'])
      name      price
        AA      100.0
       IBM       50.0
       CAT      150.0
      MSFT      200.0
        GR       95.0
      MSFT       50.0
       IBM      100.0
>>>

what if i want to have table in different format. say html table
'''
def print_table(objects, colnames):
    '''
    Make a nicely formatted table showing attributes from a list fo objects
    '''
    # Emit table headers
    for colname in colnames:
        print('{:>10s}'.format(colname), end=' ')
    print()
    for obj in objects:
        # Emit a row of table data
        for colname in colnames:
            print('{:>10s}'.format(str(getattr(obj, colname))), end=' ')
        print()
'''

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3 -i holding.py
>>> portfolio = read_portfolio("/Users/nawlekha/Desktop/pyATS/portfolio.csv")
>>> import table
>>> formatter = table.TextTableFormatter()
>>> table.print_table(portfolio, ['name', 'shares', 'price'], formatter)
      name     shares      price
        AA        100      100.0
       IBM         50       50.0
       CAT        150      150.0
      MSFT        200      200.0
        GR         95       95.0
      MSFT         50       50.0
       IBM        100      100.0
>>>

# to get comma separated values
nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3 -i holding.py
>>> portfolio = read_portfolio("/Users/nawlekha/Desktop/pyATS/portfolio.csv")
>>> import table
>>> formatter = table.print_table(portfolio, ['name', 'shares'], formatter)
Traceback (most recent call last):
  File "", line 1, in
NameError: name 'formatter' is not defined
>>> formatter = table.CSVTableFormatter()
>>> table.print_table(portfolio, ['name', 'shares'], formatter)
name,shares
AA,100
IBM,50
CAT,150
MSFT,200
GR,95
MSFT,50
IBM,100
>>>

# to print html table format
nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3 -i holding.py
>>> portfolio = read_portfolio("/Users/nawlekha/Desktop/pyATS/portfolio.csv")
>>> import table
>>> formatter = table.HTMLTableFormatter()
>>> table.print_table(portfolio, ['name', 'shares'], formatter)

name
shares

AA
100

IBM
50

CAT
150

MSFT
200

GR
95

MSFT
50

IBM
100

>>>

#Advanced Inheritance:
Sometimes it will be tricky when we have more than one init function. will modify table.py to illustrate this

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3 -i holding.py
>>> import table
>>> formatter = table.TextTableFormatter(width=25)
>>> table.print_table(portfolio, ['name', 'shares', 'price'], formatter)
Traceback (most recent call last):
  File "", line 1, in
NameError: name 'portfolio' is not defined
>>> portfolio = read_portfolio('/Users/nawlekha/Desktop/pyATS/portfolio.csv')
>>> formatter = table.TextTableFormatter(width=25)
>>> table.print_table(portfolio, ['name', 'shares', 'price'], formatter)
                     name                    shares                     price
                       AA                       100                     100.0
                      IBM                        50                      50.0
                      CAT                       150                     150.0
                     MSFT                       200                     200.0
                       GR                        95                      95.0
                     MSFT                        50                      50.0
                      IBM                       100                     100.0

## we can take existing class and customise the way we want

>>> class QuotedTextTableFormatter(table.TextTableFormatter):
...     def row(self, rowdata):
...             # Put quotes around all values
...             quoted = ['"{}"'.format(d) for d in rowdata]
...             super().row(quoted)
...
>>> formatter = QuotedTextTableFormatter()
>>> table.print_table(portfolio, ['name', 'shares', 'price'], formatter)
      name     shares      price
      "AA"      "100"    "100.0"
     "IBM"       "50"     "50.0"
     "CAT"      "150"    "150.0"
    "MSFT"      "200"    "200.0"
      "GR"       "95"     "95.0"
    "MSFT"       "50"     "50.0"
     "IBM"      "100"    "100.0"
>>>

#Use of multiple inheritances. Customizing classes and overriding attributes

# just inherit from object no relation to table formatter   
class QuotedMixin(object):
    def row(self, rowdata):
        quoted = ['"{}"'.format(d) for d in rowdata]
        super().row(quoted)

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3 -i holding.py
>>> import table
>>> class Formatter(table.QuotedMixin, table.CSVTableFormatter):
...     pass
...
>>> formatter = Formatter()
>>> portfolio = read_portfolio('/Users/nawlekha/Desktop/pyATS/portfolio.csv')
>>> table.print_table(portfolio, ['name', 'shares', 'price'], formatter)
name,shares,price
"AA","100","100.0"
"IBM","50","50.0"
"CAT","150","150.0"
"MSFT","200","200.0"
"GR","95","95.0"
"MSFT","50","50.0"
"IBM","100","100.0"
>>>
>>>
>>> class Formatter(table.QuotedMixin, table.HTMLTableFormatter):
...     pass
...
>>> formatter = Formatter()
>>> portfolio = read_portfolio('/Users/nawlekha/Desktop/pyATS/portfolio.csv')
>>> table.print_table(portfolio, ['name', 'shares', 'price'], formatter)
name
shares
price

"AA"
"100"
"100.0"

"IBM"
"50"
"50.0"

"CAT"
"150"
"150.0"

"MSFT"
"200"
"200.0"

"GR"
"95"
"95.0"

"MSFT"
"50"
"50.0"

"IBM"
"100"
"100.0"


##Designing of Inheritance:

we can also put print_table function under TableFormatter class in table.py

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3 -i holding.py
>>> import table
>>> formatter = table.TextTableFormatter()
>>> formatter.print_table(portfolio, ['name','shares'])
Traceback (most recent call last):
  File "", line 1, in
NameError: name 'portfolio' is not defined
>>> portfolio = read_portfolio('/Users/nawlekha/Desktop/pyATS/portfolio.csv')
>>> formatter.print_table(portfolio, ['name','shares'])
      name     shares
        AA        100
       IBM         50
       CAT        150
      MSFT        200
        GR         95
      MSFT         50
       IBM        100
>>>

##Defensive programming with Abstract Base Class:

from abc import ABC, abstractmethod
# abc is python standard module library

we can recast TableFormatter so that it can inherit from ABC
class TableFormatter(ABC)

then declare headings and row as abstract method

    @abstractmethod
    def headings(self, headers):
        pass

    @abstractmethod
    def row(self, rowdata):
        pass

Instead of raising NotImplementedError this way we will get the exact error

## we can also do type checking
if not isinstance(formatter, TableFormatter):
raise TypeError(‘formatter must be a TableFormatter’)
        formatter.headings(colnames)
        for obj in objects:
            rowdata = [str(getattr(obj, colname)) for colname in colnames ]
            formatter.row(rowdata)

# if somebody pass wrong class, this will catch that

## How inheritance actually works ?

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3 -i inherit1.py
>>> a = A()
>>> a.spam()
A.spam
Parent.spam
>>>
>>>
>>> exit()
nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$
nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3 -i inherit1.py
>>> b = B()
>>> b.spam()
B.spam
A.spam
Parent.spam
>>>

# how this work is - every class keeps a record of its Parents using method resolution order (mro)

>>> B.__mro__
(, , , )
>>>

# this describes a chain of ancestors

>>> A.__mro__
(, , )
>>>

## lets create C and D class and merge them

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3 -i inherit1.py
>>> a = A()
>>> a.spam()
A.spam
Parent.spam
>>> c = C()
>>> c.spam()
C.spam
Parent.spam
>>>
>>> d = D()
>>> d.spam()
D.spam
Parent.spam

# lets do multiple inheritance

>>> class E(A,C,D):
...     pass
...
>>> e = E()
>>> e.spam()
A.spam
C.spam
D.spam
Parent.spam
>>>

# if we change the order, output will line in that order

>>> class F(D,C,A):
...     pass
...
>>> f = F()
>>> f.spam()
D.spam
C.spam
A.spam
Parent.spam
>>>

Two rules of inheritance
1. Any child class always needs to be checked before parent.
2. If you have more than one parents, then it should be checked in order.

>>> F.__mro__
(, , , , , )
>>>

>>> E.__mro__
(, , , , , )
>>>

use of super here is, it find itself in mro chain and moves one order above.
when we call C it will find itself in mro and moves one order above i.e.

## Python magic methods
>>> x = 42
>>> x + 10
52
>>> x * 10
420

same thing can be achieved as
>>> x.__add__(10)
52
>>> x.__mul__(10)
420
>>>

>>> names = ['IBM', 'YAHOO', 'CISCO']
>>> names[0]
'IBM'
or
>>> names.__getitem__(0)
'IBM'
>>> names[1] = 'FB'
or
>>> names.__setitem__(1, 'FB')
>>>

## use of magic method to implement code
>>> class Point(object):
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
print('Add', other)

>>> p = Point(2,3)
>>> p + 10
Add 10
>>> p + 'hello'
Add hello
>>> p + [1,2,3]
Add [1, 2, 3]
>>> p + (4,5)
Add (4, 5)
>>>

## Making objects printable and debuggable

for debugging purpose, we can use a “repr” method in class
in holding.py file
  # used to control string output
  def __repr__(self):
    return 'Holding({!r}, {!r}, {!r}, {!r})'.format(self.name, self.date, self.shares, self.price)


nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3 -i holding.py
>>> h = Holding('AA', '2017-08-21', 100, 30.36)
>>> h
Holding('AA', '2017-08-21', 100, 30.36)
>>>

This has nice output now.

>>> portfolio = read_portfolio('/Users/nawlekha/Desktop/pyATS/portfolio.csv')
>>> portfolio
[Holding('AA', '06/11/07', 100, 100.0), Holding('IBM', '13/05/17', 50, 50.0), Holding('CAT', '23/09/06', 150, 150.0), Holding('MSFT', '01/02/06', 200, 200.0), Holding('GR', '31/10/06', 95, 95.0), Holding('MSFT', '09/07/06', 50, 50.0), Holding('IBM', '17/05/07', 100, 100.0)]

This is very much descriptive now

#we can change the string representation of object by using special method __repr__ under class

There is another method __str__ which can be used for string conversion or print function

  def __str__(self):
    return '{} share of {} at ${:0.2f}'.format(self.shares, self.name, self.price)

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3 -i holding.py
>>> h = Holding('AA', '2017-08-21', 100, 30.36)
>>> print(h)
100 share of AA at $30.36

## without print
>>> h
Holding('AA', '2017-08-21', 100, 30.36)
>>>

>>> str(h) ## string method output
'100 share of AA at $30.36'
>>> repr(h) ## represents method output
"Holding('AA', '2017-08-21', 100, 30.36)"
>>>

>>>
>>> from datetime import date
>>> d = date(2012, 12, 21)
>>> print(d)
2012-12-21
>>> d
datetime.date(2012, 12, 21)
>>> repr(d)
'datetime.date(2012, 12, 21)'
>>> str(d)
'2012-12-21'
>>>

## Making a custom container object

python special method
for size we can have __len__ method
to get array indexing we can have __getitem__ method
for for loop, we can have __iter__

def __len__(self):
return len(self.holdings)

def __getitem__(self, n):
return self.holdings[n]

def __iter__(self):
return self.holdings.__iter__()

Every python operation is mapped to some method name

using this if we execute holding.py we can do length, indexing and for loop on portfolio

we can also do indexing by name by modifying __getitem__

def __getitem__(self, n):
if isinstance(n, str):
return [h for h in self.holdings if h.name == n]
else:
return self.holdings[n]

portfolio[‘IBM’]

## making a custom context manager
# normal way of using resource. use it and release it.

>>> import threading
>>> lock = threading.Lock()
>>> lock.acquire()
True
>>> print("Use the lock")
Use the lock
>>> lock.release()
>>>

## This might be a problem since somebody might forgot to close file or release resource. One solution is use with statement

>>> with lock:
print("Use the lock")

Use the lock
>>>

Lets create a Manager class

class Manager(object):
    def __enter__(self):
        print('Entering')
        return 'some value'

    def __exit__(self, ty, val, tb):
        print('Exiting')
        print(ty, val, tb)

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3
Python 3.6.1 (v3.6.1:69c0db5050, Mar 21 2017, 01:21:04)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import manager
>>> m = manager.Manager()
>>> m

>>>
>>> with m:
...     print('Hello world')
...
Entering
Hello world
Exiting
None None None
>>>


>>> with m:
...     print('hello world')
...     for i in range(3):
...             print(i)
...
Entering
hello world
0
1
2
Exiting
None None None

##Here exit method is kind of used for cleanup purpose

>>> with m as val:
...     print('val =', val)
...
Entering
val = some value
Exiting
None None Non

>>> with m:
...     print('hello world')
...     int('n/a') # some bad input
...
Entering
hello world
Exiting
invalid literal for int() with base 10: 'n/a'
Traceback (most recent call last):
  File "", line 3, in
ValueError: invalid literal for int() with base 10: 'n/a'

## Encapsulation (Owning the dot)
## Instance representation, attribute access and naming convention


nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3 -i holding.py
>>> h = Holding('AA', '2017-08-22', 100, 32.2)
>>> h
Holding('AA', '2017-08-22', 100, 32.2)
>>> h.name
'AA'
>>> h.shares
100
>>> h.price
32.2
>>>

## Every instance we make is a layer on top of python dict

>>> h.__dict__
{'name': 'AA', 'date': '2017-08-22', 'shares': 100, 'price': 32.2}
>>> h.__dict__['name']
'AA'
>>> h.__dict__['yow'] = 42
>>> h.yow
42
>>>

## Every instance we make gets its own dictionary

>>> g = Holding('IBM', '2008-04-1', 50, 91.1)
>>> g.__dict__
{'name': 'IBM', 'date': '2008-04-1', 'shares': 50, 'price': 91.1}
>>>

## “.” operation is actually working out of dictionary

>>> del h.yow
>>> h.__dict__
{'name': 'AA', 'date': '2017-08-22', 'shares': 100, 'price': 32.2}
>>>
>>>

>>> h.cost()
3220.0000000000005
>>> h.sell(25)
>>> h.__dict__
{'name': 'AA', 'date': '2017-08-22', 'shares': 75, 'price': 32.2}
>>>

## we don’t see cost and sell in dictionary because those functions are part of the class

>>> Holding

>>> h.__class__

>>> Holding.__dict__
mappingproxy({'__module__': '__main__', '__init__': , '__repr__': , '__str__': , 'cost': , 'sell': , '__dict__': , '__weakref__': , '__doc__': None})
>>>

## Holding class has its own dictionary. Here we can see cost and sell method

>>> Holding.__dict__['cost']

>>> Holding.__dict__['cost'](h)
2415.0
>>>

Naming conventions:
use of single and double __ on attributes

>>> class Spam(object):
...     def __init__(self, value):
...             self._value = value # "Private" or some internal implementation
...
>>> s = Spam(42)
>>> s._value
42
>>> s._value = 45
>>>

we can access and change it but we should not do that

## Managed attributes with properties
use Holding class. lets not allow anyone to set some value to price

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3 -i holding.py
>>> h = Holding('AA', '2017-08-10', 100, 32.2)
>>> h.sahres
Traceback (most recent call last):
  File "", line 1, in
AttributeError: 'Holding' object has no attribute 'sahres'
>>> h.shares
100
>>> h.price
32.2
>>> h.price = 'a lot'
>>>

>>> h.price = '38.4'
>>> h.cost()
'38.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.4'
>>> 100 * 38.4
3840.0
>>> 100 * '38.4'
'38.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.438.4'
>>>

Let's lock this code and not allow anyone to do this change

  def get_price(self):
    return self.price

  def set_price(self, newprice):
    if not isinstance(newprice, float):
      raise TypeError('Expected float')
    self.price = newprice

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3 -i holding.py
>>>  h = Holding('AA', '2017-08-10', 100, 32.2)
  File "", line 1
    h = Holding('AA', '2017-08-10', 100, 32.2)
    ^
IndentationError: unexpected indent
>>> h = Holding('AA', '2017-08-10', 100, 32.2)
>>> h.get_price()
32.2
>>> h.set_price('142.3')
Traceback (most recent call last):
  File "", line 1, in
  File "holding.py", line 13, in set_price
    raise TypeError('Expected float')
TypeError: Expected float
>>> h.set_price(142.3)
>>>

One solution is hide the price using private attribute

Either we have to do _price everywhere in file . To avoid that we can use price as property

  @property
  def get_price(self):
    return self._price

  @price.setter
  def set_price(self, newprice):
    if not isinstance(newprice, float):
      raise TypeError('Expected float')
    self._price = newprice

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3 -i holding.py
>>> h = Holding('AA', '2017-08-10', 100, 32.2)
>>> h.price
32.2
>>> h.shares
100
>>> h.date
'2017-08-10'
>>> h.name
'AA'
>>> h.price = 45.23
>>> h.price = '45.23'
Traceback (most recent call last):
  File "", line 1, in
  File "holding.py", line 15, in price
    raise TypeError('Expected float')
TypeError: Expected float
>>>

##if we set wrong value it will generate a error. It’s a type checking
## With @property we can do different types of checking
## other thing is we can use it to avoid usage problems
## cost is a method. To call it we have to put parenthesis


>>> h.cost()
4523.0
>>> h.cost

>>>

with the use of property we can call cost as h.cost

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3 -i holding.py
>>> h = Holding('AA', '2017-08-10', 100, 32.2)
>>> h.cost
3220.0000000000005
>>>

Take attributes and hide them behind property. This gives us way of locking down internals of the object

## Managed attributes with Descriptors

Typing property in every single code for type checking is tedious.
To tackle this problem, lets use __repr__ also we should know how “.” intercept property

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3 -i holding.py
>>> h = Holding('AA', '2017-08-10', 100, 32.2)
>>> h.__class__

>>> h.__class__.__dict__['shares']

>>> p = _
>>> p

>>> hasattr(p, '__get__')
True
>>> p.__get__(h)
100
>>> h.shares = 100
>>> h.shares = '100'
Traceback (most recent call last):
  File "", line 1, in
  File "holding.py", line 27, in shares
    raise TypeError('Expected int')
TypeError: Expected int
>>> p = h.__class__.__dict__['shares']
>>> p

>>>


>>>
>>> hasattr(p, '__set__')
True
>>> p.__set__(h, '100')
Traceback (most recent call last):
  File "", line 1, in
  File "holding.py", line 27, in shares
    raise TypeError('Expected int')
TypeError: Expected int
>>>

## Descriptor is a object that implement “.” (Encapsulation)

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3 -i validate.py
>>> p = Point(2,3)
>>> p.x
2
>>> p.y
3
>>> p.__dict__
{'x': 2, 'y': 3}
>>> p.x = 45
>>> p.y = 23
>>> p.__dict__
{'x': 45, 'y': 23}
>>> p.x = 'a lot' # set bad value
Traceback (most recent call last):
  File "", line 1, in
  File "validate.py", line 11, in __set__
    raise TypeError('Expected int')
TypeError: Expected int
>>> p.x = 4.5
Traceback (most recent call last):
  File "", line 1, in
  File "validate.py", line 11, in __set__
    raise TypeError('Expected int')
TypeError: Expected int
>>> p.z = 45
>>> p.z = 'a lot' # this only cares about x and y
>>>


nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3 -i validate.py
>>>  h = Holding('AA', '2017-08-10', 100, 32.2)
  File "", line 1
    h = Holding('AA', '2017-08-10', 100, 32.2)
    ^
IndentationError: unexpected indent
>>> h = Holding('AA', '2017-08-10', 100, 32.2)
>>> h.shares
100
>>> h.price = '32.2'
Traceback (most recent call last):
  File "", line 1, in
  File "validate.py", line 25, in __set__
    raise TypeError('Expected int')
TypeError: Expected int
>>> h.shares = 'a lot'
Traceback (most recent call last):
  File "", line 1, in
  File "validate.py", line 11, in __set__
    raise TypeError('Expected int')
TypeError: Expected int
>>>

## Object Wrappers and Proxies

other way to take ownership of .
builtin attributes like getattribute

>>> h.__getattribute__

>>> h.__getattribute__('name')
'AA'
>>> h.__getattribute__('shares')
100
>>>

>>> class Spam(object):
...     def __getattribute__(self, name):
...             print('Getting:', name)
...
>>> s = Spam()
>>> s.x
Getting: x
>>> s.spam
Getting: spam
>>> s.foo
Getting: foo
>>>

>>> h.shares = 100
or
>>> h.__setattr__('shares', 100)

>>> class Readonly(object):
...     def __init__(self, obj):
...             self._obj = obj
...     def __getattr__(self, name):
...             return getattr(self._obj, name)
...     def __setattr__(self, name, value):
...             if name == '_obj':
...                     super().__setattr__(name, value)
...             else:
...                     raise AttributeError('Read only')
...
>>> h
<__main__ .holding="" 0x102a85f98="" at="" object="">
>>> p = Readonly(h)
>>> p.name
'AA'
>>> p.shares
100

>>> p.shares = 50
Traceback (most recent call last):
  File "", line 1, in
  File "", line 10, in __setattr__
AttributeError: Read only
>>>

## Functions as objects:

Whatever operation we can do on variable object same operations we can do on functions.

>> x = 10
>>> y = 'hello world'
>>> items = [10,20]
>>>
>>> def greeting(name):
print('Hello', name)

>>> greeting('nawraj')
Hello nawraj
>>> greeting

>>> g = greeting
>>> g('nawraj')
Hello nawraj
>>> items
[10, 20]
>>> items.append(greeting)
>>> items
[10, 20, ]
>>> items[2]

>>> items[2]('nawraj')
Hello nawraj
>>>

>>> import time
>>> def after(seconds, func):
time.sleep(seconds)
func()

>>> def hello():
print('Hello World')

>>> after(5, hello) ## issue here is, there is no difference in calling variable and function
Hello World

>>> names = ['dave', 'Thomas', 'Lewis', 'paula']
>>> names.sort(key=lambda name: name.upper())
>>> names
['dave', 'Lewis', 'paula', 'Thomas']
>>> a = lambda x: 10 * x
>>> a(10)
100
>>> greeting('nawraj')
Hello nawraj
>>> after(5, greeting)
Traceback (most recent call last):
  File "", line 1, in
    after(5, greeting)
  File "", line 3, in after
    func()
TypeError: greeting() missing 1 required positional argument: 'name'

>>> ## we can solve it using lambda function
>>> after(5, lambda: greeting('nawraj'))  ## passing function as argument
Hello nawraj
>>>

>>> def add(x,y):
def do_add():
print('Adding {} + {} -> {}'.format(x,y,x+y))
return x+y
return do_add

>>> a = add(2,3)
>>> a
.do_add at 0x105604950>
>>> a()
Adding 2 + 3 -> 5
5
>>> ## this is returing a function
>>> b = add('hello', 'world')
>>> b()
Adding hello + world -> helloworld
'helloworld'
>>> x
10
>>> y
'hello world'

## Even after deleting x and y we are able to see the output. Actually add will get the variables and pass it to do_add

>>> del x
>>> del y
>>> x
Traceback (most recent call last):
  File "", line 1, in
    x
NameError: name 'x' is not defined
>>> y
Traceback (most recent call last):
  File "", line 1, in
    y
NameError: name 'y' is not defined
>>> a()
Adding 2 + 3 -> 5
5
>>> b()
Adding hello + world -> helloworld
'helloworld'
>>>

##  Generating code with Closures
nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3 -i typedproperty.py
>>> h = Holding('AA', '2017-08-10', 100, 32.2)
>>> h.shares
100
>>> h.price
32.2
>>> h.price = '32.2'
Traceback (most recent call last):
  File "", line 1, in
  File "typedproperty.py", line 12, in prop
    expected_type))
TypeError: Expected
>>> h.shares = '100'
Traceback (most recent call last):
  File "", line 1, in
  File "typedproperty.py", line 12, in prop
    expected_type))
TypeError: Expected
>>>

## Metaprogramming and Decorators
>>> def func(x,y,z):
print(x,y,z)

>>> func(1,2,3)
1 2 3
>>> func(1,z=2,y=3)
1 3 2
>>> ## These are two ways of passing arguments to function
>>>
>>> def func(x, *args):
print(x)
print(args)

>>> ## This will take any extra number of positional arguments
>>>
>>> ## atleast one argument should be passed
>>>
>>> func(1)
1
()
>>> fun(1,2,3,4,5)
Traceback (most recent call last):
  File "", line 1, in
    fun(1,2,3,4,5)
NameError: name 'fun' is not defined
>>> func(1,2,3,4,5)
1
(2, 3, 4, 5)
>>> ## all extra arguments goes to arg tuple
>>>
>>> def func(x, **kwargs):
print(x)
print(kwargs)

>>> ## This will take atleast one arguments and any other extra keyword arguments
>>>
>>> func(1)
1
{}
>>> func(1, xmin=10, xmax=20, color='red')
1
{'xmin': 10, 'xmax': 20, 'color': 'red'}
>>>
>>>
>>> ## we can also have fun having both args and kwargs. This will take any combination whatsoever
>>> def func(*args, **kwargs):
print(args)
print(kwargs)

>>> func()
()
{}
>>> func(1,2,3,4)
(1, 2, 3, 4)
{}
>>> func(1,2,x=20,y=3)
(1, 2)
{'x': 20, 'y': 3}
>>>
>>>
>>> argument goes to tuple and kwargs goes to dict
SyntaxError: invalid syntax
>>>
>>> ## we can also call a function using tuple and dictionary
>>>
>>> def func(a,b,c,d):
print(a,b,c,d)

>>> args = (1,2)
>>> kwargs = {'c':3,'d':-1}
>>> func(*args, **kwargs)
1 2 3 -1
>>>
>>> data = (1,2,3,4)
>>> func(*data)
1 2 3 4
>>>
>>>
>>>
>>> def add(x,y):
return x+y

>>> def add_wrapper(*args, **kwargs):
print('Wrapping!')
return add(*args, **kwargs)

>>> add_wrapper(2,3)
Wrapping!
5
>>> add_wrapper(y=3,x=2)
Wrapping!
5
>>> add_wrapper(2, y=3)
Wrapping!
5

## Don’t repeat yourself - Introducing Decorators
Let’s have file sample.py with below code

def add(x,y):
    print('Calling add')
    return x+y

def sub(x,y):
    print('Calling sub')
    return x-y

def mul(x,y):
    print('Calling mul')
    return x*y

one irritating thing about this code is ..we have to add print for logging in every function

## Instead of this we can generalise the code. Create logcall.py

def logged(func):
    #Idea here is: Give me function, I'll put logging around it

    def wrapper(*args, **kwargs):
        print('Calling', func.__name__)
        return func(*args, **kwargs)

    return wrapper

>>> def add(x,y):
return x+y

>>> import logcall
>>> add = logcall.logged(add)
>>> add
.wrapper at 0x100662e18>
>>> add(2,3)
Calling add
5
>>> def sub(x,y)
SyntaxError: invalid syntax
>>> def sub(x,y):
return x-y

>>> sub = logcall.logged(sub)
>>> sub(4,5)
Calling sub
-1
>>>

## by this way, we don’t have to change logging message in every function
## Now lets modify sample.py using decorators


from logical import logged

@logged
def add(x,y):
    print('Calling add')
    return x+y
or
def add(x,y):
    print('Calling add')
    return x+y
add = logged(add)

@logged
def sub(x,y):
    print('Calling sub')
    return x-y

@logged
def mul(x,y):
    print('Calling mul')
    return x*y

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3
Python 3.6.1 (v3.6.1:69c0db5050, Mar 21 2017, 01:21:04)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import sample
Adding logging to add
Adding logging to sub
Adding logging to mul
>>>

>>> sample.add(2,3)
Calling add
Calling add
5
>>> sample.mul(4,5)
Calling mul
Calling mul
20
>>> sample.sub(2,3)
Calling sub
Calling sub
-1
>>>

nawlekha@NAWLEKHA-M-Q1GZ:~/Desktop/pyATS$ python3
Python 3.6.1 (v3.6.1:69c0db5050, Mar 21 2017, 01:21:04)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import sample
Adding logging to add
Adding logging to sub
Adding logging to mul
>>> sample.add

>>> sample.sub

>>> help(sample.add)

## if we want to use decorator, we will use wrapper around a function

## Class Decorators

## Metaclasses

>>> x = 42
>>> y = 'hello'
>>> z = 3.5
>>> type(x)

>>> type(y)

>>> type(z)

>>>
>>> x.__class__

>>> y.__class__

>>> z.__class__

>>>
>>> class Point(object):
def __init__(self, x, y):
self.x = x
self.y = y
def move(self, dx, dy):
self.x += dx
self.y += dy

>>> p = Point(2,3)
>>> type(p)

>>> ## every object we make has type associated with it
>>>
>>> ## same is with python inbuilt object as well
>>>
>>> a = int()
>>> a
0
>>> b = float()
>>> b
0.0
>>>
>>> # every object in python is kind of class
>>> Point

>>> int

>>> float

>>> str

>>> x
42
>>> type(x)

>>>
>>>
>>> type(Point)

>>>
>>> type(float)

>>>
>>> type

>>>
>>> ## type is a class which creates other class
>>>
>>> ## let's see what all class has
>>> name = 'Point'
>>> bases = (object,) # 1-tuple

>>> def __init__(self, x, y):
self.x = x
self.y = y

>>> def move(self, dx, dy):
self.x += dx
self.y += dy

>>> methods = {
'__init__': __init__,
'move': move
}
>>> name
'Point'
>>> bases
(,)
>>> methods
{'__init__': , 'move': }
>>> type

>>>
>>> Point = type(name, bases, methods)
>>> Point

>>> p = Point(2,3)
>>> p.x
2
>>> p.x = 20
>>>
>>>
>>> ## we can modify type class
>>> class mytype(type):
def __new__(meta, clsname, bases, methods):
print('Creating:', clsname)
print('Bases:', bases)
print('Methods:', list(methods))
return super().__new__(meta, clsname, bases, methods)

>>> Point = mytype(name, bases, methods)
Creating: Point
Bases: (,)
Methods: ['__init__', 'move']
>>> p = Point(2,3)
>>> p.x
2
>>> p.y
3
>>> p.move(2,3)
>>>
>>>
>>> class Point(metaclass=mytype):
def __init__(self, x, y):
self.x = x
self.y = y
def move(self, dx, dy):
self.x += dx
self.y += dy

Creating: Point
Bases: ()
Methods: ['__module__', '__qualname__', '__init__', 'move']
>>>
>>>
>>> class NewPoint(Point):
def yow(self):
print('Yow')

Creating: NewPoint
Bases: (,)
Methods: ['__module__', '__qualname__', 'yow']
>>>
>>> class MyPoint(NewPoint):
def spam(self):
pass

Creating: MyPoint
Bases: (,)
Methods: ['__module__', '__qualname__', 'spam']
>>>
>>> class Base(metaclass=mytype):
pass

Creating: Base
Bases: ()
Methods: ['__module__', '__qualname__']
>>> class A(Base):
def yow(self):
pass

Creating: A
Bases: (,)
Methods: ['__module__', '__qualname__', 'yow']

Iterators and Generators:
=====================
e.g. for

>>> names = ['Cisco', 'IBM', 'YAHOO']
>>> for name in names:
print(name)

Cisco
IBM
YAHOO
>>> ## there is a special method called __iter__() which does this
>>>
>>> it = names.__iter__()
>>> it

>>> ## this will return iterator object
>>> ## then python will call repetadely next on it
>>> it.__next__()
'Cisco'
>>> it.__nect__()
Traceback (most recent call last):
  File "", line 1, in
    it.__nect__()
AttributeError: 'list_iterator' object has no attribute '__nect__'
>>> it.__next__()
'IBM'
>>> it.__next__()
'YAHOO'
>>> ## this happens till we get stopIteration
>>> it.__next__()
Traceback (most recent call last):
  File "", line 1, in
    it.__next__()
StopIteration
>>>
>>>
>>> f = open('/Users/nawlekha/Desktop/pyATS/portfolio.csv','r')
>>> it = f.__iter__()
>>> it
<_io .textiowrapper="" encoding="UTF-8" mode="r" name="/Users/nawlekha/Desktop/pyATS/portfolio.csv">
>>> it.__next__()
'name,date,shares,price\n'
>>>
>>> it.__next__()
'AA,06/11/07,100,32.2\n'
>>> it.__next__()
'IBM,13/05/17,50,91.1\n'
>>> it.__next__()
'CAT,23/09/06,150,83.44\n'
>>> it.__next__()
'MSFT,01/02/06,200,51.23\n'
>>> it.__next__()
'GR,31/10/06,95,40.3\n'
>>> it.__next__()
'MSFT,09/07/06,50,65.2\n'
>>> it.__next__()
'IBM,17/05/07,100,70.04'
>>> it.__next__()
Traceback (most recent call last):
  File "", line 1, in
    it.__next__()
StopIteration
>>>

### customization of iterators


>>> ## we can do some greping with python
>>> def grep(pattern, filename):
with open(filename) as f:
for line in f:
if pattern in line:
yield line

>>> for line in grep('IBM', '/Users/nawlekha/Desktop/pyATS/portfolio.csv'):
print(line)

IBM,13/05/17,50,91.1

IBM,17/05/07,100,70.04
>>>
>>>
>>> ## Another way to define generators
>>> nums = [1,2,3,4,5,6]
>>> squares = [x*x for x in nums]
>>> squares
[1, 4, 9, 16, 25, 36]
>>> squares = (x*x for x in nums)
>>> squares
at 0x1055ff468>
>>> for x in squares:
print(x)


1
4
9
16
25
36
>>> sum(x*x for x in nums)
91
>>> f = open('/Users/nawlekha/Desktop/pyATS/portfolio.csv')
>>> ibm = (line for line in f if 'IBM' in line)
>>> for line in ibm:
print(line)

IBM,13/05/17,50,91.1

IBM,17/05/07,100,70.04
>>>
>>>
>>> ## Generators are one time use
>>> c = countdown(5)
Traceback (most recent call last):
  File "", line 1, in
    c = countdown(5)
NameError: name 'countdown' is not defined
>>> ## to use generator repeatedly we can use class

>>> class Countdown(object):
def __init__(self, start):
self.start = start
def __iter__(self):
n = self.start
while n > 0:
yield n
n -= 1

>>> c = Countdown(5)
>>> for x in c:
print(x)


5
4
3
2
1
>>> for x in c:
print(x)


5
4
3
2
1
>>>

### Watching a real-time data source with a generator

### Processing pipelines and workflows

## Coroutines. Calling coroutines with async/wait

>>> async def greeting(name):
return 'Hello' + name

>>> g = greeting('Hello')
>>> g

>>> ## co routine is a fucntion that works under the management is some code
>>> import asyncio
>>> loop = asyncio.get_event_loop()
>>> loop.run_until_complete(g)
'HelloHello'
>>>
>>>
>>>
>>> async def hello():
names = ['Nawraj','Lekhak']
for name in names:
print(await greeting(name))

>>> h = hello()
>>> h

>>> loop.run_until_complete(h)
HelloNawraj
HelloLekhak
>>>
>>>
>>> async def fib(n):
if n <= 2:
return 1
else:
return await fib(n-1) + await fib(n-2)

>>> f = fib(10)
>>> f

>>> loop.run_until_complete(f)
55
>>>
>>>
>>>