Python

Python is an interpreted, high-level, general-purpose programming language. Created by Guido van Rossum and first released in 1991, Python's design philosophy emphasizes code readability with its notable use of significant whitespace

  • Python is a strongly, dynamically typed language.

Notable Features

  • Easy to learn.
  • Supports quick development.
  • Cross-platform.
  • Open Source.
  • Extensible.
  • Embeddable.
  • Large standard library and active community.
  • Useful for a wide variety of applications.

Whitespace

Whitespace is significant in Python. Where other languages may use {} or (), Python uses indentation to denote code blocks.

Installation

There are three options available to install python on your machine

  1. Download latest Python from Official Python Website and install it.
  2. Download Anaconda distribution which comes with a bundle of packages Official Anaconda Website and install it.
  3. Download Miniconda from Miniconda website and install it (My favourite).

Philosophy

  • from the zen of python
In [1]:
import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

Comments

  • Comments are lines that exist in computer programs that are ignored by compilers and interpreters.
  • Including comments in programs makes code more readable for humans as it provides some information or explanation about what each part of a program is doing.
  • In general, it is a good idea to write comments while you are writing or updating a program as it is easy to forget your thought process later on, and comments written later may be less useful in the long term.

  • # is used for single line comments

  • """ """ or ''' ''' is used for multi line comments
In [2]:
# It is a single line comment

"""
It 
is
multi-line
comment
"""
a = 20

Keywords

  • Keywords are the reserved words in python
  • We can't use a keyword as variable name, function name or any other identifier
  • Keywords are case sentive
In [3]:
#Get all keywords in python
import keyword

print(f"Total Number of keywords in Python: {len(keyword.kwlist)}\n\n")

print(keyword.kwlist)
Total Number of keywords in Python: 35


['False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield']

Identifiers

Identifier is the name given to entities like class, functions, variables etc. in Python. It helps differentiating one entity from another.

Rules for Writing Identifiers:

  1. Identifiers can be a combination of letters in lowercase (a to z) or uppercase (A to Z) or digits (0 to 9) or an underscore (_).
  2. An identifier cannot start with a digit. 1variable is invalid, but variable1 is perfectly fine.
  3. An identifier cannot contain spaces, use _ intead
  4. Keywords cannot be used as identifiers.

Statements

In [4]:
# Instructions that a Python interpreter can execute are called **statements**.

total = 2  # single line statement

new_total = 1 + 2 + 3 + \
            4 + 5 + 6 + \
            7 + 8 + 9 # Multiline statement

extra = (1 + 2 + 3 + 
         4 + 5 + 6 +
         7 + 8 + 9) # Other way of multiline statement
In [5]:
# Multiple statements in single line
a = 20; b = 30; c = 30

Variables

  • A variable is a location in memory used to store some data (value).
  • They are given unique names to differentiate between different memory locations. The rules for writing a variable name is same as the rules for writing identifiers in Python.
  • We don't need to declare a variable before using it. In Python, we simply assign a value to a variable and it will exist. We don't even have to declare the type of the variable. This is handled internally according to the type of value we assign to the variable.
  • Python uses dynamic typing, meaning you can reassign variables to different data types. This makes Python very flexible in assigning data types; it differs from other languages that are statically typed.
  • avoid using the single characters l (lowercase letter el), O (uppercase letter oh) and I (uppercase letter eye) as they can be confused with 1 and 0

Variable Assignments

In [6]:
name = "Pyton"
ver = 3.6
name2 = "Python 3"
legacy_ver = 2.7
is_legacy_supported = False
cmp_num = 2+3j

Multiple assignements

In [7]:
name, ver, is_legacy_supported = "Python", 2.7, False

Python uses memory more efficiently.

If two variables have same value then they both point to the same memory location.

Use id() to get the memory location of the identifier

In [8]:
a = 100
b = 100
In [9]:
print(f"Location of a in memory: {id(a)}")
print(f"Location of b in memory: {id(b)}")
Location of a in memory: 140735707624304
Location of b in memory: 140735707624304

Checking the variable type with type()

In [10]:
type(name)
Out[10]:
str
In [11]:
type(ver)
Out[11]:
float
In [12]:
type(cmp_num)
Out[12]:
complex
In [13]:
type(is_legacy_supported)
Out[13]:
bool

Data types

Data types

  • Numbers (Integer [int], Floating point numbers [float], Complex numbers [complex])
  • Boolean [bool] - Either False or True
  • String [str]
  • List
  • Tuple
  • Dictionary
  • Set

Numbers

In [14]:
a = 10
b = 5.99
c = 7+6j

print(f"{a} is of type  {type(a)}")
print(f"{b} is of type  {type(b)}")
print(f"{c} is of type  {type(c)}")
10 is of type  <class 'int'>
5.99 is of type  <class 'float'>
(7+6j) is of type  <class 'complex'>

Boolean

In [15]:
a = True
b = False
print(f"{a} is of type  {type(a)}")
print(f"{b} is of type  {type(b)}")
True is of type  <class 'bool'>
False is of type  <class 'bool'>

