2. MUTITHREADING
What is Multitasking in Python?
Multitasking, in general, is the capability of performing
multiple tasks simultaneously. In technical terms,
multitasking refers to the ability of an operating system
to perform different tasks at the same time. For
instance, you are downloading something on your PC as
well as listening to songs and concurrently playing a
game, etc. All these tasks are performed by the same
OS an in sync. This is nothing but multitasking which not
just helps you save time but also increases productivity.
There are two types of multitasking in an OS:
Process-based
Thread-based
3. THREAD
What is a thread?
A thread is basically an independent flow of execution. A single process can consist of multiple
threads. Each thread in a program performs a particular task. For Example, when you are
playing a game say FIFA on your PC, the game as a whole is a single process, but it consists of
several threads responsible for playing the music, taking input from the user, running the
opponent synchronously, etc. All these are separate threads responsible for carrying out these
different tasks in the same program.
Every process has one thread that is always running. This is the main thread. This main thread
actually creates the child thread objects. The child thread is also initiated by the main thread. I
will show you all further in this article how to check the current running thread.
When to use Multithreading in Python?
Multithreading is very useful for saving time and improving performance, but it cannot be
applied everywhere.
In the previous FIFA example, the music thread is independent of the thread that takes your
input and the thread that takes your input is independent of the thread that runs your opponent.
These threads run independently because they are not inter-dependent.
Therefore, multithreading can be used only when the dependency between individual threads
does not exist.
How to achieve Multithreading in Python?
Multithreading in Python can be achieved by importing the threading module.
4. THREAD
How to create threads in Python?
Threads in Python can be created in three ways:
Without creating a class
By extending Thread class
Without extending Thread class
Without creating a class
Multithreading in Python can be accomplished
without creating a class as well.
import threading
from threading import *
5. EXAMPLE
from threading import *
print(current_thread().getName())
def mt():
print("Child Thread")
child=Thread(target=mt)
child.start()
print("Executing thread name :",current_thread().getName())
Output:
MainThread
Child Thread
Executing thread name : MainThread
The above output shows that the first thread that is present is,
the main thread. This main thread then creates a child thread
that is executing the function and then the final print statement
is executed again by the main thread.
6. EXAMPLE2
By extending the Thread class:
When a child class is created by extending the Thread class, the child class represents that a new thread is executing some
task. When extending the Thread class, the child class can override only two methods i.e. the __init__() method and the run()
method. No other method can be overridden other than these two methods.
Here is an example of how to extend the Thread class to create a thread:
import threading
import time
class mythread(threading.Thread):
def run(self):
for x in range(7):
print("Hi from child")
a = mythread()
a.start()
a.join()
print("Bye from",current_thread().getName())
Output:
Hi from child
Hi from child
Hi from child
Hi from child
Hi from child
Hi from child
Hi from child
Bye from MainThread
The above example shows that class myclass is inheriting the Thread class and the child class i.e myclass is overriding the
run method. By default, the first parameter of any class function needs to be self which is the pointer to the current object.
The output shows that the child thread executes the run() method and the main thread waits for the childs execution to
complete. This is because of the join() function, which makes the main thread wait for the child to finish.
7. EXAMPLE3
Without Extending Thread class
To create a thread without extending the Thread class, you can do as follows:
Example:
from threading import *
class ex:
def myfunc(self): #self necessary as first parameter in a class func
for x in range(7):
print("Child")
myobj=ex()
thread1=Thread(target=myobj.myfunc)
thread1.start()
thread1.join()
print("done")
Output:
Child
Child
Child
Child
Child
Child
Child
done
The child thread executes myfunc after which the main thread executes the last print statement.
8. ADVANTAGES OF USING THREADING
Multithreading has many advantages some of
which are as follows:
Better utilization of resources
Simplifies the code
Allows concurrent and parallel occurrence of
various tasks
Reduces the time consumption or response time,
thereby, increasing the performance.
9. THREAD
Starting a New Thread
To spawn another thread, you need to call following
method available in thread module −
thread.start_new_thread ( function, args[, kwargs] )
This method call enables a fast and efficient way to
create new threads in both Linux and Windows.
The method call returns immediately and the child
thread starts and calls function with the passed list
of args. When function returns, the thread terminates.
Here, args is a tuple of arguments; use an empty tuple
to call function without passing any
arguments. kwargs is an optional dictionary of keyword
arguments.
10. THREAD
The Threading Module
The newer threading module included with Python 2.4 provides much more
powerful, high-level support for threads than the thread module discussed in the
previous section.
The threading module exposes all the methods of the thread module and provides
some additional methods −
threading.activeCount() − Returns the number of thread objects that are active.
threading.currentThread() − Returns the number of thread objects in the caller's
thread control.
threading.enumerate() − Returns a list of all thread objects that are currently
active.
In addition to the methods, the threading module has the Thread class that
implements threading. The methods provided by the Thread class are as follows −
run() − The run() method is the entry point for a thread.
start() − The start() method starts a thread by calling the run method.
join([time]) − The join() waits for threads to terminate.
isAlive() − The isAlive() method checks whether a thread is still executing.
getName() − The getName() method returns the name of a thread.
setName() − The setName() method sets the name of a thread.
11. GENERATORS IN PYTHON
Prerequisites: Yield Keyword and Iterators
An iterator is an object that contains a countable number of values.
An iterator is an object that can be iterated upon, meaning that you can traverse through all the values.
Technically, in Python, an iterator is an object which implements the iterator protocol, which consist of the
methods __iter__() and __next__().
Iterator vs Iterable
Lists, tuples, dictionaries, and sets are all iterable objects. They are iterable containers which you can get an
iterator from.
All these objects have a iter() method which is used to get an iterator:
Example
Return an iterator from a tuple, and print each value:
mytuple = ("apple", "banana", "cherry")
myit = iter(mytuple)
print(next(myit))
print(next(myit))
print(next(myit))
There are two terms involved when we discuss generators.
Generator-Function : A generator-function is defined like a normal function, but whenever it needs to generate a
value, it does so with the yield keyword rather than return. If the body of a def contains yield, the function
automatically becomes a generator function.
Generator-Object : Generator functions return a generator object. Generator objects are used either by calling
the next method on the generator object or using the generator object in a “for in” loop (as shown in the above
program).
12. CREATE GENERATORS IN PYTHON
It is fairly simple to create a generator in Python. It
is as easy as defining a normal function, but with
a yield statement instead of a return statement.
If a function contains at least one yield statement (it
may contain other yield or return statements), it
becomes a generator function.
Both yield and return will return some value from a
function.
The difference is that while a return statement
terminates a function entirely, yield statement
pauses the function saving all its states and later
continues from there on successive calls.
13. DIFFERENCES BETWEEN GENERATOR FUNCTION
AND NORMAL FUNCTION
Here is how a generator function differs from a
normal function.
Generator function contains one or
more yield statements.
When called, it returns an object (iterator) but does not
start execution immediately.
Methods like __iter__() and __next__() are implemented
automatically. So we can iterate through the items
using next().
Once the function yields, the function is paused and the
control is transferred to the caller.
Local variables and their states are remembered
between successive calls.
Finally, when the function terminates, StopIteration is
raised automatically on further calls.
14. PYTHON GENERATOR EXPRESSION
Simple generators can be easily created on the fly using
generator expressions. It makes building generators easy.
Similar to the lambda functions which create anonymous
functions, generator expressions create anonymous generator
functions.
The syntax for generator expression is similar to that of a list
comprehension in Python. But the square brackets are
replaced with round parentheses.
The major difference between a list comprehension and a
generator expression is that a list comprehension produces
the entire list while the generator expression produces one
item at a time.
They have lazy execution ( producing items only when asked
for ). For this reason, a generator expression is much more
memory efficient than an equivalent list comprehension.
15. USE OF PYTHON GENERATORS
There are several reasons that make generators a powerful implementation.
1. Easy to Implement
Generators can be implemented in a clear and concise way as compared to their iterator class
counterpart. Following is an example to implement a sequence of power of 2 using an iterator
class.
2. Memory Efficient
A normal function to return a sequence will create the entire sequence in memory before
returning the result. This is an overkill, if the number of items in the sequence is very large.
Generator implementation of such sequences is memory friendly and is preferred since it only
produces one item at a time.
3. Represent Infinite Stream
Generators are excellent mediums to represent an infinite stream of data. Infinite streams
cannot be stored in memory, and since generators produce only one item at a time, they can
represent an infinite stream of data.
4. Pipelining Generators
Multiple generators can be used to pipeline a series of operations. This is best illustrated using
an example.
Suppose we have a generator that produces the numbers in the Fibonacci series. And we have
another generator for squaring numbers.
If we want to find out the sum of squares of numbers in the Fibonacci series, we can do it in the
following way by pipelining the output of generator functions together.
16. DECORATORS IN PYTHON
In Python, functions are the first class objects, which
means that –
Functions are objects; they can be referenced to,
passed to a variable and returned from other functions
as well.
Functions can be defined inside another function and
can also be passed as argument to another function.
Decorators are very powerful and useful tool in Python
since it allows programmers to modify the behavior of
function or class. Decorators allow us to wrap another
function in order to extend the behavior of wrapped
function, without permanently modifying it.
In Decorators, functions are taken as the argument into
another function and then called inside the wrapper
function.