When working with any programming language, you include comments in the code to notate your work. This details what certain parts of the code are for, and lets other developers – you included – know what you were up to when you wrote the code. This is a necessary practice, and good developers make heavy use of the comment system. Without it, things can get real confusing, real fast.
Comments are used to write things in a program that are not executed. They are used to add readability to the program, and allow the programmer to add "comments" to the code for the reader of the code.
#This is a comment in Python
"""This is not a comment."""
'This is not a comment.'
A function is a block of code that can be called with a single line. We alias the block of code(a set of statements) with a name, and then we can use it when needed by calling the function (referencing the name) of that block of code. This allows for reusability of code.
def my_func():
print("This is a function.")
print("I do not need to type this code again and again now.")
def is a Python keyword that identifies that what follows is a function name and definition.
Note that no output is generated for the above code. That is becuase the above statements only define that my_func refers to the statements in the following code block.
Also notice how the indentation is crucial here. The moment the indentation ends, (i.e. the next line does not have the indent), the function definition is said to have been completed.
my_func()
This is a function. I do not need to type this code again and again now.
To execute the statements in my_func, we call it.
The way to differentiate a variable from a function is the round brackets () following the name.
Let's consider that we want to compare 2 numbers, and print the square of the first one if it is bigger, or the cube of the second one if that one is bigger. What do we do?
a = 5
b = 10
if a>b:
print(a*a)
else:
print(b*b*b)
1000
Now, we can convert this into a function, but will we always use the same numbers for our decision? What if we want to compare 2 numbers that we calculated after some processing? How can we make functions dynamic to use values we provide them with?
Arguments are values we can give the function, so that it can operate based on those values. This allows functions to have a wide range of usability.
The variables that are used as arguments have to be defined during function declaration.
def compare_nums(a,b):
if a>b:
print(a*a)
else:
print(b*b*b)
The arguments are mentioned inside the brackets. They become variables that get their value when we call the function. We can use them and base the internal execution of the function on those arguments.
compare_nums(5,10)
1000
As shown above, we can pass the values to the function inside the round brackets.
The order of the arguments in the function definition is maintained, hence the first value passed to it will be the first argument mentioned in the definition of the function, the second one will be second and so on.
An added layer of functionality is default arguments. We can set default values to variables which we take as arguments in a function, so that if the user of the function does not intend to provide that value, we can function assuming the default value for that function.
def default_arguments_compare_nums(a=5,b=10):
if a>b:
print(a*a)
else:
print(b*b*b)
compare_nums()
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-9-17e897044b95> in <module>() ----> 1 compare_nums() TypeError: compare_nums() missing 2 required positional arguments: 'a' and 'b'
default_arguments_compare_nums()
1000
We can only provide values for a few of the default arguments as well!
default_arguments_compare_nums(15)
225
We can have a combination of normal required arguments and default arguments as well. One important syntactical note:
def three_arg(a, b=10, c=15):
print(a,b,c)
def three_arg(b=10,a,c=15):
print(a,b,c)
File "<ipython-input-13-871aa285f19b>", line 1 def three_arg(b=10,a,c=15): ^ SyntaxError: non-default argument follows default argument
In short, required arguments are compulsory, and need to be given. Default arguments can be ommitted.
Python allows functions to be called using keyword arguments. When we call functions in this way, the order (position) of the arguments can be changed. Following calls to the above function are all valid and produce the same result.
def keyword_args_compare_nums(a=5,b=10):
if a>b:
print(a*a)
else:
print(b*b*b)
keyword_args_compare_nums(a=5, b=10)
1000
keyword_args_compare_nums(b=10)
1000
keyword_args_compare_nums(b=10, a=5)
1000
You can mix positional arguments and keyword arguments in a function call as well. Just ensure all keyword arguments are after the positional arguments.
When we do not know the number of arguments to expect in a function, we use an asterisk (*) before the parameter name to denote an arbitrary number of arguments.
def arbit_args(*args):
print(type(args))
for arg in args:
print(arg)
arbit_args("A","B",1,2,3,44.5, True)
<class 'tuple'> A B 1 2 3 44.5 True
As we can see, the arguments are packed into a tuple, and given to the function.
Now, you may want to use a function to do some processing for you, and give back to you the result of that. That is made possible using return values.
Each function can return values, using the return keyword.
def find_units_dig(num):
unit_dig = num % 10
#print(unit_dig)
return unit_dig
return_val = find_units_dig(11)
print(return_val)
1
In Python you can return multiple values as well. It will automatically put all the return values in a tuple, and return it.
def ret_multiple_vals():
return 1,3,5,7,9
ret_val = ret_multiple_vals()
type(ret_val)
tuple
For any tuple, we can unpack it, by assigning each of it's elements to individual variables. But, instead of accessing each element individually by indexing, we can do it in one line by using tuple unpacking.
a,b,c,d,e = ret_multiple_vals()
print(a,b,c,d,e)
1 3 5 7 9
Note that if the number of variables we assign values to is not equal to the nummber of values in the tuple, Python will give us an error.
a,b,c = ret_multiple_vals()
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) <ipython-input-28-1b71d699a49b> in <module>() ----> 1 a,b,c = ret_multiple_vals() ValueError: too many values to unpack (expected 3)
print('*',end = '')
We used this to avoid the defualt new line at the end of every print statement.
Now, we can clearly understand that print is a function, it takes as arguments a combination of an arbitrary number of arguments and keyword arguments.
The keyword argument end allows us to tell the print function what to print after the given text on the screen.
input("Random String")
This allows us to print something onto the screen while expecting input, and returns as a string whatever the input was.
Time for a few exercises:
def tens_hundreds(num):
return num%10,num%100
def avg_nums(*args):
sum_nums = 0
count = 0
for num in args:
sum_nums+=num
count+=1
return sum_nums/count
Traditional programming languages like C are Procedural, in that they focus on modules of computatition, and they contain a series of steps to be carried out. It is how we normally think of programming. It relies on procedures.
Object oriented programming is based on the concepts of objects, which contain data and procedures to act on the data.
Consider an object as an entity, like yourself. Now, you have certain attributes like your name, date of birth, phone number and so on. These are stored as data for an object. You are an object here.
Now, consider a program to deal with students, from a college's standpoint. You are just one of the many students. For the college, each student will have certain values for a common set of attributes they are interested in. Now, we model each student using a class. An object is an instance of a class. It means that the class serves as a blueprint for object generation.
In the above example, the college may want to provide some functionality custom defined for the use case. They can define them within the class,
Now, we move on to classes and objects, the things I have been telling you about for so long.
What if we want to define our own datatype?
For example, if we wanted to support a student record, which included their name, roll number and current grade, we would want to have one variable we could access for one student's record, instead of 3.
But now, what if you wanted to define some custom functions specific to that task? Like calculating the current grade given the last grade and the grade for this semester?
Just using a function here could lead to problems. Ideally, you would want to keep all of the code relevant to this student entity together. Here, we use classes and objects.
This also allows us to use operators on custom defined class objects. (I'll get to that in a bit.)
class StudentRecord:
def __init__(self, name, roll_no):
print("Initialising",name)
self.grade = 0
self.name = name
self.roll_no = roll_no
#self.year = #3rd and 4th digits of roll_no
def print_self(self):
print(self)
print(type(self))
Note the syntax requirements:
Now, every object can have it's own values of variables. But, the function belongs to a class.
For each student, they need to have their own copy of each variable, since they will have different values(obviously). Hence, we need to find a way to refer to these values within the class functions.
Python always adds a default argument to every class method call. If the method is called via an object(object_name.method_name()), the object is passed to method as the first argument. In most programs we call this variable self. We could call it anything else, it is just a variable, that will have object assigned to it.
So, to define variables that every object can have unique values for, we use self.var_name. All this is done when an object is vreated, hence these statements are written inside __init__(). Other arguments mentioned in __init__() are the arguments needed to be passed to the class for object creation.
Initialising an object is done via typing the class name followed by round brackets, containing the arguments to __init__.
student_1 = StudentRecord("Aditya",111603029)
Initialising Aditya
student_2 = StudentRecord("Elon", 111600000)
Initialising Elon
print(student_1.name)
Aditya
print(student_1.roll_no)
111603029
print(student_2.name)
Elon
print(student_2.roll_no)
111600000
Hence, for every method inside a class, we have to consider an additional argument during function definition, which will contain the object that has been referred to.
print_self()
--------------------------------------------------------------------------- NameError Traceback (most recent call last) <ipython-input-38-8e6bd501d1e5> in <module>() ----> 1 print_self() NameError: name 'print_self' is not defined
StudentRecord.print_self()
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-39-634a981a5ffc> in <module>() ----> 1 StudentRecord.print_self() TypeError: print_self() missing 1 required positional argument: 'self'
StudentRecord.print_self(student_1)
<__main__.StudentRecord object at 0x00000228114C1198> <class '__main__.StudentRecord'>
StudentRecord("ABC", 111111111).print_self()
Initialising ABC <__main__.StudentRecord object at 0x00000228114C1EB8> <class '__main__.StudentRecord'>
student_1.print_self()
<__main__.StudentRecord object at 0x00000228114C1198> <class '__main__.StudentRecord'>
Thus as we can see above, the method inside a class cannot be called without an object calling it, or unless an object is passed to it as self and the classname is referred to.
print(type(student_1))
<class '__main__.StudentRecord'>
a = 5
print(type(a))
<class 'int'>
Now, notice one thing. The variable a is of type class int, just like student_1 is of type class StudentRecord(ignore the __main__, that's just to tell us that we have defined it in this program.
Thus, we can now see how our integer is also a class named int, and it's objects are the values that can be assigned to it.
The variable a is an object of classs int.
my_list = [5,6,7,10,'A',55.5]
print(type(my_list))
<class 'list'>
Now, try this for each datatype. You will notice that everything is of type class followed by a class_name. Hence, we commonly say that almost everything in Python is an object!! Because even your basic variables are objects, all datatypes are nothing but classes!!
class Abc:
def __init__(self):
print("Initialising.")
def __add__(self, other):
print("I'm adding now.")
print(self)
print(other)
a1 = Abc()
Initialising.
a2 = Abc()
Initialising.
a1+a2
I'm adding now. <__main__.Abc object at 0x00000228114CC198> <__main__.Abc object at 0x00000228114CC160>
What just happened?
We used these things called dunder methods to support commonly used operators on custom classes.
Since even datatypes in Python are nothing but classes, these methods exist, to allow us to implement what is commonly known as 'operator overloading' without too much hassle!
Double Underscore, or dunder for short, also called magic methods, support inbuilt functionality common to all objects. There are a lot of these,, and the ease with which we can use them is what makes Python so good.
class Abc:
def __init__(self):
print("Initialising.")
def __add__(self, other):
print("I'm adding now.")
print(self)
print(other)
def __str__(self):
return "I'm' Abc"
a3 = Abc()
Initialising.
print(a3)
I'm' Abc
Thus, __str__ allows us to dictate what gets printed when we call print function with the object of that class as an argument.
l1 = [a1, student_1, 1, 2, 'A']
print(type(l1))
<class 'list'>
l1[0]
<__main__.Abc at 0x228114cc198>
s1 = {a1, student_1, 1, 2}
s1
{1, 2, <__main__.Abc at 0x228114cc198>, <__main__.StudentRecord at 0x228114c1198>}
d1 = {a1:student_1, 2:0}
d1[a1]
<__main__.StudentRecord at 0x228114c1198>
t1 = (a1, s1, 1, 2)
t1[0]
<__main__.Abc at 0x228114cc198>
To check whether a value is a member of a list, string, tuple or set, we use the membership operator.
The membership operator is the keyword in. We can also use not in.
It evaluates to a boolean value, hence it can be used in conditions. We do not need to write a for loop to check if an element is a member of a collectible datatype. We simply use the keyword in.
l2 = [1,2,'A','B',a1]
1 in l2
True
1 not in l2
False
a1 in l2
True
To check if 2 variables are the same (refer to the same object), we use the identity operator.
The identity operator is the keyword is. We can also use not is.
It evaluates to a boolean value.
student_1 is student_2
False
1 is 2
False
'2' is "2"
True
Converting one type of a variable to annother is known as type casting.
For type casting, we mention the datatype name(str/ int/ float/ bool etc) and provide an argument to it, that is of another type.
a = input("Enter a number")
Enter a number5
print(type(a))
<class 'str'>
b = int(a) #This converts the string to an integer.
c = float(a) #This converts the string to a float.
print(type(b))
<class 'int'>
print(type(c))
<class 'float'>
d = str(b) #This converts the integer to a string
print(type(d))
<class 'str'>
This takes 2 forms
for i in range(5):
print(i)
0 1 2 3 4
for i in range(10,12):
print(i)
10 11
for i in range(5,14,3):
print(i)
5 8 11
The purpose of zip() is to map the similar index of multiple containers so that they can be used just using as single entity.
This function takes an arbitrary number of multiple containers of the same size, and returns tuples corresponding to same indices.
for tup in zip([2,3,4],(5,6,7),{'A','B',10}):
i, j, k = tup
print(i,j,k)
2 5 A 3 6 10 4 7 B
It returns an enumerate object, which contains indices and values of all items as a tuple. It is used in the for loop, to get indices and elements one by one
for i,j in enumerate(l1):
print(i, j)
print("Next")
0 <__main__.Abc object at 0x00000228114CC198> Next 1 <__main__.StudentRecord object at 0x00000228114C1198> Next 2 1 Next 3 2 Next 4 A Next
Thus, in for loops, we commonly use enumerate to get indices and elements one by one.
We can also start the numbers generated(indices) from a number other than 0. For example,
for i,j in enumerate(l1, 10):
print(i,j)
10 <__main__.Abc object at 0x00000228114CC198> 11 <__main__.StudentRecord object at 0x00000228114C1198> 12 1 13 2 14 A
This returns the length of a collectible datatype.
l1 = [2,3,4,5,6,7,8]
print(len(l1))
7
s1 = {1,2,3,4}
print(len(s1))
4
This allows you to use variable names in a string, which will be replaced by the value of the variable when executed.
"{} is a list".format(l1)
'[2, 3, 4, 5, 6, 7, 8] is a list'
The curly braces are a placeholder, we have to pass a corresponding argument to the format function.
For multiple arguments, we can use positional arguments(the first curly braces will correspond to the first argument and so on), or we can use keyword arguments.
v1 = 'A'
v2 = 'B'
"{a},{b} is a nice idea.".format(a=v1,b=v2)
'A,B is a nice idea.'
f"{v1},{v2} is a nice idea."
'A,B is a nice idea.'
Here, we preceed the string with an 'f', and the values in the curly braces are the variable names.
These are used to convert the string to lower/upper case.
s1 = "I aM a HuMaN"
s1.lower()
'i am a human'
"I aM a HuMaN".upper()
'I AM A HUMAN'
We use this to split a string based on a specific character (the default is whitespace)
"I am a human".split()
['I', 'am', 'a', 'human']
s1.split()
['I', 'aM', 'a', 'HuMaN']
To use a custom delimiter, we have to pass an argument to the split function.
s2 = "Hey;There;I;am;Here"
s2.split()
['Hey;There;I;am;Here']
s2.split(';')
['Hey', 'There', 'I', 'am', 'Here']
The opposite of split is join. split returns a list. join takes as argument a list, and joins them with the specified string delimiter.
'.'.join(['I','am','here'])
'I.am.here'
''.join(['I','Am','Here'])
'IAmHere'
More string functions can be found here.
They return the largest, smallest and sum of values in the list.
l1 = [2,3,4,5,6]
sum(l1)
20
max(l1)
6
min(l1)
2
l2 = ['a','b',2,3,4]
sum(l2)
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-118-720982d0d0db> in <module>() ----> 1 sum(l2) TypeError: unsupported operand type(s) for +: 'int' and 'str'
min(l2)
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-119-bc2d67f9fba2> in <module>() ----> 1 min(l2) TypeError: '<' not supported between instances of 'int' and 'str'
max(l2)
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-120-16f523e5eb78> in <module>() ----> 1 max(l2) TypeError: '>' not supported between instances of 'int' and 'str'
Thus, we need to have the same datatype in the list, for these functions to work.
l3 = ['a','b','z','d']
max(l3)
'z'
min(l3)
'a'
sum(l3)
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-124-8da4c4bc559b> in <module>() ----> 1 sum(l3) TypeError: unsupported operand type(s) for +: 'int' and 'str'
sum() needs all integers.
This will return a sorted list(does not modify the original list).
sorted(l1)
[2, 3, 4, 5, 6]
sorted(l2)
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-126-a078cf776ef0> in <module>() ----> 1 sorted(l2) TypeError: '<' not supported between instances of 'int' and 'str'
Again, it needs to be all integers, or all strings.
l3 = ['a','b','z','d']
sorted(l3)
['a', 'b', 'd', 'z']
More List methods can be found here.
dict_1 = {1:0,2:1,3:'a'}
dict_1.keys()
dict_keys([1, 2, 3])
This returns all the keys in that dictionary.
This removes the key-value pair and returns the value with the given key.
dict_1.pop(1)
0
More Dictionary methods can be found here.