String

  • String is sequence of Unicode characters.
  • We can use single quotes or double quotes to represent strings.
  • Multi-line strings can be denoted using triple quotes, ''' or """.
  • A string in Python consists of a series or sequence of characters - letters, numbers, and special characters.
  • Strings can be indexed - often synonymously called subscripted as well.
  • Similar to C, the first character of a string has the index 0.

Strings are immutable i.e we cannot doS[4] = 't'

In [16]:
s = "This is a string"
print(s)
print(f"variable s is a type of {type(s)}")
This is a string
variable s is a type of <class 'str'>
In [17]:
# Multiline string
s = """This is 
multiline string 
in Python
"""

print(s)
This is 
multiline string 
in Python

In [18]:
new_str = "This is python string" 

# accessing first character in string using index
new_str[0]
Out[18]:
'T'
In [19]:
# accessing last character in string using index
new_str[-1]
Out[19]:
'g'
In [20]:
# slicing [Start:stop: step]
new_str[5:]
Out[20]:
'is python string'
In [21]:
new_str[8:-3]
Out[21]:
'python str'

List

  • List is an ordered sequence of items. It is one of the most used datatype in Python and is very flexible.
  • All the items in a list do not need to be of the same type.
  • Declaring a list: Items separated by commas are enclosed within brackets [ ].
  • List is mutable
In [22]:
a = [10, 20.5, 1+2j, "Hello", True]
print(a[1])               #print 1st index element
20.5
In [23]:
# List is mutable
a[1] = 19.99
In [24]:
a
Out[24]:
[10, 19.99, (1+2j), 'Hello', True]

Tuples

  • Tuple is an ordered sequence of items same as list. The only difference is that tuples are immutable.Tuples once created cannot be modified.
In [25]:
a = (10, 20.5, 1+2j, "Hello", True)
print(a[1])               #print 1st index element
20.5

Set

  • Set is an unordered collection of unique items.
  • Set is defined by values separated by comma inside braces { }.
  • Items in a set are not ordered.

Since set is unordered we cannot access elements using indices like list/tuple/string

In [26]:
a = {10, 30, 20, 40, 8}
print(a)
{40, 8, 10, 20, 30}

Dictionary

  • Dictionary is an unordered collection of key-value pairs.
  • dictionaries are defined within braces {} with each item being a pair in the form key:value.
  • Key and value can be of any type.
In [27]:
d = {'a': "apple", 'b': "bat", 'c': 'cat'}
In [28]:
print(d['a'])
apple

Type conversion

In [29]:
int(99.99)
Out[29]:
99
In [30]:
float(25)
Out[30]:
25.0
In [31]:
str(20)
Out[31]:
'20'
In [32]:
a = [1, 2, 3, 2]

print(type(a))      #type of a is list 

s = set(a)          #convert list to set using set() method

print(type(s))
<class 'list'>
<class 'set'>
In [33]:
list("Python")
Out[33]:
['P', 'y', 't', 'h', 'o', 'n']

Operators

Operators are special symbols in Python that carry out arithmetic or logical computation. The value that the operator operates on is called the operand.

  1. Arithmetic operators
  2. Comparison (Relational) operators
  3. Logical (Boolean) operators
  4. Bitwise operators
  5. Assignment operators
  6. Identity operators
  7. Membership operators

Arithmetic Operators

  • Arithmetic operators are used to perform mathematical operations like addition, subtraction, multiplication etc.

+ , -, *, /, %, //, ** are arithmetic operators

In [34]:
x, y = 25, 7

#addition
print(f"Addition of {x} and {y}: {x + y}")

#subtraction(-)
print(f"Subtraction of {x} and {y}: {x - y}")

#multiplication(*)
print(f"Multiplication of {x} and {y}: {x * y}")

#division(/)
print(f"Division of {x} and {y}: {x / y}")

#modulo division (%)
print(f"Modulo division (remainder) of {x} and {y}: {x % y}")

#Floor Division (//)
print(f"Floor division (nearest lower integer) of {x} and {y}: {x // y}")

#Exponent (**)
print(f"Exponenet of {x} and {y}: {x ** y}")
Addition of 25 and 7: 32
Subtraction of 25 and 7: 18
Multiplication of 25 and 7: 175
Division of 25 and 7: 3.5714285714285716
Modulo division (remainder) of 25 and 7: 4
Floor division (nearest lower integer) of 25 and 7: 3
Exponenet of 25 and 7: 6103515625
In [35]:
5//2
Out[35]:
2

Comparision Operators

  • Comparison operators are used to compare values. It either returns True or False according to the condition.

>, <, ==, !=, >=, <= are comparision operators

In [36]:
a, b = 25, 7         

#check a is less than b
print(f"{a} less than {b} : {a < b}")
#check a is greater than b
print(f"Is {a} greater than {b} : {a > b}")
#check a is equal to b
print(f"Is {a} equal to {b} : {a == b}")
#check a is not equal to b (!=)
print(f"Is {a} not equal to {b} : {a != b}")
#check a greater than or equal to b
print(f"Is {a} greater than or equal to {b} : {a >= b}")
#check a less than or equal to b
print(f"Is {a} less than or equal to {b} : {a <= b}")
25 less than 7 : False
Is 25 greater than 7 : True
Is 25 equal to 7 : False
Is 25 not equal to 7 : True
Is 25 greater than or equal to 7 : True
Is 25 less than or equal to 7 : False

Logical Operators

  • Logical operators are and, or, not operators.
In [37]:
a, b = True, False

#print a and b
print(f"{a} and {b} is {a and b}")

#print a or b
print(f"{a} or {b} is {a and b}")

#print not b
print(f"not {b} is {not b}")
print(f"not {a} is {not a}")
True and False is False
True or False is False
not False is True
not True is False

Bitwise operators

  • Bitwise operators act on operands as if they were string of binary digits. It operates bit by bit

&, |, ~, ^, >>, << are Bitwise operators

In [38]:
a, b = 10, 4

#Bitwise AND
print(f"{a} & (bitwise AND) {b} is {a & b}")

#Bitwise OR
print(f"{a} | (bitwise OR) {b} is {a | b}")

#Bitwise NOT
print(f"~ (bitwise NOT) {a} is {~ a}")
print(f"~ (bitwise NOT) {b} is {~ b}")


#Bitwise XOR
print(f"{a} ^ (bitwise XOR) {b} is {a ^ b}")


#Bitwise rightshift
print(f"{a} >> (bitwise RIGHTSHIFT) {b} is {a >> b}")

#Bitwise Leftshift
print(f"{a} << (bitwise LEFTSHIFT) {b} is {a << b}")
10 & (bitwise AND) 4 is 0
10 | (bitwise OR) 4 is 14
~ (bitwise NOT) 10 is -11
~ (bitwise NOT) 4 is -5
10 ^ (bitwise XOR) 4 is 14
10 >> (bitwise RIGHTSHIFT) 4 is 0
10 << (bitwise LEFTSHIFT) 4 is 160

Assignment operators

  • Assignment operators are used in Python to assign values to variables.
  • a = 5 is a simple assignment operator that assigns the value 5 on the right to the variable a on the left.

=, +=, -=, *=, /=, %=, //=, **=, &=, |=, ^=, >>=, <<= are Assignment operators

In [39]:
a = 64
print(f"a is {a}")

# add AND (a += 10 is equivalent to a = a + 10)
a += 10
print(f"After Add and Assign 10 to a: {a}")

#subtract AND (-=)
a -= 10
print(f"After Subtract and Assign 10 to a: {a}")

#Multiply AND (*=)
a *= 10
print(f"After Multiply and Assign 10 to a: {a}")

#Divide AND (/=)
a /= 10
print(f"After Divide and Assign 10 to a: {a}")

#Modulus AND (%=)
a %= 10
print(f"After Modulus and Assign 10 to a: {a}")

#Exponent AND (**=)
a **= 10
print(f"After Exponent and Assign 10 to a: {a}")

#Floor Division (//=)
a //= 10
print(f"After Floor Divsion and Assign 10 to a: {a}")
a is 64
After Add and Assign 10 to a: 74
After Subtract and Assign 10 to a: 64
After Multiply and Assign 10 to a: 640
After Divide and Assign 10 to a: 64.0
After Modulus and Assign 10 to a: 4.0
After Exponent and Assign 10 to a: 1048576.0
After Floor Divsion and Assign 10 to a: 104857.0

Identity operators

is and is not are the identity operators in Python.

They are used to check if two values (or variables) are located on the same part of the memory.

In [40]:
a = 5
b = 5

#5 is object created once both a and b points to same object

print(f"A is B: {a is b}")    
print(f"A is not B: {a is not b}")
A is B: True
A is not B: False
In [41]:
s1 = "Python"
s2 = "Python"

print(f"S1 is S2: {s1 is s2}")    
print(f"S1 is not S2: {s1 is not s2}")
S1 is S2: True
S1 is not S2: False
In [42]:
s1 = "Python"
s2 = "Java"

print(f"S1 is S2: {s1 is s2}")    
print(f"S1 is not S2: {s1 is not s2}")
S1 is S2: False
S1 is not S2: True

Membership operators

in and not in are the membership operators in Python.

They are used to test whether a value or variable is found in a sequence (string, list, tuple, set and dictionary).

In [43]:
lst = [1, 2, 3, 4]

print(f"List is {lst}")

#check 1 is present in a given list or not
print(f"check 1 is present in a given list or not {1 in lst}")       

#check 5 is present in a given list
print(f"check 5 is present in a given list or not {5 in lst}")       
List is [1, 2, 3, 4]
check 1 is present in a given list or not True
check 5 is present in a given list or not False
In [44]:
d = {1: "a", 2: "b"}
print(1 in d)
True

Control Flow

if statement

If statement

# syntax
if test_expression:
    code
  • The program evaluates the test expression and will execute statement(s) only if the text expression is True.
  • If the text expression is False, the statement(s) is not executed.

Python interprets non-zero values as True. None and 0 are interpreted as False.

In [45]:
num = 10

# try 0, -1 and None
if num:
    print("Number is positive")
print("This will print always")      #This print statement always print

#NOTE::: Experiment by changing number  
Number is positive
This will print always

if - else statement

If-else statement

# syntax
if test_expression:
    code of if
else:
    code of else
In [46]:
num = 10

# try 0, -1 and None
if num:
    print("Number is positive")
else:
    print("Number is negative")
    
print("This will print always")      #This print statement always print

#NOTE::: Experiment by changing number  
Number is positive
This will print always

if - elif - else statement

If-elif-else statement

# syntax
if test_expression:
    code
elif test_expression2:
    code
else:
    code
In [47]:
num = 0

# try 0, -1 and None
if num > 0:
    print("Number is positive")
elif num == 0:
    print("Number is Zero")
else:
    print("Number is negative")
    
print("This will print always")      #This print statement always print

#NOTE::: Experiment by changing number  
Number is Zero
This will print always

Nested If-else statements

In [48]:
num = 10.5

if num >= 0:
    if num == 0:
        print("Zero")
    else:
        print("Positive number")
else:
    print("Negative Number")
Positive number
In [49]:
# FIND THE LARGEST AMONG THREE NUMBERS
num1 = 10
num2 = 50
num3 = 15

if (num1 >= num2) and (num1 >= num3):           #logical operator   and
    largest = num1
elif (num2 >= num1) and (num2 >= num3):
    largest = num2
else:
    largest = num3
print(f"Largest element among three numbers is: {largest}")
Largest element among three numbers is: 50

While loop

  • The while loop is used to iterate over a block of code as long as the test expression (condition) is true.
# Syntax
while test_expression:
    code
  • The body of the loop is entered only if the test_expression evaluates to True.
  • After one iteration, the test expression is checked again.
  • This process continues until the test_expression evaluates to False.

While loop

In [50]:
#Find product of all numbers present in a list
lst = [10, 20, 30, 40, 60] # 10*20*30*40*60 = 14400000

product = 1
index = 0

while index < len(lst):
    product *= lst[index]
    index += 1

print(f"Product is: {product}")
Product is: 14400000
  • we can have an optional else block with while loop.
  • The else part is executed if the condition in the while loop evaluates to False. The while loop can be terminated with a break statement.
  • In such case, the else part is ignored. Hence, a while loop's else part runs if no break occurs and the condition is false.
In [51]:
numbers = [1, 2, 3,4,5]

#iterating over the list
index = 0
while index < len(numbers):
    print(numbers[index])
    index += 1
    
else:
    print("no item left in the list")
1
2
3
4
5
no item left in the list
In [52]:
# To check whether given number is prime or not
num = int(input("Enter a number: "))        #convert string to int


isDivisible = False;

i=2;
while i < num:
    if num % i == 0:
        isDivisible = True;
        print (f"{num} is divisible by {i}")
    i += 1;
    
if isDivisible:
    print(f"{num} is NOT a Prime number")
else:
    print(f"{num} is a Prime number")
19 is a Prime number

For loop

  • The for loop in Python is used to iterate over a sequence (list, tuple, string) or other iterable objects.
  • Iterating over a sequence is called traversal.
# syntax
for element in sequence :
    code
  • Here, element is the variable that takes the value of the item inside the sequence on each iteration.
  • Loop continues until we reach the last item in the sequence.

For loop

In [53]:
#Find product of all numbers present in a list

lst = [10, 20, 30, 40, 60]

product = 1

#iterating over the list
for el in lst:
    product *= el

print(f"Product is: {product}")
Product is: 14400000

range()

  • We can generate a sequence of numbers using range() function.
    • ex: range(10) will generate numbers from 0 to 9 (10 numbers).
  • We can also define the start, stop and step size as range(start,stop,step size). step size defaults to 1 if not provided.
  • This function does not store all the values in memory, it would be inefficient. So it remembers the start, stop, step size and generates the next number on the go.
In [54]:
#print range of 10
for i in range(10):
    print(i, end="\t")
0	1	2	3	4	5	6	7	8	9	
In [55]:
for i in range(0, 20, 2):
    print(i)
0
2
4
6
8
10
12
14
16
18
In [56]:
lst = ["python", "java", "go", "rust", "typescript", "c++"]

#iterate over the list using index
#for index in range(len(lst)):
#    print(lst[index])

for el in lst:
    print(el)
python
java
go
rust
typescript
c++

For loop with else

  • A for loop can have an optional else block as well. The else part is executed if the items in the sequence used in for loop exhausts.
  • break statement can be used to stop a for loop. In such case, the else part is ignored.
  • Hence, a for loop's else part runs if no break occurs.
In [57]:
numbers = [1, 2, 3]

#iterating over the list
for item in numbers:
    print(item)
else:
    print("no item left in the list")
1
2
3
no item left in the list
In [58]:
# Display all the prime numbers in a range
index1 = 20
index2 = 50

print(f"Prime numbers between {index1} and {index2} are :", end="  ")

for num in range(index1, index2+1):      #default step size is 1
    if num > 1:
        isDivisible = False
        for index in range(2, num):
            if num % index == 0:
                isDivisible = True
        if not isDivisible:        
            print(num, end="\t")
Prime numbers between 20 and 50 are :  23	29	31	37	41	43	47	

Break and Continue statements

  • In Python, break and continue statements can alter the flow of a normal loop.
  • Loops iterate over a block of code until test expression is false, but sometimes we wish to terminate the current iteration or even the whole loop without cheking test expression.
  • The break and continue statements are used in these cases.

Break

break break example

In [59]:
numbers = [1, 2, 3, 4, 5, 6]
for num in numbers:          #iterating over list
    if num == 4:
        break
    print(num)
else:
    print("in the else-block")
print("Outside of for loop")
1
2
3
Outside of for loop
In [60]:
# check given number is Prime number or not (using break)
num = int(input("Enter a number: "))        #convert string to int


isDivisible = False;

i=2;
while i < num:
    if num % i == 0:
        isDivisible = True;
        print (f"{num} is divisible by {i}")
        break; # this line is the only addition.
    i += 1;
    
if isDivisible:
    print(f"{num} is NOT a Prime number")
else:
    print(f"{num} is a Prime number")
29 is a Prime number

Continue

Continue Continue example

In [61]:
#print odd numbers present in a list
numbers = [1, 2, 3, 4, 5]

for num in numbers:
    if num % 2 == 0:
        continue
    print(num)
else:
    print("else-block")
1
3
5
else-block

Data structures

A data structure is a collection of data elements (such as numbers or characters—or even other data structures) that is structured in some way, for example, by numbering the elements. The most basic data structure in Python is the "sequence".

  • Indepth view of derived data types
  1. List
  2. Tuple
  3. Sets
  4. Dictionary
  5. Strings

List

  • List is one of the Sequence Data structure
  • Lists are collection of items (Strings, integers or even other lists)
  • Lists are enclosed in [ ]
  • Each item in the list has an assigned index value.
  • Each item in a list is separated by a comma
  • Lists are mutable, which means they can be changed.

List creation

In [62]:
empty_list = list()

empty_list_2 = []

lst = ['one', 'two', 'three', 'four'] # list of strings

lst2 = [1, 2, 3, 4] #list of integers

lst3 = [[1, 2], [3, 4]] # list of lists

lst4 = [1, 'python', 24, 1.24] # list of different datatypes

print(lst4)
[1, 'python', 24, 1.24]

List length

  • len() function returns the length of the list
In [63]:
lst = ['one', 'two', 'three', 'four']
print(len(lst))
4

List Append

  • append() is used to add elements at the end of the list
  • extend() is used to join two lists
  • + to extend the list
In [64]:
lst.append('five')
print(lst)
['one', 'two', 'three', 'four', 'five']
In [65]:
l1 = ["one", "two", "three"]
l2 = ["four", "five", "six"]

l1.append(l2)
print(l1)
['one', 'two', 'three', ['four', 'five', 'six']]
In [66]:
l1 = ["one", "two", "three"]
l2 = ["four", "five", "six"]

l1.extend(l2)
print(l1)
['one', 'two', 'three', 'four', 'five', 'six']
In [67]:
l1 = ["one", "two", "three"]
l2 = ["four", "five", "six"]

l3 = l1 + l2
print(l3)
['one', 'two', 'three', 'four', 'five', 'six']

List Insert

  • insert() method is used to insert element at specified location
In [68]:
lst.insert(0, "Zero")
lst.insert(3, 4)
print(lst)
['Zero', 'one', 'two', 4, 'three', 'four', 'five']

List Remove

  • remove() method removes the first occurence of the given element from the list
  • del delete the element at specified index. If didn't specify any index it deletes the whole list from memory then you're not able to access it.
  • pop() Pop removes and returns the element at specified location in a list
In [69]:
lst1 = [1,2,3,4,3,3]
lst1.remove(3)
print(lst1)
lst1.pop(4)
print(lst1)

del lst1[1]
print(lst1)

del lst1
[1, 2, 4, 3, 3]
[1, 2, 4, 3]
[1, 4, 3]

List reverse

  • reverse() method reverses the list items
In [70]:
lst = ['one', 'two', 'three', 'four', 'five']

lst.reverse()
print(lst)
['five', 'four', 'three', 'two', 'one']

List Sorting

  • sorted() function returns the sorted list.
  • sort() method sorts the list inplace.
In [71]:
nums = [8, 9, 5, 4 ,7, 2, 6, 3, 1]

sorted_lst = sorted(nums)

print(f"Original list\t {nums}")
print(f"Sorted list\t {nums}")
print(f"Reverse Sorted list\t {sorted(nums, reverse=True)}")
Original list	 [8, 9, 5, 4, 7, 2, 6, 3, 1]
Sorted list	 [8, 9, 5, 4, 7, 2, 6, 3, 1]
Reverse Sorted list	 [9, 8, 7, 6, 5, 4, 3, 2, 1]
In [72]:
nums.sort()
print(f"Sorted list\t {nums}")
Sorted list	 [1, 2, 3, 4, 5, 6, 7, 8, 9]

Lists having Multiple reference

In [73]:
lst = [1, 2, 3, 4, 5]
abc = lst
abc.append(6)

# original list also got appended 6
print("Original list: ", lst)
Original list:  [1, 2, 3, 4, 5, 6]

String Split to create a list

In [74]:
s = "This is python notes"
s_lst = s.split()
print(s_lst)
['This', 'is', 'python', 'notes']

List Indexing

  • Each item in the list has an assigned index value starting from 0.
  • Accessing elements in a list is called indexing.
In [75]:
lst = [1, 2, 3, 4]
print(lst[1]) #print second element

# -ve indexing starts from end
print(lst[-2])
2
3

List Slicing

  • Accessing parts of segements is called slicing
  • [start: end: step]
  • start is inclusive
  • end is exclusive
  • step default to 1
In [76]:
numbers = [10, 20, 30, 40, 50,60,70,80]

#print all numbers
print(numbers[:]) 

#print from index 0 to index 3
print(numbers[0:4])

#print alternate elements in a list
print(numbers[::2])

#print elemnts start from 0 through rest of the list
print(numbers[2::2])
[10, 20, 30, 40, 50, 60, 70, 80]
[10, 20, 30, 40]
[10, 30, 50, 70]
[30, 50, 70]

List count

  • It returns the frequency of given item in a list
In [77]:
numbers = [1, 2, 3, 1, 3, 4, 1, 2, 1, 5]

#frequency of 1 in a list
print(f"Frequency of 1: {numbers.count(1)}")

#frequency of 3 in a list
print(f"Frequency of 3: {numbers.count(3)}")
Frequency of 1: 4
Frequency of 3: 2

List looping

In [78]:
lst = ['one', 'two', 'three', 'four', 'five']

for ele in lst:
    print(ele)
one
two
three
four
five

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.

In [79]:
# without list comprehension
squares = []
for i in range(10):
    squares.append(i**2)   #list append
print(squares)
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
In [80]:
#using list comprehension
squares = [i**2 for i in range(10)]
print(squares)
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
In [81]:
lst = [-10, -20, 10, 20, 50]

#create a new list with values doubled
new_lst = [i*2 for i in lst]
print(new_lst)

#filter the list to exclude negative numbers
new_lst = [i for i in lst if i >= 0]
print(new_lst)


#create a list of tuples like (number, square_of_number)
new_lst = [(i, i**2) for i in range(10)]
print(new_lst)
[-20, -40, 20, 40, 100]
[10, 20, 50]
[(0, 0), (1, 1), (2, 4), (3, 9), (4, 16), (5, 25), (6, 36), (7, 49), (8, 64), (9, 81)]
In [82]:
matrix = [
    [1, 2, 3, 4],
    [5, 6, 7, 8],
    [9, 10, 11, 12]
]

#transpose of a matrix without list comprehension
transposed = []
for i in range(4):
    lst = []
    for row in matrix:
        lst.append(row[i])
    transposed.append(lst)

print(transposed)
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]
In [83]:
#with list comprehension
transposed = [[row[i] for row in matrix] for i in range(4)]
print(transposed)
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]

Tuple

  • A tuple is similar to list
  • The diffence between the two is that we can't change the elements of tuple once it is assigned whereas in the list, elements can be changed

Tuple creation

In [84]:
#empty tuple
t = ()
t = tuple()

#tuple having integers
t = (1, 2, 3)
print(t)

#tuple with mixed datatypes
t = (1, 'delta', 28, 'omega')
print(t)

#nested tuple
t = (1, (2, 3, 4), [1, 'alpha', 28, 'omega'])
print(t)
(1, 2, 3)
(1, 'delta', 28, 'omega')
(1, (2, 3, 4), [1, 'alpha', 28, 'omega'])
In [85]:
#only parenthesis is not enough
t = ('python')
type(t)
Out[85]:
str
In [86]:
#need a comma at the end
t = ('python',)
type(t)
Out[86]:
tuple
In [87]:
#parenthesis is optional only if it has one element
t = "python", 
print(type(t))

print(t)
<class 'tuple'>
('python',)

Accesssing elements in tuple

In [88]:
t = ("Python", "Java", "C++", "Go", "TypeScript")

print(t[0])
print(t[-1])
print(t[2])
Python
TypeScript
C++

Slicing

In [89]:
t = (1, 2, 3, 4, 5, 6)

print(t[1:4])

#print elements from starting to 2nd last elements
print(t[:-2])

#print elements from starting to end
print(t[:])
(2, 3, 4)
(1, 2, 3, 4)
(1, 2, 3, 4, 5, 6)

Changing a tuple

  • unlike lists, tuples are immutable
  • This means that elements of a tuple cannot be changed once it has been assigned. But, if the element is itself a mutable datatype like list, its nested items can be changed.
In [90]:
t = (1, 2, 3, 4, [5, 6, 7])
In [91]:
t[4][1] = 'Python'
print(t)
(1, 2, 3, 4, [5, 'Python', 7])

Tuple concatenation

In [92]:
t = (1, 2, 3) + (4, 5, 6)
print(t)
(1, 2, 3, 4, 5, 6)
In [93]:
t = (('Python', ) * 4)
print(t)
('Python', 'Python', 'Python', 'Python')

Tuple deletion

  • del is used to delete the tuple
In [94]:
#delete entire tuple using del keyword
t = (1, 2, 3, 4, 5, 6)

del t

Tuple count

  • count() method returns the frequency of the given element
In [95]:
numbers = (1, 2, 3, 1, 3, 4, 1, 2, 1, 5)

#frequency of 1 in a tuple
print(f"Frequency of 1: {numbers.count(1)}")

#frequency of 3 in a tuple
print(f"Frequency of 3: {numbers.count(3)}")
Frequency of 1: 4
Frequency of 3: 2

Tuple index

  • index() method returns the index of first elements that is equal to the specified value
In [96]:
t = (1, 2, 3, 1, 3, 3, 4, 1)

print(t.index(3))
2

Tuple membership

In [97]:
t = (1,2,3,4,5,6,7,8,9)
print(1 in t)
print(0 in t)
True
False

Tuple length

  • len() function returns the length of tuple
In [98]:
t = (1,2,3,4,5,6,7,8)
print(len(t))
8

Tuple sort

  • sorted() function is returns the sorted list
In [99]:
t = (2,1,3,5,8,6,7,4)
sorted_t = sorted(t)
print(f"Original tuple:\t {t}")
print(f"Sported list:\t {sorted_t}")
Original tuple:	 (2, 1, 3, 5, 8, 6, 7, 4)
Sported list:	 [1, 2, 3, 4, 5, 6, 7, 8]

Sets

  • A set is an unordered collection of items. Every element is unique (no duplicates).
  • The set itself is mutable. We can add or remove items from it.
  • Sets can be used to perform mathematical set operations like union, intersection, symmetric difference etc.

Set Creation

  • {} empty curly bases represnt the dictionary not set.
In [100]:
s = set()
s = {1,2,3}
In [101]:
s = set([1,2,3,2,1,2,2,3,4,5])
print(s)
{1, 2, 3, 4, 5}

Add element to set

  • add() method to add the single element to set
  • update() method to add multiple elements to set
In [102]:
s = {1,3,4}

s.add(2)
print(s)
{1, 2, 3, 4}
In [103]:
s.update([5,6,7,2,3])
print(s)
{1, 2, 3, 4, 5, 6, 7}
In [104]:
s.update([8,9], {0,1,2})
print(s)
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

Remove elements from a set

  • discard() method removes the specified item
  • remove() method removes the specified item. It throws error if the specified item is not present in set
  • pop() method removes random element from set
  • clear() method removes all the items in a set
In [105]:
s = {1,2,3,4,5,6}
print(s)
{1, 2, 3, 4, 5, 6}
In [106]:
s.discard(5)
print(s)
{1, 2, 3, 4, 6}
In [107]:
s.remove(2)
print(s)
{1, 3, 4, 6}
In [108]:
s.pop()
print(s)
{3, 4, 6}
In [109]:
s = {1, 2, 3, 4, 5}
s.clear()
print(s)
set()

Set operations [Union]

  • | or union() method
In [110]:
set1 = {1, 2, 3, 4, 5}
set2 = {3, 4, 5, 6, 7}
In [111]:
print(set1 | set2)
{1, 2, 3, 4, 5, 6, 7}
In [112]:
print(set1.union(set2))
{1, 2, 3, 4, 5, 6, 7}

Set operations [Intersection]

  • & or intersection() method
In [113]:
print(set1 & set2)
{3, 4, 5}
In [114]:
print(set1.intersection(set2))
{3, 4, 5}

Set operations [Difference]

  • set of elements that are only in set1 but not in set2
  • - or difference() method
In [115]:
print(set1 - set2)
{1, 2}
In [116]:
print(set1.difference(set2))
{1, 2}

Set operations [Symmetric difference]

  • set of elements in both set1 and set2 except those that are common in both.
  • ^ or symmetric_difference() method
In [117]:
print(set1 ^ set2)
{1, 2, 6, 7}
In [118]:
print(set1.symmetric_difference(set2))
{1, 2, 6, 7}

Set Operations [Subset]

  • issubset() method
In [119]:
#find issubset()
x = {"a","b","c","d","e"}
y = {"c","d"}


#check x is subset of y
print(f"set 'x' is subset of 'y' ? {x.issubset(y)}") 

#check y is subset of x
print(f"set 'y' is subset of 'x' ? { y.issubset(x)}")
set 'x' is subset of 'y' ? False
set 'y' is subset of 'x' ? True

Frozen Sets

Frozen sets has the characteristics of sets, but we can't be changed once it's assigned. While tuple are immutable lists, frozen sets are immutable sets

Frozensets can be created using the function frozenset()

Sets being mutable are unhashable, so they can't be used as dictionary keys. On the other hand, frozensets are hashable and can be used as keys to a dictionary.

This datatype supports methods like copy(), difference(), intersection(), isdisjoint(), issubset(), issuperset(), symmetric_difference() and union(). Being immutable it does not have method that add or remove elements.

In [120]:
set1 = frozenset([1, 2, 3, 4])
set2 = frozenset([3, 4, 5, 6])
In [121]:
print(f"Union: {set1 | set2}")
print(f"Intersection: {set1 & set2}")
print(f"Symmetric difference: {set1 ^ set2}")
Union: frozenset({1, 2, 3, 4, 5, 6})
Intersection: frozenset({3, 4})
Symmetric difference: frozenset({1, 2, 5, 6})

Dictionary

Python dictionary is an unordered collection of items. While other compound data types have only value as an element, a dictionary has a key: value pair.

Dictionary Creation

In [122]:
#empty dictionary
my_dict = {}
my_dict = dict()

#dictionary with integer keys
my_dict = {1: 'abc', 2: 'xyz'}
print(my_dict)

#dictionary with mixed keys
my_dict = {'name': 'satish', 1: ['abc', 'xyz']}
print(my_dict)


#create empty dictionary using dict()
my_dict = dict()

my_dict = dict([(1, 'abc'), (2, 'xyz')])    #create a dict with list of tuples
print(my_dict)
{1: 'abc', 2: 'xyz'}
{'name': 'satish', 1: ['abc', 'xyz']}
{1: 'abc', 2: 'xyz'}

Accessing the dictionary

In [123]:
my_dict = {'name': 'Python', 'version': 3.8, 'address': 'python.org'}

#get name
print(my_dict['name'])
print(my_dict.get('address'))
print(my_dict['version'])
Python
python.org
3.8

Add or modify elements

In [124]:
my_dict = {'name': 'Python', 'age': 29, 'address': 'python.org'}
print(my_dict)
{'name': 'Python', 'age': 29, 'address': 'python.org'}
In [125]:
my_dict['name'] = 'Python 3'
my_dict['Version'] = 3.8
In [126]:
print(my_dict)
{'name': 'Python 3', 'age': 29, 'address': 'python.org', 'Version': 3.8}

Delete or remove element

  • pop() method removes the specified item
  • popitem() method removes the random item
  • clear() removes all the items
  • del removes the specified item. if not specified it removes the variable from memory
In [127]:
my_dict = {'name': 'Python', 'version': 3.8, 'address': 'python.org'}

my_dict.pop('version')
print(my_dict)
{'name': 'Python', 'address': 'python.org'}
In [128]:
my_dict = {'name': 'Python', 'version': 3.8, 'address': 'python.org'}

my_dict.popitem()

print(my_dict)
{'name': 'Python', 'version': 3.8}
In [129]:
squares = {2: 4, 3: 9, 4: 16, 5: 25}

#delete particular key
del squares[2]

print(squares)
{3: 9, 4: 16, 5: 25}
In [130]:
#remove all items
squares.clear()

print(squares)
{}
In [131]:
squares = {2: 4, 3: 9, 4: 16, 5: 25}

#delete dictionary itself
del squares

# print(squares) #NameError because dict is deleted

Copy

In [132]:
squares = {2: 4, 3: 9, 4: 16, 5: 25}

my_dict = squares.copy()
print(my_dict)
{2: 4, 3: 9, 4: 16, 5: 25}

Generate a Dictionary from sequence

  • fromkeys[seq[, v]] Return a new dictionary with keys from seq and value equal to v (defaults to None).
In [133]:
subjects = {}.fromkeys(['Math', 'Science', 'Social'], 0)
print(subjects)
{'Math': 0, 'Science': 0, 'Social': 0}

accessing keys, values and both

  • keys() method returns all the keys in a dict
  • values() method returns all the values in a dict
  • items() method returns all the key value pairs in a dict
In [134]:
squares = {2:4, 3:9, 4:16, 5:25}
print(squares.items())
dict_items([(2, 4), (3, 9), (4, 16), (5, 25)])
In [135]:
squares = {2:4, 3:9, 4:16, 5:25}
print(squares.keys())
dict_keys([2, 3, 4, 5])
In [136]:
squares = {2:4, 3:9, 4:16, 5:25}
print(squares.values())
dict_values([4, 9, 16, 25])

Dictionary Comprehension

  • Dict comprehensions are just like list comprehensions but for dictionaries it need two temporary variables one for key and one for value
In [137]:
d = {'a': 1, 'b': 2, 'c': 3}
for pair in d.items():
    print(pair)
('a', 1)
('b', 2)
('c', 3)
In [138]:
#Creating a new dictionary with only pairs where the value is larger than 2
d = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
new_dict = {k:v for k, v in d.items() if v > 2}
print(new_dict)
{'c': 3, 'd': 4}
In [139]:
d = {'a':1,'b':2,'c':3,'d':4,'e':5}
d = {k + 'c':v * 2 for k, v in d.items() if v > 2}
print(d)
{'cc': 6, 'dc': 8, 'ec': 10}

Strings

  • In Python, string is a sequence of Unicode character.

Create a string

  • Strings can be created by enclosing characters inside a single quote or double quotes.
  • Even triple quotes can be used in Python but generally used to represent multiline strings and docstrings.
In [140]:
my_string = 'Hello'

print(my_string)


my_string = "Hello"
print(my_string)


my_string = '''Hello'''
print(my_string)
Hello
Hello
Hello

Accessing a character in a string

  • We can access individual characters using indexing and a range of characters using slicing.

Index starts from 0.

  • Trying to access a character out of index range will raise an IndexError.
  • The index must be an integer. We can't use float or other types, this will result into TypeError.
  • Python allows negative indexing for its sequences.
In [141]:
my_string = "Hello"

#print first Character
print(my_string[0])

#print last character using negative indexing
print(my_string[-1])

#slicing 2nd to 5th character
print(my_string[2:5])
H
o
llo

Delete a string

del deletes the complete string

In [142]:
my_string = "Hello"
del my_string

Concatenation

  • Joining of two or more strings into a single one is called concatenation.
  • The + operator does this in Python. Simply writing two string literals together also concatenates them.
  • The * operator can be used to repeat the string for a given number of times.
In [143]:
s1 = "Hello "
s2 = "Oliver"

#concatenation of 2 strings
print(s1 + s2)

#repeat string n times
print(s1 * 3)
Hello Oliver
Hello Hello Hello 

Iterating through string

In [144]:
count = 0
for l in "Hello World":
    if l == 'o':
        count += 1
print(count, ' letters found')
2  letters found

String Membership

  • in operator to test membership
In [145]:
print('l' in 'Hello World')
True

String methods

  • Some of the commonly used methods are lower(), upper(), join(), split(), find(), replace() etc
In [146]:
"Hello".lower()
Out[146]:
'hello'
In [147]:
"Hello".upper()
Out[147]:
'HELLO'
In [148]:
"hello".capitalize()
Out[148]:
'Hello'
In [149]:
x = "This will split all words in a list".split()
print(x)
['This', 'will', 'split', 'all', 'words', 'in', 'a', 'list']
In [150]:
' '.join(x)
Out[150]:
'This will split all words in a list'
In [151]:
"Good Morning".find("Mo")
Out[151]:
5
In [152]:
s1 = "Bad morning"

s2 = s1.replace("Bad", "Good")

print(s1)
print(s2)
Bad morning
Good morning
In [153]:
### Palindrome program
myStr = "Madam"

#convert entire string to either lower or upper
myStr = myStr.lower()

#reverse string
revStr = reversed(myStr)


#check if the string is equal to its reverse
if list(myStr) == list(revStr):
    print("Given String is palindrome")
else:
    print("Given String is not palindrome")
Given String is palindrome
In [154]:
### Sort words in Alphabetic order
myStr = "python Program to Sort words in Alphabetic Order"

#breakdown the string into list of words
words = myStr.split()

#sort the list
words.sort()

#print Sorted words are
for word in words:
    print(word)
Alphabetic
Order
Program
Sort
in
python
to
words

Functions

Function is a group of related statements that perform a specific task.

Functions help break our program into smaller and modular chunks. As our program grows larger and larger, functions make it more organized and manageable.

It avoids repetition and makes code reusable.

Syntax:

def function_name(parameters):
    """
    Doc String
    """

    Statement(s)
  1. keyword def marks the start of function header

  2. Parameters (arguments) through which we pass values to a function. These are optional

  3. A colon(:) to mark the end of funciton header

  4. Doc string describe what the function does. This is optional

  5. return statement to return a value from the function. This is optional

In [155]:
# Function definition
def print_name(name):
    """ 
    This function prints the name
    """
    print("Hello " + str(name)) 
In [156]:
# function call
print_name("Python")
Hello Python

Doc String

  • The first string after the function header is called the docstring and is short for documentation string.
  • Although optional, documentation is a good programming practice, always document your code
  • Doc string will be written in triple quotes so that docstring can extend up to multiple lines
In [157]:
print(print_name.__doc__) # print doc string of the function
 
    This function prints the name
    

return Statement

  • The return statement is used to exit a function and go back to the place from where it was called.
  • return statement can contain an expression which gets evaluated and the value is returned.
  • if there is no expression in the statement or the return statement itself is not present inside a function, then the function will return None Object.
In [158]:
# Function defintion
def get_sum(lst):
    """
    This function returns the sum of all the elements in a list
    """
    #initialize sum
    _sum = 0
    
    #iterating over the list
    for num in lst:
        _sum += num

    return _sum
In [159]:
# function call
s = get_sum([1,2,3,4,5,6,7])
print(f"Sum of given list is{s}")
Sum of given list is28
In [160]:
print(get_sum.__doc__)
    This function returns the sum of all the elements in a list
    
In [161]:
def compute_HCF(a, b):
    """
    Computing HCF of two numbers
    """
    smaller = b if a > b else a  #consice way of writing if else statement
    
    hcf = 1
    for i in range(1, smaller+1):
        if (a % i == 0) and (b % i == 0):
            hcf = i
    return hcf


num1 = 6
num2 = 36
print(f"H.C.F of {num1} and {num2} is: {compute_HCF(num1, num2)}")
H.C.F of 6 and 36 is: 6

Types of functions

  1. Built-in Functions ex: print(), abs(), filter(), divmod(), type(), dir() etc.
  2. User-defined Functions
    • Functions that we define ourselves to do certain specific task are referred as user-defined functions
    • If we use functions written by others in the form of library, it can be termed as library functions.

Advantages of User-defined functions

  1. User-defined functions help to decompose a large program into small segments which makes program easy to understand, maintain and debug.
  2. If repeated code occurs in a program. Function can be used to include those codes and execute when needed by calling that function.
  3. Programmars working on large project can divide the workload by making different functions.
In [162]:
# one more example
def product_numbers(a, b):
    """
    this function returns the product of two numbers
    """
    product = a * b
    return product

num1 = 10
num2 = 20
print (f"product of {num1} and {num2} is {product_numbers(num1, num2)}")
product of 10 and 20 is 200

Different types of Parameters/arguments in a functions

  1. Positional arguments
  2. Default arguments
  3. Arbitrary arguments
  4. Key Word arguments

Arguments should be in the above defined order while defining a function

In [163]:
# positional argument example
def greet(name, msg):
    """
    This function greets to person with the provided message
    """
    print("Hello {0} , {1}".format(name, msg))
    
greet("Dave", "Good Morning")
Hello Dave , Good Morning
In [164]:
# Default argument example
def greet(name, msg="Good Morning"):
    """
    This function greets to person with the provided message
    """
    print("Hello {0} , {1}".format(name, msg))
    
greet("Dave")

greet("Dave", "How are you?")
Hello Dave , Good Morning
Hello Dave , How are you?
In [165]:
def greet(*names):
    """
    This function greets all persons in the names tuple 
    """
#     print(names)
    
    for name in names:
        print(f"Hello,  {name}.")

greet("Adya", "Aditi", "Ananya", "Ankit", "Sonal")
Hello,  Adya.
Hello,  Aditi.
Hello,  Ananya.
Hello,  Ankit.
Hello,  Sonal.

Recursive Function

  • A function can call other functions. It is even possible for the function to call itself.
  • They are known as recursive functions.
In [166]:
# print factorial of a number using recurion
def factorial(num):
    """
    This is a recursive function to find the factorial of a given number
    """
    return 1 if num == 1 else (num * factorial(num-1))

num = 5
print(f"Factorial of {num} is {factorial(num)}")
Factorial of 5 is 120
In [167]:
def fibonacci(num):
    """
    Recursive function to print fibonacci sequence
    """
    return num if num <= 1 else fibonacci(num-1) + fibonacci(num-2)

nterms = 10
print("Fibonacci sequence")
for num in range(nterms):
    print(fibonacci(num), end="\t")
Fibonacci sequence
0	1	1	2	3	5	8	13	21	34	

Advantages of Recursive function

  1. Recursive functions make the code look clean and elegant.
  2. A complex task can be broken down into simpler sub-problems using recursion.
  3. Sequence generation is easier with recursion than using some nested iteration.

Disadvantages of Recursive function

  1. Sometimes the logic behind recursion is hard to follow through.
  2. Recursive calls are expensive (inefficient) as they take up a lot of memory and time.
  3. Recursive functions are hard to debug.

Lambda or Anonymous Functions

  • In Python, anonymous function is a function that is defined without a name.
  • While normal functions are defined using the def keyword, in Python anonymous functions are defined using the lambda keyword.
  • Lambda functions are used extensively along with built-in functions like filter(), map()
In [168]:
# def double(x):
#     return x*2
# The above commented function rewrite as a lambda functoin 
double = lambda x: x *2
In [169]:
print(double(6))
12
In [170]:
lst = [1, 2, 3, 4, 5]
even_lst = list(filter(lambda x: (x%2 == 0), lst))
print(even_lst)
[2, 4]
In [171]:
from functools import reduce

lst = [1, 2, 3, 4, 5]
product_lst = reduce(lambda x, y: x*y, lst)
print(product_lst)
120

Modules

  • Modules refer to a file containing Python statements and definitions.
    • A file containing Python code, for e.g.: abc.py, is called a module and its module name would be "abc".
  • We use modules to break down large programs into small manageable and organized files. Furthermore, modules provide reusability of code.
  • We can define our most used functions in a module and import it, instead of copying their definitions into different programs.

Import the module

  • We import the modules using import keyword
  • Using the module name we can access the function using dot . operation.
In [172]:
import math

print(f"The value of PI: {math.pi:.3f}")
print(f"The value of e: {math.e:.3f}")
The value of PI: 3.142
The value of e: 2.718

Import with renaming

In [173]:
import datetime as dt

dt.datetime.now()
Out[173]:
datetime.datetime(2020, 4, 20, 2, 16, 42, 682697)
In [174]:
#  we can import specific names without importing everything
from math import pi, e

print(f"The value of PI: {pi:.3f}")
print(f"The value of e: {e:.3f}")
The value of PI: 3.142
The value of e: 2.718

dir() function

We can use the dir() function to find out names that are defined inside a module.

In [175]:
# functions in datetime module
dir(dt)
Out[175]:
['MAXYEAR',
 'MINYEAR',
 '__builtins__',
 '__cached__',
 '__doc__',
 '__file__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 'date',
 'datetime',
 'datetime_CAPI',
 'sys',
 'time',
 'timedelta',
 'timezone',
 'tzinfo']

Packages

  • Packages are a way of structuring Python’s module namespace by using “dotted module names”.
  • A directory must contain a file named init.py in order for Python to consider it as a package. This file can be left empty but we generally place the initialization code for that package in this file.
  • We can import modules from packages using the dot (.) operator.

Package directory structure

# from the above package structure
import Game.Level.start

File Handling

  • File is a named location on disk to store related information. It is used to permanently store data in a non-volatile memory (e.g. hard disk).
  • Since, random access memory (RAM) is volatile which loses its data when computer is turned off, we use files for future use of the data.
  • When we want to read from or write to a file we need to open it first. When we are done, it needs to be closed, so that resources that are tied with the file are freed.

File operation:

  1. Open a file
  2. Read or write (perform operation)
  3. Close the file
  • Python has a built-in function open() to open a file. This function returns a file object, also called a handle, as it is used to read or modify the file accordingly.

  • We can specify the mode while opening a file. In mode, we specify whether we want to read 'r', write 'w' or append 'a' to the file. We also specify if we want to open the file in text mode or binary mode.

Mode Mode Description
'r' Open a file for reading. (default)
'w' Open a file for writing. Creates a new file if it does not exist or truncates the file if it exists.
'x' Open a file for exclusive creation. If the file already exists, the operation fails.
'a' Open for appending at the end of the file without truncating it. Creates a new file if it does not exist.
't' Open in text mode. (default)
'b' Open in binary mode.
'+' Open a file for updating (reading and writing)
  • The default encoding is platform dependent. In windows, it is 'cp1252' but 'utf-8' in Linux.
  • So, we must not also rely on the default encoding or else our code will behave differently in different platforms.
  • Hence, when working with files in text mode, it is highly recommended to specify the encoding type.
  • Closing a file will free up the resources that were tied with the file and is done using the close() method.
  • Python has a garbage collector to clean up unreferenced objects but, we must not rely on it to close the file.

    But more often everyone we forget to close the file. So, the community recommends to use with

In [176]:
# writing to a file
with open("test.txt",'w', encoding='utf-8') as f:
   f.write("We are writing this file from Python\n")
   f.write("Open the file using with statement\n")
   f.write("The good thing about with is it automatically closes when the block inside with is exited.\n")
In [177]:
# reading from a file
with open('test.txt', 'r', encoding='utf-8') as f:
    # Read the whole file from the cursor position
    s = f.read()
    # moving cursor to the initial position
    f.seek(0)
    # reading line by line 
    print(f.readline())
    
print(s)
We are writing this file from Python

We are writing this file from Python
Open the file using with statement
The good thing about with is it automatically closes when the block inside with is exited.

Exception Handling

When writing a program, we, more often than not, will encounter errors. Error caused by not following the proper structure (syntax) of the language is called syntax error or parsing error.

Errors can also occur at runtime and these are called exceptions.

They occur, for example, when a file we try to open does not exist (FileNotFoundError), dividing a number by zero (ZeroDivisionError), module we try to import is not found (ImportError) etc.

Whenever these type of runtime error occur, Python creates an exception object. If not handled properly, it prints a traceback to that error along with some details about why that error occurred and terminates program.

Try, except, finally

Python has many built-in exceptions which forces your program to output an error when something in it goes wrong.

When these exceptions occur, it causes the current process to stop and passes it to the calling process until it is handled. If not handled, our program will crash.

For example, if function A calls function B which in turn calls function C and an exception occurs in function C. If it is not handled in C, the exception passes to B and then to A. If never handled, an error message is spit out and our program come to a sudden unexpected halt.

Exception Handling

  • raise allows you to throw an exception at any time.
  • assert enables you to verify if a certain condition is met and throw an exception if it isn’t.
  • In the try clause, all statements are executed until an exception is encountered.
  • except is used to catch and handle the exception(s) that are encountered in the try clause.
  • else lets you code sections that should run only when no exceptions are encountered in the try clause.
  • finally enables you to execute sections of code that should always run, with or without any previously encountered exceptions.
In [178]:
import sys

lst = ['b', 0, 2]

for entry in lst:
    try:
        print("The entry is", entry)
        r = 1 / int(entry)
    except:
        print("Oops!", sys.exc_info()[0],"occured.")
        print("Next entry.")
        print("***************************")
        
print(f"The reciprocal of {entry} is {r}")
The entry is b
Oops! <class 'ValueError'> occured.
Next entry.
***************************
The entry is 0
Oops! <class 'ZeroDivisionError'> occured.
Next entry.
***************************
The entry is 2
The reciprocal of 2 is 0.5
In [179]:
#  catching specific exception
lst = ['b', 0, 2]

for entry in lst:
    try:
        print("****************************")
        print("The entry is", entry)
        r = 1 / int(entry)
    except(ValueError):
        print("This is a ValueError.")
    except(ZeroDivisionError):
        print("This is a ZeroError.")
    except:
        print("Some other error")
        
print("The reciprocal of", entry, "is", r)
****************************
The entry is b
This is a ValueError.
****************************
The entry is 0
This is a ZeroError.
****************************
The entry is 2
The reciprocal of 2 is 0.5
In [180]:
# Raise exception
try:
    num = int(input("Enter a positive integer:"))
    if num <= 0:
        raise ValueError("Error:Entered negative number")
except ValueError as e:
    print(e)
Error:Entered negative number
In [181]:
try:
    f = open('test.txt')   
finally:
    f.close()

Object oriented Programming

Everything in Python is an Object

x = 1
help(x)
dir(x)

Object : It is an instance of class. Objects combine functions with data ex: List

Class

Class is the blue print of an object. Which contains attributes and methods

User defined objects are created using the class keyword. The class is a blueprint that defines the nature of a future object. From classes we can construct instances. An instance is a specific object created from a particular class. For example, above we created the object x which was an instance of a int object.

  • By convention the class name starts with a capital letter.
  • Inside of the class we can define class attributes and methods.
  • An attribute is a characteristic of an object. A method is an operation we can perform with the object.
In [182]:
class Patient:
    """Medical centre patient"""
    pass

x = Patient()

Methods

Methods are functions defined inside the body of a class. They are used to perform operations with the attributes of our objects. Methods are a key concept of the OOP paradigm. They are essential to dividing responsibilities in programming, especially in large applications.

__init__()

  • It is also called a constructor function.
  • It calls automatically whenever the object is created
  • The function takes one mandatory parameter which is called self. Here self refers to the object itself.
  • The variable created by the instructor are unique to each instance.

  • Class wide variables/attributes are same for any instance. In our example status is class wide attribute

In [183]:
class Patient:
    """
        Attribuets
        ----------
        name: Patient Name
        age: Patient age
        conditions: Existing Medical conditions
    """
    
    status = "patient"
    
    def __init__(self, name, age):
        self.name = name
        self.age = age
        self.conditions = []
        
    def get_details(self):
        print(f"Patient record: {self.name}, {self.age} years, Current.Info: {self.conditions if self.conditions else 'nil'}.")
        
    def add_info(self, information):
        self.conditions.append(information)
In [184]:
john = Patient("John", 29)
dave = Patient("Dave", 34)
finn = Patient("Finn", 40)

# print(f"{john.name:8} {john.age:3} {john.status}")
# print(f"{dave.name:8} {dave.age:3} {dave.status}")

john.add_info("Treated for Fever - Paracetamol prescribed")
finn.add_info("Symptoms: Fever, Cough, Headache")
finn.add_info("Suggested: 2 days observation")

john.get_details()
finn.get_details()
dave.get_details()
Patient record: John, 29 years, Current.Info: ['Treated for Fever - Paracetamol prescribed'].
Patient record: Finn, 40 years, Current.Info: ['Symptoms: Fever, Cough, Headache', 'Suggested: 2 days observation'].
Patient record: Dave, 34 years, Current.Info: nil.
In [185]:
john.status
Out[185]:
'patient'

Inherirtance

Inheritance is a way to form new classes using classes that have already been defined. The newly formed classes are called derived classes, the classes that we derive from are called base classes. Important benefits of inheritance are code reuse and reduction of complexity of a program. The derived classes (descendants) override or extend the functionality of base classes (ancestors).

Sometimes it makes sense for a derived class to inherit qualities from two or more base classes. Python allows for this with multiple inheritance.

Things get complicated when you have several base classes and levels of inheritance. This is resolved using Method Resolution Order - a formal plan that Python follows when running object methods.

  • super() it refers to the parent class.
In [186]:
class Infant(Patient):
    """Patient under 2 years
        Attribuets
        ----------
        name: Patient Name
        age: Patient age
        vaccinations: Vaccinations given
    """
    
    def __init__(self, name, age):
        self.vaccinations = []
        super().__init__(name, age)
        
    def add_vac(self, vaccine):
        self.vaccinations.append(vaccine)
        
    def get_details(self):
        s = f"""****************************************************
        Patient record: {self.name}, {self.age} years
        Patient has had {self.vaccinations if self.vaccinations else None} vaccines
        {self.name} IS AN INFANT, HAS HE HAD ALL HIS CHECKS?\n\n"""
        
        print(s)
In [187]:
ted = Infant("Ted", 1)
ted.add_vac("MMR")

ted.get_details()
****************************************************
        Patient record: Ted, 1 years
        Patient has had ['MMR'] vaccines
        Ted IS AN INFANT, HAS HE HAD ALL HIS CHECKS?


Speical Methods

  • __repr__: The “official” string representation of an object. This is how you would make an object of the class. The goal of repr is to be unambiguous.
  • __str__: The “informal” or nicely printable string representation of an object. This is for the enduser. etc.

REFERENCE 1

REFERENCE 2

In [188]:
class BankAccount:
    """
    BANK ACCOUNT
        Attributes
        ----------
        balance: Amount left in the account
    """
    def __init__(self, balance=0.0):
        self.balance = balance
        
    def display_balance(self):
        print(f"Your balance is {self.balance}")
        
    def make_deposit(self):
        amount = float(input("How much would you like to deposit? $"))
        self.balance += amount
        print(f"DEPOSIT SUCCESSFUL: Now your balance is: {self.balance}")
        
    def make_withdrawl(self):
        amount = float(input("How much would you like to withdraw? $"))
        if amount >= self.balance:
            print(f"You dont have sufficient funds, Your balance is {self.balance}")
        else:
            self.balance -= amount
            print(f"WITHDRAWL SUCCESSFUL: Now your balance is: {self.balance}")
            
    def __str__(self):
        return f"Your balance is {self.balance}"
In [189]:
my_bank = BankAccount(500)
In [190]:
print(my_bank)
Your balance is 500
In [191]:
my_bank.make_withdrawl()
WITHDRAWL SUCCESSFUL: Now your balance is: 200.0
In [192]:
my_bank.make_deposit()
DEPOSIT SUCCESSFUL: Now your balance is: 450.99

Decorators

  • Decorators can be thought of as functions which modify the functionality of another function. They help to make your code shorter and more "Pythonic".
  • It is also called MetaProgramming.
In [193]:
def new_decorator(func):
    def wrap_func():
        print("Code would be here, before executing the func")

        func()

        print("Code here will execute after the func()")

    return wrap_func
In [194]:
@new_decorator
def func_needs_decorator():
    print("This function is in need of a Decorator")
In [195]:
func_needs_decorator()
Code would be here, before executing the func
This function is in need of a Decorator
Code here will execute after the func()
In [196]:
def smart_divide(func):
   def inner(a,b):
      print("I am going to divide",a,"and",b)
      if b == 0:
         print("Whoops! cannot divide")
         return

      return func(a,b)
   return inner

@smart_divide
def divide(a,b):
    return a/b
In [197]:
print(divide(2, 5))
print(divide(2, 0))
I am going to divide 2 and 5
0.4
I am going to divide 2 and 0
Whoops! cannot divide
None
In [198]:
def star(func):
    def inner(*args, **kwargs):
        print("*" * 30)
        func(*args, **kwargs)
        print("*" * 30)
    return inner

def percent(func):
    def inner(*args, **kwargs):
        print("%" * 30)
        func(*args, **kwargs)
        print("%" * 30)
    return inner
In [199]:
@star
@percent
def printer(msg):
    print(msg)
printer("Hello")
******************************
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Hello
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
******************************
In [200]:
@percent
@star
def printer(msg):
    print(msg)
printer("Hello")
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
******************************
Hello
******************************
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

Iterators and Generators

  • Generators simplifies creation of iterators. A generator is a function that produces a sequence of results instead of a single value
  • Generators allow us to generate as we go along, instead of holding everything in memory. ex: range()
  • Each time the yield statement is executed the function generates a new value.

  • In most aspects, a generator function will appear very similar to a normal function. The main difference is when a generator function is compiled they become an object that supports an iteration protocol.

  • The main advantage here is that instead of having to compute an entire series of values up front, the generator computes one value and then suspends its activity awaiting the next instruction. This feature is known as state suspension.
In [201]:
def gen_cubes(n):
    for num in range(n):
        yield num ** 3
In [202]:
for x in gen_cubes(10):
    print(x, end="\t")
0	1	8	27	64	125	216	343	512	729	
In [203]:
def integers():
    """Infinite sequence of integers."""
    i = 1
    while True:
        yield i
        i = i + 1

def squares():
    for i in integers():
        yield i * i

def take(n, seq):
    """Returns first n values from the given sequence."""
    seq = iter(seq)
    result = []
    try:
        for i in range(n):
            result.append(next(seq))
    except StopIteration:
        pass
    return result
In [204]:
print(take(10, squares()))
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
In [205]:
def simple_gen():
    for x in range(3):
        yield x
In [206]:
g = simple_gen()
print(next(g))
print(next(g))
print(next(g))
print(next(g))
0
1
2
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-206-a14aa3b61e3f> in <module>
      3 print(next(g))
      4 print(next(g))
----> 5 print(next(g))

StopIteration: 

Generator expression

In [207]:
a = (x*x for x in range(11))
sum(a)
Out[207]:
385
In [ ]: