An Informal introduction to Python

https://docs.python.org/3/tutorial/introduction.html

Python is an easy to learn, powerful programming language. It has efficient high-level data structures and a simple but effective approach to object-oriented programming. Python’s elegant syntax and dynamic typing, together with its interpreted nature, make it an ideal language for scripting and rapid application development in many areas on most platforms.

Classic vs Floor division

print(19/5)  # classic division returns float
print(52//3)  # floor division discards the fractional part
3.8
17

To calculate powers

# 2^5
print(2**5)
32

In interactive mode, the last printed expression is assigned to the variable _

Round a real number up to 2 places after decimal

k = 12.03635
print(round(k, 2))
12.04

type() method

h = "Kolkata"
print(type(h))
i = 56
print(type(i))
<class 'str'>
<class 'int'>

Strings

  • \ can be used to escape quotes
  • can be also be enclosed within other pair of quotes
  • to print raw strings, use r

Raw string

print(r"C:\nebula\MachineLearning")
# without it, \n escape sequence gets triggered
print("C:\nebula\MachineLearning")
C:\nebula\MachineLearning
C:
ebula\MachineLearning

String literals can span multiple lines

# Prevent automatic inclusion of end of lines by using \ operator
print("""\
asdad
awewer
bnghjny
it rty    dsrfgtr t
er    sdefsdfg    dggh""")
asdad
awewer
bnghjny
it rty    dsrfgtr t
er    sdefsdfg    dggh

Strings are 0-based indexed

str = "David Attenborough"
print(str)
print(str[2] + str[9])
print(str[-2]) # starts counting from the right
# -2 is the second last character
# Note that since zero cannot be negative, negative indices start from -1.
David Attenborough
ve
g

Slicing strings

str = 'Jekyll'
print(str[1:]) #inclusive of 1th element
print(str[2:]) 
print(str[:3]) # exclusive of the 3rd element
print(str[:2] + str[4:])
print(str[-2:])
ekyll
kyll
Jek
Jell
ll
s = "jupyter"
print(s[2:4])
py
  • Like Java, Python strings are immutable.

Length of string

# str = "Jekyll"
print(len(str))
6
str1 = "Data Structures"
str2 = "Algorithms"
str3 = "operating system"
print(str1.lower())
print(str2.upper())
print(str3.capitalize())
print(str1.endswith("Structures"))
print(str2.endswith("thms"))
print(str3.startswith("oper"))
data structures
ALGORITHMS
Operating system
True
True
True

Reverse a string

# Python string library has no reverse() method
def reverse(s):
  temp = ""
  for i in s:
    temp = i + temp
  return temp

s = "Kerala"
print(reverse(s))
alareK
# hard way of reversing strings
# converting the string to list, swapping elements in the list
# and then converting back the list to string
def rev(s):
    s = list(s)
    start = 0
    end = len(s)-1
    while(start < end):
        s[start], s[end] = s[end], s[start] #swapping
        start = start + 1
        end = end - 1
    return "".join(s)

print(rev("mayukh"))
hkuyam
# Using recursion
def reverse1(s):
  if len(s) == 0:
    return s
  else:
    return reverse(s[1:]) + s[0]

s = "Kolkata"
print(reverse1(s))
atakloK
# Using extended slice syntax
# Easiest way to reverse a string
def reverse3(s):
  return s[::-1]

s = "Quadilateral"
print(reverse3(s))
laretalidauQ
# Using reversed
def reverse4(s):
  return "".join(reversed(s))

s = "Gradle"
print(reverse4(s))
eldarG

Split a string

line = "Brian Karnighan's algorithm is the most efficient way to count set bits."
print(line.split()) # No delimiter takes whitespace as a delimiter implicitly
['Brian', "Karnighan's", 'algorithm', 'is', 'the', 'most', 'efficient', 'way', 'to', 'count', 'set', 'bits.']
word = "dock:for:ipad"
print(word.split(":"))
print(word.split(":", 1)) # maxsplit: 1
['dock', 'for', 'ipad']
['dock', 'for:ipad']

String to integer

s = "101011"
l = int(s) #typecasting string to int
print(l)
101011

Printing out multiples of strings

print("Yes"*5 + "No"*2)
YesYesYesYesYesNoNo

List to string

a = ["I", "am", "here"]
print(" ".join(a))
I am here

Check anagram or not

  • Counter is a subclass of dictionary object. The Counter() function in collections module takes an iterable or a mapping as the argument and returns a Dictionary. In this dictionary, a key is an element in the iterable or the mapping and value is the number of times that element exists in the iterable or the mapping.
  • The Counter() function can take a dictionary as an argument. In this dictionary, the value of a key should be the 'count' of that key.
from collections import Counter 
def is_anagram(str1, str2): 
 return Counter(str1) == Counter(str2) 

word1 = input("Enter first word:")
word2 = input("Enter second word:")
print(is_anagram(word1, word2))
Enter first word:geek
Enter second word:kgee
True

Flatten lists

import itertools
a = [[1, 2], [3, 4], [5, 6]]
b = list(itertools.chain.from_iterable(a))

print(b)
[1, 2, 3, 4, 5, 6]

Lists

# This is a list
# Like strings, lists can be indexed and sliced
veggies = ["potato", "carrot", "brinjal"]
print(veggies)
print(veggies[2])
print(veggies[-1])
# all slice operations return a new list(a copy) containing the requested elements
print(veggies[1:2])
# lists supports concatenation
# unlike strings, lists are mutable
# use append() method to add elements to the end of a list
veggies.append("ladyfinger")
print(veggies)
# assignment to slices is also possible
veggies[2:] = ["beetroot", "ginger"]
print(veggies)
# length of list
print(len(veggies))
# we can nest lists too
x = [['a', 'b'], ['k', 'l']]
print(x, "Length = ", len(x))
['potato', 'carrot', 'brinjal']
brinjal
brinjal
['carrot']
['potato', 'carrot', 'brinjal', 'ladyfinger']
['potato', 'carrot', 'beetroot', 'ginger']
4
[['a', 'b'], ['k', 'l']] Length =  2

Fibonacci program

# Prints Fibonacci series upto Nth term where N < k
def fibonacci(k):
  first, second = 0, 1 # multiple assignment
  while first < k:
    print(first, end=', ')
    first, second = second, first+second

# invoke function
fibonacci(20)
0, 1, 1, 2, 3, 5, 8, 13, 
# Prints Fibonacci series upto Nth term
def fibo(n):
  first, second = 0, 1
  l = [] #empty list to store fibonacci numbers
  for i in range(n):
    #print(first, end=" ")
    l.append(first)
    first, second = second, first+second
  print(l)
  print("Length of the list: ", len(l))

fibo(20)
    
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181]
Length of the list:  20
# Prints Nth fibonacci number
def nthfibo(n):
  first, second = 0, 1
  for i in range(n-1):
    first, second = second, first+second
  print("%dth term is %d" %(n, first))
  # formatted output using string modulo operator

nthfibo(20)
20th term is 4181

If statement

n = int(input("Enter a number: "))
if n<0:
  print("It is a negative number.")
elif n==0:
  print("It is a zero.")
else:
  print("It is a positive number.") 
Enter a number: -6
It is a negative number.

Switch or case statement

Unlike other languages, Python does not have this feature. Python encourages to use if and elif instead.

We can do something similar to switch-case by using dictionary mapping. Read here: https://www.geeksforgeeks.org/switch-case-in-python-replacement/

For statement

# Python’s for statement iterates over the items of 
# any sequence (a list or a string), in the order 
# that they appear in the sequence. 
# similar to for each loop in Java
word = ['cat', 'dog', 'tiger', 'lion']
for w in word:
  w = w.capitalize()

print(word)
['cat', 'dog', 'tiger', 'lion']

while loop

i = 10
while i > 0:
  print(i, end=" ")
  i = i - 1
10 9 8 7 6 5 4 3 2 1 

Find second largest element in array

import sys

t = int(input())
while t > 0:
    n = int(input())
    arr = list(map(int, input().split()))
    first = second = -sys.maxsize # maximum negative integer
    for i in range(len(arr)):
        if arr[i] > first:
            second = first
            first = arr[i]
        elif arr[i] > second and arr[i] != first:
            second = arr[i]
    print(second)
    t = t - 1
1
5
56 2 3 99 6
56

sys.maxsize and -sys.maxsize

  • An integer giving the maximum value a variable of type Py_ssize_t can take. It’s usually 2^31 - 1 on a 32-bit platform and 2^63 - 1 on a 64-bit platform.

range() method

# range() method generated arithmetic progressions.
# and proves useful in case of for loop
for i in range(10):
  print(i, end=" ")
0 1 2 3 4 5 6 7 8 9 
# range(a, b, c)
# a = start
# b = stop
# c = step (common difference of AP)
for i in range(5, 30, 5):
  print(i, end=" ")
print()
for i in range(-2, -40, -6):
  print(i, end=" ")
5 10 15 20 25 
-2 -8 -14 -20 -26 -32 -38 
# iterating over the indices of a sequence using range() and len()
str = "software engineering"
for i in range(len(str)):
  print(str[i], end=" ")
s o f t w a r e   e n g i n e e r i n g 

list() method

# we can create lists from iterables using this method
list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# we can create a list from string
list("maharashtra")
['m', 'a', 'h', 'a', 'r', 'a', 's', 'h', 't', 'r', 'a']

break statement and else clause on loop

The break statement, like in C, breaks out of the innermost enclosing for or while loop.

Loop statements may have an else clause; it is executed when the loop terminates through exhaustion of the list (with for) or when the condition becomes false (with while), but not when the loop is terminated by a break statement.

Find primes upto N

def primes(n):
  for i in range(1, n+1): # 1 to 15
    for j in range(2, i):
      if i % j == 0:
        print(i, "equals", j, "*", i//j)
        break
    else:
      # loop fell through without finding a factor
      # else belongs to the for loop
      # loop's else clause runs when no break occurs
      # similar to try (in exception handling), when no exception occurs
      # else clause of try executes
      print("%d is a prime number" %(i))

primes(15)
1 is a prime number
2 is a prime number
3 is a prime number
4 equals 2 * 2
5 is a prime number
6 equals 2 * 3
7 is a prime number
8 equals 2 * 4
9 equals 3 * 3
10 equals 2 * 5
11 is a prime number
12 equals 2 * 6
13 is a prime number
14 equals 2 * 7
15 equals 3 * 5

Find primes within a range

  • we can do it efficiently if we use sieve of eratosthenes method
def prime1(l, u):
  for i in range(l, u+1):
    count = 0
    for j in range(1, i+1):
      if i%j == 0:
        count+=1
    
    if count == 2:
      print(i, end=" ")

prime1(5, 30)
5 7 11 13 17 19 23 29 

Perfect number

Perfect number, a positive integer that is equal to the sum of its proper divisors.

import math

def perfect(n):
  sum = 1
  k = int(math.ceil(math.sqrt(n+1)))
  for i in range(2, k):
    if n%i == 0:
      sum+=(i+(n/i))
    
  if sum == n:
    print("{0} is a perfect number.".format(n))
  else:
    print("{0} is not a perfect number.".format(n))

perfect(6)
perfect(496)
perfect(494)
6 is a perfect number.
496 is a perfect number.
494 is not a perfect number.

Perfect number within range

import math

def perfect1(l, u):
  # corner case
  if l == 1:
    l = 2

  for i in range(l, u+1):
    sum = 1
    k = int(math.ceil(math.sqrt(i+1)))
    for j in range(2, k):
      if i%j == 0:
        sum+=(j + (i/j))
    if sum == i:
      print(i, end=" ")

  print()

perfect1(1, 10000)
perfect1(20, 90000)
6 28 496 8128 
28 496 8128 

Strong number

Strong number is a special number whose sum of factorial of digits is equal to the original number.

def factorial(n):
  if n <= 0:
    return 1
  else:
    return n * factorial(n-1)

def strong(n):
  temp = n
  sum = 0
  while n > 0:
      f = factorial(n%10)
      sum += f
      n = n // 10
  
  if sum == temp:
    print("It's a strong number.")
  else:
    print("It's not a strong number.")

strong(145)
strong(2)
strong(56)
strong(21)
It's a strong number.
It's a strong number.
It's not a strong number.
It's not a strong number.

continue statement

Like in C, continue statement continues with the next iteration of the loop.

pass statement

Defining a function

  • The keyword def introduces a function definition. It must be followed by the function name and the parenthesized list of formal parameters. The statements that form the body of the function start at the next line, and must be indented.
  • The first statement of the function body can optionally be a string literal; this string literal is the function’s documentation string, or docstring like """This function parses JSON"""
  • The execution of a function introduces a new symbol table used for the local variables of the function.
  • The actual parameters (arguments) to a function call are introduced in the local symbol table of the called function when it is called; thus, arguments are passed using call by value (where the value is always an object reference, not the value of the object).
  • In fact, even functions without a return statement do return a value, albeit a rather boring one. This value is called None (it’s a built-in name). Writing the value None is normally suppressed by the interpreter if it would be the only value written.

return statement

def sublist(fruits, m, n):
  return fruits[m:n]

fruits = ["mango", "apple", "guave", "grapes", "banana", "pineapple", "cucumber", "pomegranate", "watermelon", "naseberry"]
sublist(fruits, 2, 5)
['guave', 'grapes', 'banana']
#return statement with more than one values
def one():
    a = 5
    b = 10
    a, b = two(a, b)
    print("One prints", a, b)
    
def two(a, b):
    a=9
    b=7
    print("Two prints", a, b)
    return a, b
    
one()
Two prints 9 7
One prints 9 7

Default argument values in function

def ask_ok(prompt, retries=4, reminder='Please try again!'): # two arguments have been assigned a default
    while True:
        ok = input(prompt)
        if ok in ('y', 'ye', 'yes'):
            return True
        if ok in ('n', 'no', 'nop', 'nope'):
            return False
        retries = retries - 1
        if retries <= 0:
            raise ValueError('invalid user response')
        print(reminder)

# ask_ok("Do you really want to quit?")
ask_ok("Okay?", 2, "Only yes or no!")
Okay?nop
False

in and not in operator

Lambda expression

Small anonymous functions are created with the lambda keyword. No need of function name and return statement

Read more https://www.geeksforgeeks.org/python-lambda-anonymous-functions-filter-map-reduce/

g = lambda x: x*x*x
print(g(3))
27

filter() method

# The filter() takes in a function and a list as arguments.
# This offers an elegant way to filter out all the elements of a sequence, 
# for which the function returns True. 
li = [5, 7, 22, 97, 54, 62, 77, 23, 73, 61]
final_list = list(filter(lambda x:(x%2 != 0), li))
print(final_list)
[5, 7, 97, 77, 23, 73, 61]

Unpacking Argument Lists

a = [3, 8, 1] # [start, stop, step]
print(list(range(*a)))
[3, 4, 5, 6, 7]

More on lists

# list.append(x) - adds to the end of the list
l1 = [1, 2, 3]
l1.append(4)
print("After append ", l1)

# list.extend(iterable) - extends the list by appending all the items from the iterable
l1.extend(range(6, 10))
print("After extend ", l1)

# list.insert(i, x) - i is the index value and x is the data to insert
l1.insert(4, 5)
print("After insert ", l1)

# list.remove(x) - removes first item from the list whose value is equal to x. Raises ValueError if not found
l1.remove(3)
print("After remove ", l1)

# list.pop([i]) - passing the parameter is optional and hence written within [].
# removes the item at a given position
# list.pop() removes and returns the last item in the list
print(l1.pop())
print("After pop() ", l1)

# del operator deletes an element from a specified index or a slice
del l1[4]
print("After del[4] ", l1)

# list.clear() - removes all items from the list. Equivalent to del list[:]
l2 = [98, 56, 21, 30]
print("Before clear() ", l2)
l2.clear()
print("After clear() ", l2)

# list.index(x[, start[, end]])
# Return zero-based index in the list of the first item whose value is equal to x. 
# Raises a ValueError if there is no such item.
# The optional arguments start and end are interpreted as in the slice notation 
# and are used to limit the search to a particular subsequence of the list. 
# The returned index is computed relative to the beginning of 
# the full sequence rather than the start argument.
print("Element 4 is at", l1.index(4))
print("Element 7 in slice l1[1:len(l1)] is at", l1.index(7, 1, len(l1)))
# print(l1.index(7, 1, 3)) # raises ValueError since 7 is not in the slice | 1 | 2 | 3 |

# list.count(x) - return the number of times x appears in the list
l2 = [6, 51, 6, 21, 52, 6, 2]
print("Count of occurences of element 6", l2.count(6))

#list.sort(key=None, reverse=False) - sorts elements in place
print("Before sort in ascending order", l2)
l2.sort()
print("After sort in ascending order", l2)
l2.sort(reverse=True)
print("After sort in descending order", l2)

# list.reverse() - reverse the elements in 
l1.reverse()
print("Reversing the list", l1)

# list.copy() - returns a shallow copy of the list. Equivalent to list[:]
print("list copied", l2.copy())
After append  [1, 2, 3, 4]
After extend  [1, 2, 3, 4, 6, 7, 8, 9]
After insert  [1, 2, 3, 4, 5, 6, 7, 8, 9]
After remove  [1, 2, 4, 5, 6, 7, 8, 9]
9
After pop()  [1, 2, 4, 5, 6, 7, 8]
After del[4]  [1, 2, 4, 5, 7, 8]
Before clear()  [98, 56, 21, 30]
After clear()  []
Element 4 is at 2
Element 7 in slice l1[1:len(l1)] is at 4
Count of occurences of element 6 3
Before sort in ascending order [6, 51, 6, 21, 52, 6, 2]
After sort in ascending order [2, 6, 6, 6, 21, 51, 52]
After sort in descending order [52, 51, 21, 6, 6, 6, 2]
Reversing the list [8, 7, 5, 4, 2, 1]
list copied [52, 51, 21, 6, 6, 6, 2]

List can have elements of different types

  • Though it's not recommended to store heterogenous elements in a list. You can use tuples to store them.
k = ['a', 254, 65, 8.2, "dart"]
print(k)
['a', 254, 65, 8.2, 'dart']

Using lists as stacks

  • stack follows LIFO(last-in, first-out) principle
  • to push we use append()
  • to pop we use pop()
stack = [3, 8, 2]
stack.append(4)
stack.append(7)
print(stack)
print(stack.pop())
print(stack.pop())
print(stack)
[3, 8, 2, 4, 7]
7
4
[3, 8, 2]

Using lists as queues

  • queue follows FIFO(first-in, first-out) principle
  • It is also possible to use a list as a queue, where the first element added is the first element retrieved.
  • However, lists are not efficient for this purpose.
  • While appends and pops from the end of list are fast but doing inserts or pops from the beginning of a list is slow (because all of the other elements have to be shifted by one) to implement a queue, use collections.deque which was designed to have fast appends and pops from both ends.
  • Deques support thread-safe, memory efficient appends and pops from either side of the deque with approximately the same O(1) performance in either direction.
from collections import deque
queue = deque(["Amit", "Karthik", "Sammy", "Avik"])
queue.append("Tanmoy")
queue.append("Komal")
print("Elements in queue:", list(queue))
print(queue.popleft())
print(queue.popleft())
print(queue)
Elements in queue: ['Amit', 'Karthik', 'Sammy', 'Avik', 'Tanmoy', 'Komal']
Amit
Karthik
deque(['Sammy', 'Avik', 'Tanmoy', 'Komal'])

List comprehensions

  • List comprehensions provide a concise way to create lists. Common applications are to make new lists where each element is the result of some operations applied to each member of another sequence or iterable, or to create a subsequence of those elements that satisfy a certain condition.
  • basic syntax: new_list = [expression for_loop_one_or_more condtions]
squares = []
for x in range(10):
    squares.append(x**2)

print(squares)
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
squares = list(map(lambda x:x**2, range(10)))
print(squares)
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
# more concise and readable
# this is list comprehension
squares = [x**2 for x in range(1, 10)]
print(squares)
[1, 4, 9, 16, 25, 36, 49, 64, 81]
# If the expression is a tuple, it must be parenthesized.
[(x, y) for x in [1,2,3] for y in [3,1,4] if x != y]
[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]
# flatten a list using a list comprehension with two 'for'
vec = [[1,2,3], [4,5,6], [7,8,9]]
[num for elem in vec for num in elem]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
# call a method on each element
freshfruit = ['  banana', '  loganberry ', 'passion fruit  ']
[weapon.strip() for weapon in freshfruit]
['banana', 'loganberry', 'passion fruit']

Capitalize each word in a string

str = input()
l = str.split()
print(" ".join(word.capitalize() for word in l)) # list comprehension
operating system
Operating System

map() method

# listify the list of strings individually

l = ['sat', 'bat', 'cat', 'mat'] 

test = list(map(list, l)) 
print(test) 
[['s', 'a', 't'], ['b', 'a', 't'], ['c', 'a', 't'], ['m', 'a', 't']]
# string containing numbers to list of numbers
num = "86428452"
n = list(map(int, num))
print(n)
[8, 6, 4, 2, 8, 4, 5, 2]

Nested list comprehensions

# transpose a matrix
# using nested list comprehensions
matrix = [
    [1, 2, 3, 4],
    [5, 6, 7, 8],
    [9, 10, 11, 12],
]

matrix = [[row[i] for row in matrix] for i in range(4)]

for row in matrix:
  print(row)
[1, 5, 9]
[2, 6, 10]
[3, 7, 11]
[4, 8, 12]

zip() method

  • The purpose of zip() is to map the similar index of multiple containers so that they can be used just using as single entity.
  • Syntax : zip(*iterators)
  • Parameters : Python iterables or containers ( list, string etc )
  • Return Value : Returns a single iterator object, having mapped values from all the containers.
# using zip()
matrix = [
    [1, 2, 3, 4],
    [5, 6, 7, 8],
    [9, 10, 11, 12],
]

print(list(zip(*matrix))) # *matrix unpacks argument lists
[(1, 5, 9), (2, 6, 10), (3, 7, 11), (4, 8, 12)]

Tuples and sequences

  • List, strings, tuples, range are all examples of sequence types
  • tuple consists of a number of values seperated by commas
  • tuples may be nested
  • tuples are immutable
  • tuples when printed are always enclosed in parentheses
  • generally, tuples are used to store heterogenous elements whereas lists are used to store homogenous elements
  • Note that multiple assignment is really just a combination of tuple packing and sequence unpacking.
t = 748, 9709, "Hello"
print(t)
y = t, 76, 97 # nested tuple
print(y)
print(t[2])
# tuple consists no item
p = ()
print(len(p))
# tuple has one item
o = "Hey!", # note the trailing comma
print(len(o))
print(o)
(748, 9709, 'Hello')
((748, 9709, 'Hello'), 76, 97)
Hello
0
1
('Hey!',)
# multiple assignment is really just a combination of tuple packing and sequence unpacking
# x, y, z = 56, 25, 89
# print(x, y, z)
# same as:
t = 56, 25, 89 # tuple packing
x, y, z = t # sequence unpacking
print(x, y, z)
56 25 89
# sorting a tuple 
t = (25, 65, 1, 3, 99)
k = sorted(t)
print(k)
[1, 3, 25, 65, 99]

Sets

  • a set is an unordered collection with no duplicate elements
  • Basic uses include membership testing and eliminating duplicate entries.
  • also supports mathematical operations like union, intersection, difference and symmetric difference
  • like list comprehensions, set comprehensions are also supported
  • sets can also be created using set() constructor
basket = {'apple', 'orange', 'apple', 'pear', 'orange', 'banana'}
print(basket) # notice: duplicates have been removed
# search an element in set by fast membership testing
print("apple" in basket)
print("guave" in basket)
{'banana', 'apple', 'pear', 'orange'}
True
False
# set operations
a = set("abracadabra")
b = set("alacazam")
print(a, b, end="\n\n")

# letters in a but not in b
print(a-b)
# letters in a or b or both
print(a | b)
# letters in both a and b
print(a & b)
# letters in a or b but not both
print(a ^ b)
{'d', 'a', 'c', 'r', 'b'} {'a', 'c', 'z', 'l', 'm'}

{'r', 'd', 'b'}
{'d', 'a', 'c', 'z', 'r', 'm', 'l', 'b'}
{'a', 'c'}
{'r', 'z', 'd', 'l', 'm', 'b'}
# set comprehensions
a = {x for x in "alacazam" if x not in "alm"}
print(a)
{'c', 'z'}

Dictionaries

  • till now we have read built-in types those were sequences
  • now we will study dictionary which is a mapping type/object
  • a mapping object maps hashable values to arbitary objects
  • mappings are mutable objects
  • Dictionaries can be created by placing a comma-separated list of key: value pairs within braces or by the dict constructor
  • Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys
  • Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key.
  • You can't use lists as keys since they are mutable
tel = {"jack": 4098, "sape": 4139, "klein": 5206}
print(tel)
# add new key: value pair
tel["dire"] = 8521
print(tel)

del tel["sape"]
print(tel)

l = list(tel) # list of keys
l = sorted(l)
print(l)
print("kol" in l)
{'jack': 4098, 'sape': 4139, 'klein': 5206}
{'jack': 4098, 'sape': 4139, 'klein': 5206, 'dire': 8521}
{'jack': 4098, 'klein': 5206, 'dire': 8521}
['dire', 'jack', 'klein']
False
dd = {1: 25, 2: 30, 3:58}
print(dd[1])
25
# The dict() constructor builds dictionaries directly from sequences of key-value pairs:
d = dict([('sape', 4139), ('guido', 4127), ('jack', 4098)])
print(d)
{'sape': 4139, 'guido': 4127, 'jack': 4098}
# dict comprehensions
f = {x: x**2 for x in (2, 4, 6)}
print(f)
{2: 4, 4: 16, 6: 36}
# when keys are simple strings, it is sometimes easier to specify pairs using keyword arguments
g = dict(sape=4139, guido=4127, jack=4098)
print(g)
{'sape': 4139, 'guido': 4127, 'jack': 4098}

update()

  • Update the dictionary with the key/value pairs from other, overwriting existing keys. Return None.
  • update() accepts either another dictionary object or an iterable of key/value pairs (as tuples or other iterables of length two). If keyword arguments are specified, the dictionary is then updated with those key/value pairs: d.update(red=1, blue=2).
tel = {"jack": 4098, "sape": 4139, "klein": 5206}
print("Before update", tel["jack"])
tel.update(jack=6102)
print("After update", tel["jack"])
Before update 4098
After update 6102

Difference between sorted() and sort()

Looping techniques

# When looping through dictionaries, the key and corresponding value 
# can be retrieved at the same time using the items() method.
knights = {'gallahad': 'the pure', 'robin': 'the brave'}

for k, v in knights.items():
  print(k, v)
gallahad the pure
robin the brave
# When looping through a sequence, the position index and corresponding 
# value can be retrieved at the same time using the enumerate() function.
# i = index, v = value
l = ["tic", "tac", "toe"]
for i, v in enumerate(l):
  print(i, v)
0 tic
1 tac
2 toe
# to loop over two or more sequence we can use zip()
questions = ['name', 'quest', 'favorite color']
answers = ['lancelot', 'the holy grail', 'blue']
for q, a in zip(questions, answers):
    print('What is your {0}?  It is {1}.'.format(q, a)) 
What is your name?  It is lancelot.
What is your quest?  It is the holy grail.
What is your favorite color?  It is blue.
# To loop over a sequence in reverse, first specify the sequence 
# in a forward direction and then call the reversed() function.
for i in reversed(range(5, 30, 5)):
  print(i, end=" ")
25 20 15 10 5 
# To loop over a sequence in sorted order, use the sorted() function 
# which returns a new sorted list while leaving the source unaltered.
basket = ['apple', 'orange', 'apple', 'pear', 'orange', 'banana']
for b in sorted(basket):
  print(b)
apple
apple
banana
orange
orange
pear

More on conditions

  • operators in and not in check whether a value occurs or not in a sequence
  • operators is and is not compare whether two objects are equal or not; this only matters for mutable objects like lists
  • all comparison operators have the same priority, which is lower than that of all numerical operators
  • comparisons can be chained, a < b == c tests whether a is less than b and moreover b equals c
  • not has the highest priority and or has the lowest
  • the boolean operators and & or are so-called short circuit operators. Evaluated from left to right and evaluation stops as soon as the outcome is determined
  • when used as a general value and not as a boolean, the return value of short-circuit operators is the last evaluated argument

Precedence of operators

a = 1
b = 0
c = 5
print(a and not b or c)
print(not a and b)
True
False
string1, string2, string3 = '', 'Old', 'New'
print(string1 or (False or string3))
print(string1 or string2 or string3)
print(string1 or (string2 or string3))
New
Old
Old

What's left to read in this chapter?

  • Comparing Sequences and Other Types

What are modules?

  • Python has a way to put definitions in a file and use them in a script or in an interactive instance of the interpreter. Such a file is called a module; definitions from a module can be imported into other modules or into the main module (the collection of variables that you have access to in a script executed at the top level and in calculator mode).
  • A module is a file containing Python definitions and statements.
# Save this as addsub.py

def add(a, b):
  print(a+b)

def subtract(a, b):
  print(abs(a-b))

Import this file with the following command:

import addsub

invoke add/subtract method inside the interpreter by:

addsub.add(50, 20)

if you intend to use a method often then assign it a local name:

add = addsub.add then invoke it add(25, 20)

Dunder or magic methods

They are the methods having two prefix and suffix underscores in the method name. Dunder here means “Double Under (Underscores)”. These are commonly used for operator overloading.

The __init__ method for initialization is invoked without any call, when an instance of a class is created, like constructors in certain other programming languages such as C++, Java, C#, PHP etc. These methods are the reason we can add two strings with ‘+’ operator without any explicit typecasting.

Meaning of underscores in Pyhton: https://dbader.org/blog/meaning-of-underscores-in-python

# declare our own string class
class String:

    # magic method to initiate object
    def __init__(self, string):
        self.string = string


if __name__ == '__main__':

    # object creation
    string1 = String('Hello')

# print object location
print(string1)

# print the string attribute value
print(string1.string)
<__main__.String object at 0x7fa46866a7d0>
Hello
  • Now let's add a method to represent our object
class String:

    def __init__(self, string):
        self.string = string

    # print our string object
    # similar to toString() method in Java
    def __repr__(self):
        return f"Object: {self.string}"

if __name__ == '__main__':

    string1 = String('Hello')

    print(string1)
Object: Hello
  • If we try to concatenate a string object say "World" to our own String object, we cannot do that. There is a method __add__ that can help.
class String:

    def __init__(self, string):
        self.string = string

    def __repr__(self):
        return f"Object: {self.string}"

    def __add__(self, other):
        return self.string + other

if __name__ == '__main__':

    string1 = String('Hello')

    # concatenate String object and a string
    print(string1 + ' World')
Hello World
  • Within a module, the module’s name (as a string) is available as the value of the global variable __name__
# If the source file is executed as the main program, 
# the interpreter sets the __name__ variable to have a value “__main__”.
print(__name__)
__main__

We can get the file path where our module is located by using __file__ method. Usage: <method_name>.__file__

How to reload modules?

  • If you've made some changes in your module file then to reflect the changes in the interpreter you need to do:
    • import importlib
    • importlib.reload(modulename)

More on modules

  • Modules can import other modules
  • A module can contain executable statements as well as function definitions. These statements are intended to initialize the modules.
  • There is a variant of the import statement that imports names from a module directly into the importing module’s symbol table:
    • from addsub import add, subtract This does not introduce the module name from which the imports are taken in the local symbol table (so in the example, fibo is not defined).
  • To import all names that a module defines.
    • from addsub import *
  • If the module name is followed by as, then you can play this way:
    • import addsub as adsu
    • adsu.add(50, 89)

Similar to command-line arguments

# Syntax: python3 filename.py <arguments>
def add(a, b):
  print(a+b)

def subtract(a, b):
  print(abs(a-b))

if __name__ == "__main__":
    import sys
    add(int(sys.argv[1]), int(sys.argv[2]))

# run it as:
# python3 addsub.py 41 21
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-1-e64a5f1e21b3> in <module>
      8 if __name__ == "__main__":
      9     import sys
---> 10     add(int(sys.argv[1]), int(sys.argv[2]))
     11 
     12 # run it as:

ValueError: invalid literal for int() with base 10: '-f'

dir() function

  • It is an interesting method
  • displays all the names in a module
import sys
print(dir(sys), end=" ")
# dir() without arguments lists all names that you have defined currently
print(dir(), end=" ")

What's left to read in this chapter?

  • After Packages

Old string formatting

a = 10
b = 8.92
print("a = %d and b = %.2f" %(a, b))
a = 10 and b = 8.92

f-strings or Formatted String Literals

import math
print(f"The value of pi is {math.pi:.3f} approx.")
The value of pi is 3.142 approx.

Passing an integer after the ':' will cause that field to be a minimum number of characters wide. This is useful for making columns line up.

table = {'Sjoerd': 4127, 'Jack': 4098, 'Dcab': 8637678}
for name, phone in table.items():
    print(f"{name:10} = {phone:10d}")
Sjoerd     =       4127
Jack       =       4098
Dcab       =    8637678

String format method

print("a = {0} and b = {1}".format(a, b))
a = 10 and b = 8.92

If keyword arguments are used in the str.format() method, their values are referred to by using the name of the argument.

print('Name = {name}, Roll = {roll}.'.format(name='Aheri', roll='16'))
Name = Aheri, Roll = 16.

If you have a really long format string that you don’t want to split up, it would be nice if you could reference the variables to be formatted by name instead of by position. This can be done by simply passing the dict and using square brackets '[]' to access the keys

table = {'Sjoerd': 4127, 'Jack': 4098, 'Dcab': 8637678}
print('Jack: {0[Jack]:d}; Sjoerd: {0[Sjoerd]:d}; Dcab: {0[Dcab]:d}'.format(table))
Jack: 4098; Sjoerd: 4127; Dcab: 8637678

This is a better way. This could also be done by passing the table as keyword arguments with the ‘**’ notation.

table = {'Sjoerd': 4127, 'Jack': 4098, 'Dcab': 8637678}
print('Jack: {Jack:d}; Sjoerd: {Sjoerd:d}; Dcab: {Dcab:d}'.format(**table))
Jack: 4098; Sjoerd: 4127; Dcab: 8637678

Classes

https://docs.python.org/3/tutorial/classes.html

In this chapter, we'll get to know how OOP concepts are being implemented in Python.

Here is an interesting article on comparison between OOP in Python and Java: https://realpython.com/oop-in-python-vs-java/

What are namespaces?

A namespace is a system to have a unique name for each and every object in Python. An object might be a variable or a method. Python itself maintains a namespace in the form of a Python dictionary.

A namespace is a mapping from names to objects. Most namespaces are currently implemented as Python dictionaries, but that’s normally not noticeable in any way (except for performance), and it may change in the future. Examples of namespaces are: the set of built-in names (containing functions such as abs(), and built-in exception names); the global names in a module; and the local names in a function invocation. In a sense the set of attributes of an object also form a namespace. The important thing to know about namespaces is that there is absolutely no relation between names in different namespaces; for instance, two different modules may both define a function maximize without confusion — users of the modules must prefix it with the module name.

Suppose, this is the expression z.real, real is an attribute of the object z.

modname.funcname, modname is a module object and funcname is an attribute of it.

Types of namespaces

When Python interpreter runs solely without and user-defined modules, methods, classes, etc. Some functions like print(), id() are always present, these are built in namespaces. When a user creates a module, a global namespace gets created, later creation of local functions creates the local namespace. The built-in namespace encompasses global namespace and global namespace encompasses local namespace.

Lifetime of a namespace

A lifetime of a namespace depends upon the scope of objects, if the scope of an object ends, the lifetime of that namespace comes to an end. Hence, it is not possible to access inner namespace’s objects from an outer namespace.

A special quirk of Python is that – if no global or nonlocal statement is in effect – assignments to names always go into the innermost scope. Assignments do not copy data — they just bind names to objects. The same is true for deletions: the statement del x removes the binding of x from the namespace referenced by the local scope. In fact, all operations that introduce new names use the local scope: in particular, import statements and function definitions bind the module or function name in the local scope.

The global statement can be used to indicate that particular variables live in the global scope and should be rebound there; the nonlocal statement indicates that particular variables live in an enclosing scope and should be rebound there.

# using nonlocal variables

def outer():
    a = 5

    def inner():
        nonlocal a
        a = 10

    inner()
    print("Value of a using nonlocal is : ", a)

outer()
Value of a using nonlocal is :  10
# without using nonlocal variables

def outer():
    a = 4
    
    def inner():
        # here a local copy of a is being created
        a = 6
        
    inner()
    print("Value of a without using nonlocal is : ", a)
    
outer()      
Value of a without using nonlocal is :  4
# using global and nonlocal variables
def scope_test():
    def do_local():
        spam = "local spam"

    def do_nonlocal():
        nonlocal spam
        spam = "nonlocal spam"

    def do_global():
        global spam
        spam = "global spam"

    spam = "test spam"
    do_local()
    print("After local assignment:", spam)
    do_nonlocal()
    print("After nonlocal assignment:", spam)
    do_global()
    print("After global assignment:", spam)

scope_test()
print("In global scope:", spam)
After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam

Note how the local assignment (which is default) didn’t change scope_test’s binding of spam. The nonlocal assignment changed scope_test’s binding of spam, and the global assignment changed the module-level binding.

Are there private variables in python? What is name mangling?

Read here https://www.geeksforgeeks.org/private-variables-python/?ref=rp

“Private” instance variables that cannot be accessed except from inside an object don’t exist in Python. However, there is a convention that is followed by most Python code: a name prefixed with an underscore (e.g. _spam) should be treated as a non-public part of the API (whether it is a function, a method or a data member).

How is encapsulation achieved in python?

Read here https://www.geeksforgeeks.org/encapsulation-in-python/

Class objects

Class objects support two kinds of operations: attribute references and instantiation.

class Car: # class definiton
    """A class called Car"""
    # class attributes or class variables
    brand = "Hyundai"
    model = "S14"
    
    # methods
    def printDetails(self):
        print(self.brand, end="\n")
        print(self.model)
        
# instantiating the class & assigning the 
# object reference to the variable c
c = Car()
c.printDetails() # method call
#attribute references
print(c.brand + " " + c.model)
print(c.printDetails)
print(c.__doc__) # __doc__ is also a valid attribute
Hyundai
S14
Hyundai S14
<bound method Car.printDetails of <__main__.Car object at 0x7fbc859a2dd0>>
A class called Car

Data attributes need not be declared; like local variables, they spring into existence when they are first assigned to.

c.color = "black"
print(c.color)
black

Constructors in Python. Really?

The __init__ method is similar to what constructors do. They initialize object instances at the time when class instantiation is done.

class Complex:
    def __init__(self, realpart, imagpart):
        self.r = realpart
        self.i = imagpart
        
cn = Complex(3, 8)
print(str(cn.r) + " + " + str(cn.i) + "i")
3 + 8i

Method objects

Here is another way of invoking a method

m = c.printDetails # this is a method object that is assigned to m
print(m()) # calling it
Hyundai
S14
None

The special thing about methods is that the instance object is passed as the first argument of the function.

Class and instance variables

Generally speaking, instance variables are for data unique to each instance and class variables are for attributes and methods shared by all instances of the class.

class PinkFloyd:
    # class variables
    
    genre = "Psychedelic rock" # attribute
    
    def __init__(self, name): # method
        self.name = name # instance variable 
        
d = PinkFloyd("David Gilmour")
r = PinkFloyd("Richard Wright")
print(d.genre)
print(r.genre)
print(d.name)
print(r.name)
Psychedelic rock
Psychedelic rock
David Gilmour
Richard Wright

Random remarks

https://docs.python.org/3/tutorial/classes.html#random-remarks

  • Classes are not usable to implement pure abstract data types. In fact, nothing in Python makes it possible to enforce data hiding - it is all based upon convention.
  • Often, the first argument of a method is called self. This is nothing more than a convention: the name self has absolutely no special meaning to Python. Note, however, that by not following the convention your code may be less readable to other Python programmers

Inheritance

class Animal: # Base class
    def __init__(self, weight, height):
        # private instance variables
        self._weight = weight
        self._height = height
        
    def makeNoise(self):
        return "I'm too generic to make any noise."
    
    def getWeight(self):
        return self._weight
    
    def getHeight(self):
        return self._height
        
class Dog(Animal): # Derived class inheriting Animal class
    def makeNoise(self):
        return "Bark! Bark!"
    
class Cat(Animal): # Derived class inheriting Animal class
    def makeNoise(self):
        return "Meow! Meow!"
    
a = Animal(0, 0)
d = Dog(50, 60)
c = Cat(15, 22)

print(a.makeNoise())
print(d.makeNoise())
print(c.makeNoise())
print(a.getWeight(), a.getHeight())
print(c.getWeight(), c.getHeight())

print(isinstance(c, Cat))
print(issubclass(Dog, Cat))
print(issubclass(Dog, Animal))
I'm too generic to make any noise.
Bark! Bark!
Meow! Meow!
0 0
15 22
True
False
True
  • class DerivedClassName(modname.BaseClassName) if base class is in some other modules

Multiple inheritance

class DerivedClassName(Base1, Base2, Base3):

Something similar to C's struct

class Employee: # this is an empty class definition
    pass

    def __repr__(self):
        return f"Name: {self.name} \nDept: {self.dept}\nSalary: {self.salary}"

john = Employee()  # Create an empty employee record

# Fill the fields of the record
john.name = 'John Doe'
john.dept = 'computer lab'
john.salary = 1000

print(john)
Name: John Doe 
Dept: computer lab
Salary: 1000

Iterator for your own class

Behind the scenes, the for statement calls iter() on the container object. The function returns an iterator object that defines the method __next__() which accesses elements in the container one at a time. When there are no more elements, __next__() raises a StopIteration exception which tells the for loop to terminate. You can call the __next__() method using the next() built-in function

class Traverse:
    """Iterator for looping over a sequence"""
    def __init__(self, data):
        self.data = data
        self.index = len(data)
        self.curr = 0
        
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.curr == self.index:
            raise StopIteration
        
        self.curr = self.curr + 1
        return self.data[self.curr - 1]
    
    
x = Traverse("mayukh")
for c in x:
    print(c)
m
a
y
u
k
h

Generators

Generators are a simple and powerful tool for creating iterators. They are written like regular functions but use the yield statement whenever they want to return data. Each time next() is called on it, the generator resumes where it left off (it remembers all the data values and which statement was last executed).

def traverse(data):
    for index in range(0, len(data), 1):
        yield data[index]
        
for c in traverse("earth"):
    print(c)
e
a
r
t
h
  • Anything that can be done with generators can also be done with class-based iterators as described in the previous section. What makes generators so compact is that the __iter__() and __next__() methods are created automatically.

  • Another key feature is that the local variables and execution state are automatically saved between calls. This made the function easier to write and much more clear than an approach using instance variables like self.index and self.data.

  • In addition to automatic method creation and saving program state, when generators terminate, they automatically raise StopIteration. In combination, these features make it easy to create iterators with no more effort than writing a regular function.

Generator expressions

# other comprehensions
xvec = [10, 20, 30]
yvec = [7, 5, 3]
print(sum(x*y for x,y in zip(xvec, yvec))) # dot product

# unique_words = set(word for line in page  for word in line.split())
# valedictorian = max((student.gpa, student.name) for student in graduates)

# generator expressions
data = 'golf'
list(data[i] for i in range(len(data)-1, -1, -1))
260
['f', 'l', 'o', 'g']