PP_Module-4_Notes
PP_Module-4_Notes
INTRODUCTION
Object-oriented programming is a programming paradigm that provides a means of
structuring programs so that properties and behaviors are bundled into individual objects.
For instance, an object could represent a person with properties like a name, age, and
address and behaviors such as walking, talking, breathing, and running. Or it could
represent an email with properties like a recipient list, subject, and body and behaviors
like adding attachments and sending.
Put another way, object-oriented programming is an approach for modeling concrete,
real-world things, like cars, as well as relations between things, like companies and
employees, students and teachers, and so on.
OOP models real-world entities as software objects that have some data associated with
them and can perform certain functions.
As Python is a multi-paradigm programming language, it will supports different
programming approaches.
So it uses Object-Oriented Programming (OOP) approach to solve a programming
problem by creating objects.
Class:
A class is a blueprint for the object.
The class can be defined as a collection of objects.
It is a logical entity that has some specific attributes and methods.
For example: if you have an employee class, then it should contain an attribute and
method, i.e. an email id, name, age, salary, etc.
Class is defined under a “Class” Keyword.
Example (Empty class):
class student:
pass
Object:
An object (instance) is an instantiation of a class.
When class is defined, only the description for the object is defined. Therefore, no
memory or storage is allocated.
The example for object of student class can be:
s1 = student() Here, s1 is an object of class student.
Similarly we can create multiple objects for single class.
Method:
The method is a function that is associated with an object. In Python, a method is not
unique to class instances. Any object type can have methods.
Inheritance:
Inheritance is the most important aspect of object-oriented programming, which
simulates the real-world concept of inheritance. It specifies that the child object acquires
all the properties and behaviors of the parent object.
By using inheritance, we can create a class which uses all the properties and behavior of
another class. The new class is known as a derived class or child class, and the one whose
properties are acquired is known as a base class or parent class.It provides the re-usability
of the code.
Polymorphism:
Polymorphism contains two words "poly" and "morphs". Poly means many, and morph
means shape(forms).
It is an ability (in OOP) to use a common interface for multiple forms (data types).
By polymorphism, we understand that one task can be performed in different ways.
Suppose, we need to color a shape, there are multiple shape options (rectangle, square,
circle). However we could use the same method to color any shape. This concept is called
Polymorphism.
Another example - you have a class animal, and all animals speak. But they speak
differently. Here, the "speak" behavior is polymorphic in a sense and depends on the
animal.
Encapsulation:
Encapsulation is also an essential aspect of object-oriented programming.
It is used to restrict access to methods and variables.
This prevents data from direct modification which is called encapsulation.
In encapsulation, code and data are wrapped together within a single unit from being
modified by accident.
Abstraction:
Abstraction is used to hide the internal functionality of the function from the users.
The users only interact with the basic implementation of the function, but inner working
is hidden. User is familiar with that "what function does" but they don't know "how it
does."
In simple words, we all use the smart phone and very much familiar with its functions
such as camera, voice-recorder, call-dialing, etc., but we don't know how these operations
are happening in the background.
Let's take another example - When we use the TV remote to increase the volume. We
don't know how pressing a key increases the volume of the TV. We only know to press
the "+" button to increase the volume.
That is exactly the abstraction that works in the object-oriented concept.
CLASS
A Python class is a group of attributes and methods.
What is Attribute?
Attributes are represented by variable that contains data.
What is Method?
Method performs an action or task. It is similar to function.
Rules:
The class name can be any valid identifier.
It can't be Python reserved word.
A valid class name starts with a letter, followed by any number of letter, numbers or
underscores.
A class name generally starts with Capital Letter.
Example:
1. class Mobile:
def __init__(self):
self.model = ‘RealMe X’
OBJECT (INSTANCE)
Object is class type variable or class instance. To use a class, we should create an object
to the class.
Instance creation represents allotting memory necessary to store the actual data of the
variables.
Each time you create an object of a class a copy of each variables defined in the class is
created.
In other words you can say that each object of a class has its own copy of data members
defined in the class.
Syntax: -
object_name = class_name()
object_name = class_name(arg)
Example:
1. class Mobile:
def __init__(self):
self.model = ‘RealMe X’
def show_model (self):
print(‘Model:’, self.model)
realme = Mobile()
2. class Mobile:
def __init__(self, model):
self.model = model
def show_model (self):
print(‘Model:’, self.model)
realme = Mobile()
A block of memory is allocated on heap. The size of allocated memory is to be decided
from the attributes and methods available in the class (Mobile).
After allocating memory block, the special method __init__() is called internally. This
method stores the initial data into the variables.
The allocated memory location address of the instance is returned into object (realme).
The memory location is passed to self.
We can access variable and method of a class using class object or instance of class.
object_name.variable_name
realme.model
object_name.method_name ( )
realme.show_model ( );
object_name.method_name (parameter_list)
realme.show_model(1000);
self Variable:
self is a default variable that contains the memory address of the current object.
This variable is used to refer all the instance variable and method.
When we create object of a class, the object name contains the memory location of the
object.
This memory location is internally passed to self, as self knows the memory address of
the object so we can access variable and method of object.
self is the first argument to any object method because the first argument is always the
object reference. This is automatic, whether you call it self or not.
def __init__(self):
def show_model(self):
Each time you create an object of a class a copy of each variables defined in the class
is created.
class Mobile:
def __init__(self,model):
self.model = Model
def show_model (self):
print(‘Model:’, self.model)
realme = Mobile(“Realme X”)
redmi = Mobile(“Redmi 9”)
poco = Mobile(“Poco M2”)
Constructor:
Python supports a special type of method called constructor for initializing the instance
variable of a class.
A class constructor, if defined is called whenever a program creates an object of that
class.
A constructor is called only once at the time of creating an instance.
If two instances are created for a class, the constructor will be called once for each
instance.
class Mobile:
def __init__(self):
self.model =‘RealMe X’
realme = Mobile( )
1. class Mobile:
def __init__(self, m):
self.model = m
realme = Mobile('Realme X')
2. class Mobile:
def __init__(self, m, v):
self.model = m
self.volumn = v
redmi = Mobile('Redmi 7s', 50)
del object_name.attribute_name
class Mobile:
def __init__(self,model,price):
self.model = model
self.price = price
realme = Mobile("Realme X","25000")
print(realme.model,realme.price)
#for the last line it results an attribute error as price is no longer an attribute
Deleting Objects:
We can delete the object itself using the del statement.
class employee:
def __init__(self,name,salary):
self.name=name
self.salary=salary
e1=employee("XYZ","30000")
#It prints XYZ
print(e1.name)
#Deleting e1 object
del e1
#It results in name error,e1 is not defined
print(e1.name)
When we create an e1 object , a new instance object is created in memory and the name
e1 binds with it.
When we delete the object using the del e1 statement, this binding is removed and the
name e1 is deleted from the corresponding namespace.
The object, however, continues to exist in memory and if no other name is bound to it , it
is later automatically destroyed.
This automatic destruction of unreferenced objects in Python is also called garbage
collection.
Destructors in Python:
Just like a constructor is used to create and initialize an object, a destructor is used to
destroy the object and perform the final clean up.
Although in python we do have garbage collector to clean up the memory, but its not
just memory which has to be freed when an object is dereference or destroyed, it can be a
lot of other resources as well, like closing open files, closing database connections,
cleaning up the buffer or cache etc.
Hence when we say the final clean up, it doesn't only mean cleaning up the memory
resources.
The __del__() method is used as the destructor method in Python. The user can call
the __del__() method when all the references of the object have been deleted, and it
becomes garbage collected.
class employee:
def __init__(self,name,salary):
self.name=name
self.salary=salary
def __del__(self):
print("Destructor called")
e1=employee("XYZ","30000")
print(e1.name)
print(e1.salary)
del e1
Output:
XYZ
30000
Destructor called
Note: As there is only one reference e1 for object, so upon deleting reference e1,
__del__( ) method is called . Because for calling destructor reference count should be
zero.
class employee:
def __init__(self,name,salary):
self.name=name
self.salary=salary
def __del__(self):
print("Destructor called")
e1=employee("XYZ","30000")
e2=e1
e3=e1
print(e1.name)
print(id(e1),id(e2),id(e3))
del e1
Output:
XYZ
57824984 57824984 57824984
Note: As there are multiple references such as e1,e2,e3 for object, so upon deleting reference e1,
__del__( ) method will not be called . Because for calling destructor reference count should be
zero but here still the reference count is 2.
class employee:
def __init__(self,name,salary):
self.name=name
self.salary=salary
def __del__(self):
print("Destructor called")
e1=employee("XYZ","30000")
e2=e1
e3=e1
print(e1.name)
del e1
print(e2.salary)
del e2
print(e3.name)
del e3
Output:
XYZ
30000
XYZ
Destructor called
CLASS ATTRIBUTES
As an object-oriented language, Python provides two scopes for attributes: class attributes
and instance attributes.
Class attributes are tied only to the classes in which they are defined, and since instance
objects are the most commonly used objects in everyday OOP, instance data attributes are
the primary data attributes you will be using.
Class data attributes are useful only when a more "static" data type is required which is
independent of any instances.
So a class attribute is a Python variable that belongs to a class rather than a particular
object.
It is shared between all the objects of this class and it is defined outside the constructor
function, __init__(self,...), of the class.
Example:
class Employee:
Company="TCS" # Class Attribute
print(Employee.Company)
#Output is: TCS
>>>Employee.__dict__
mappingproxy({'__module__': '__main__', '__doc__': 'Employee is a class with Company
as class attribute', 'Company': 'TCS', '__dict__': <attribute '__dict__' of 'Employee'
objects>, '__weakref__': <attribute '__weakref__' of 'Employee' objects>})
As dir() returns a list of (just the) names of an object's attributes while __dict__ is a
dictionary whose attribute names are the keys and whose values are the data values of the
corresponding attribute objects.
The output also reveals __doc__ and __module__, which are special class attributes that
all classes have (in addition to __dict__).
>>>Employee.__name__
'Employee'
>>>Employee.__doc__
'Emplyee is a class with Company as class attribute'
>>>Employee.__bases__
(<class 'object'>,)
>>>Employee.__dict__
mappingproxy({'__module__': '__main__', '__doc__': 'Emplyee is a class with Company as class
attribute', 'Company': 'TCS', '__dict__': <attribute '__dict__' of 'Employee' objects>,
'__weakref__': <attribute '__weakref__' of 'Employee' objects>})
>>>Employee.__class__
<class 'type'>
>>>Employee.__module__
'__main__'
__name__ is the string name for a given class. This may come in handy in cases where a
string is desired rather than a class object. Even some built-in types have this attribute,
and we will use one of them to showcase the usefulness of the __name__ string.
__doc__ is the documentation string for the class, similar to the documentation string for
functions and modules, and must be the first unassigned string succeeding the header
line.
__bases__ deals with inheritance, which we will cover later in this chapter; it contains a
tuple that consists of a class's parent classes.
The aforementioned __dict__ attribute consists of a dictionary containing the data
attributes of a class.
Python supports class inheritance across modules. To better clarify a class's description,
the __module__ was introduced so that a class name is fully qualified with its module.
When we access the __class__ attribute of any class, you will find that it is indeed an
instance of a type object. In other words, a class is a type now
INSTANCE ATTRIBUTES
An instance attribute is a Python variable belonging to only one object.
It is only accessible in the scope of the object and it is defined inside the constructor
function of a class. For example, __init__(self,..).
So Instance Attributes are unique to each object, (an instance is another name for an
object).
Here, any Employee object we create will be able to store its name and age. We can
change attribute value of an object of employee, without affecting any other employee
objects we’ve created.
The data values can be associated with a particular instance of any class and are
accessible via the familiar dotted-attribute notation.
These values are independent of any other instance or of the class it was instantiated
from.
When an instance is deallocated, so are its attributes.
Output:
IOT
IT
CSE
CSE
CSE
If a constructor is defined, it should not return any object because the instance object is
automatically returned after the instantiation call.
Correspondingly, __init__() should not return any object (or return None); otherwise,
there is a conflict of interest because only the instance should be returned.
Attempting to return any object other than None will result in a TypeError exception:
>>> class MyClass:
... def __init__(self):
... print 'initialized'
... return 1 ...
>>> mc = MyClass()
initialized
Similar to classes, instances also have a __dict__ special attribute (also accessible by
calling vars() and passing it an instance), which is a dictionary representing its attributes:
>>> c.__dict__ {'foo': 'roger', 'bar': 'shrubber'}
As you can see, c currently has no data attributes, but we can add some and recheck the
__dict__ attribute to make sure they have been added properly:
>>> c.foo = 1
>>> c.bar = 'SPAM'
>>> '%d can of %s please' % (c.foo, c.bar)
'1 can of SPAM please'
>>> c.__dict__
{'foo': 1, 'bar': 'SPAM'}
Instance Attributes versus Class Attributes:
Output:
My name is A from object 1
My name is B from object 2
My name is C from object 2
In the previous example two instances namely samp1 and samp2 are created. Note that
when the function alterIt() is applied to the second instance, only that particular
instance’s value is changed.
The line samp1.myFunc2() will be expanded as sample.myFunc2(samp1).
For this method no explicit argument is required to be passed. The instance samp1 will be
passed as argument to the myFunc2(). The line samp1.myFunc2() will generate the error
:
Traceback (most recent call last):
File "C:/Python38/OOPS/bound_unbound.py", line 19, in <module>
samp1.myFunc2() #----------> error line
TypeError: myFunc2() takes 0 positional arguments but 1 was given
It means that this method is unbound. It does not accept any instance as an argument.
These functions are unbound functions.
Static Method:
A method can be made static in two ways:
Using staticmethod()
Using decorator
Using staticmethod(): The staticmethod() function takes the function to be converted as its
argument and returns the static version of that function. A static function knows nothing about
the class, it just works with the parameters passed to it.
class Foo():
def bar(x):
return x + 5
Foo.bar = staticmethod(Foo.bar)
f=Foo()
print(f.bar(4))
Output: 9
Using decorators:
These are features of Python used for modifying one part of the program using another
part of the program at the time of compilation.
The decorator that can be used to make a method static is
@staticmethod
This informs the built-in default metaclass not to create any bound methods for this
method.
Once this line is added before a function, the function can be called using the class name.
Now if I declare @staticmethod the self argument isn't passed implicitly as the first
argument
class Foo():
@staticmethod
def bar(x):
return x + 5
f=Foo()
print(f.bar(4))
Output:
9
COMPOSITION
It is one of the fundamental concepts of Object-Oriented Programming.
In this concept, we will describe a class that references to one or more objects of other
classes as an Instance variable.
Here, by using the class name or by creating the object we can access the members of one
class inside another class.
It enables creating complex types by combining objects of different classes.
It means that a class Composite can contain an object of another class Component.
This type of relationship is known as Has-A Relation.
In the below figure Classes are represented as boxes with the class
name Composite and Component representing Has-A relation between both of them.
Example: Composite Class as Employee and Component Class as Salary
Output:
490000
INHERITANCE
Inheritance is the capability of one class to derive or inherit the properties from another class.
The benefits of inheritance are:
It represents real-world relationships well.
It provides reusability of a code. We don’t have to write the same code again and again.
Also, it allows us to add more features to a class without modifying it.
It is transitive in nature, which means that if class B inherits from another class A, then
all the subclasses of B would automatically inherit from class A.
So Inheritance models is called an is a relationship. This means that when you have
a Derived class that inherits from a Base class, you created a relationship
where Derived is a specialized version of Base.
Inheritance is represented using the Unified Modeling Language or UML in the following
way:
Classes are represented as boxes with the class name on top. The inheritance relationship
is represented by an arrow from the derived class pointing to the base class.
The word extends is usually added to the arrow.
In an inheritance relationship:
Classes that inherit from another are called derived classes, subclasses, or subtypes.
Classes from which other classes are derived are called base classes or super classes.
A derived class is said to derive, inherit, or extend a base class.
Let’s say you have a base class Animal and you derive from it to create a Horse class.
The inheritance relationship states that a Horse is an Animal.
This means that Horse inherits the interface and implementation of Animal,
and Horse objects can be used to replace Animal objects in the application.
Sample Example:
class Parent():
def first(self):
print('first function')
class Child(Parent):
def second(self):
print('second function')
ob = Child()
ob.first()
ob.second()
Output:
first function
second function
In the above program, you can access the parent class function using the child class
object.
__init__( ) Function:
The __init__() function is called every time a class is being used to make an object.
When we add the __init__() function in a parent class, the child class will no longer be
able to inherit the parent class’s __init__() function.
The child’s class __init__() function overrides the parent class’s __init__() function.
Example to demonstrate init method in inheritance:
class Person:
def __init__(self,fname,age):
self.firstname=fname
self.age=age
def view(self):
print(self.firstname,self.age)
class Employee(Person):
def __init__(self,fname,age):
Person.__init__(self,fname,age)
self.lastname ="Harryson"
def view(self):
print("First Name:" , self.firstname)
print("Last Name:" , self.lastname)
print("Age:" , self.age)
ob = Employee("John" , '28')
ob.view()
Output:
First Name: John
Last Name: Harryson
Age: 28
Types of Inheritance:
Inheritance is categorized based on the hierarchy followed and the number of parent classes and
subclasses involved.
There are five types of inheritances:
Single Inheritance
Multiple Inheritance
Multilevel Inheritance
Hierarchical Inheritance
Hybrid Inheritance
Single Inheritance:
This type of inheritance enables a subclass or derived class to inherit properties and
characteristics of only one parent class, this avoids duplication of code and improves
code reusability.
Single Inheritance Example:
class Parent:
def func1(self):
print("This is function 1")
class Child(Parent):
def func2(self):
print("This is function 2 ")
ob = Child()
ob.func1()
ob.func2()
Output:
This is function 1
This is function 2
Multiple Inheritance:
This inheritance enables a child class to inherit from more than one parent class.
This type of inheritance is not supported by java classes, but python does support this
kind of inheritance.
It has a massive advantage if we have a requirement of gathering multiple characteristics
from different classes.
Output:
this is function 1
this is function 2
this is function 3
Multilevel Inheritance
In multilevel inheritance, the transfer of the properties of characteristics is done to more
than one class hierarchically.
To get a better visualization we can consider it as an ancestor to grandchildren relation or
a root to leaf in a tree with more than one level.
Output:
this is function 1
this is function 2
this is function 3
Hierarchical Inheritance
This inheritance allows a class to host as a parent class for more than one child class or
subclass.
This provides a benefit of sharing the functioning of methods with multiple child classes,
hence avoiding code duplication.
Output:
this is function 1
this is function 2
this is function 1
this is function 3
Hybrid Inheritance
An inheritance is said hybrid inheritance if more than one type of inheritance is
implemented in the same code.
This feature enables the user to utilize the feature of inheritance at its best.
This satisfies the requirement of implementing a code that needs multiple inheritances in
implementation.
class Child1(Parent):
def func2(self):
print("this is function 2")
class Child2(Parent):
def func3(self):
print(" this is function 3")
class Child3(Child1,Child2):
def func4(self):
print(" this is function 4")
ob = Child3()
ob.func1()
Output:
this is function one
isinstance():
The isinstance() Boolean function is useful for determining if an object is an instance of a
given class.
It has the following syntax:
isinstance(obj1, obj2)
isinstance() returns True
if obj1 is an instance of class obj2 or is an instance of a subclass of obj2 (and False
otherwise), as indicated in the following examples:
>>> class C1(object):
pass
>>> class C2(object):
pass
>>> c1 = C1()
>>> c2 = C2()
>>>isinstance(c1, C1)
True
>>>isinstance(c2, C1)
False
>>>isinstance(c1, C2)
False
super():
The super() function in Python makes class inheritance more manageable and
extensible. The function returns a temporary object that allows reference to a parent class
by the keyword super.
The super() function has two major use cases:
To avoid the usage of the super (parent) class explicitly.
To enable multiple inheritances.
class Employee(Person):
def __init__(self):
print('John is working in a good company')
super().__init__('John')
e1 = Employee()
Output:
John is working in a good company
John is a nice person
vars():
This is an inbuilt function in Python.
The vars() method takes only one parameter and that too is optional.
It takes an object as a parameter which may be can a module, a class, an instance, or
any object having __dict__ attribute.
Syntax:
vars(object)
The method returns the __dict__ attribute for a module, class, instance, or any other
object if the same has a __dict__ attribute.
If the object fails to match the attribute, it raises a TypeError exception.
Objects such as modules and instances have an updateable __dict__ attribute however,
other objects may have written restrictions on their __dict__ attributes.
vars() acts like locals() method when an empty argument is passed which implies that
the locals dictionary is only useful for reads since updates to the locals dictionary are
ignored.
class C(object):
pass
>>> c = C()
>>> c.foo = 100
>>> c.bar = 'Python'
>>> c.__dict__
{'foo': 100, 'bar': 'Python'}
>>> vars(c)
{'foo': 100, 'bar': 'Python'}
TYPES VS. CLASSES/INSTANCES
A user-defined class (or the class "object") is an instance of the class "type".
So, we can see, that classes are created from type. In Python3 there is no difference
between "classes" and "types". They are in most cases used as synonyms.
The fact that classes are instances of a class "type" allows us to program meta classes.
>>> c=“abc"
>>> type(c)
<class 'str'>
>>> type(str)
<class 'type'>
>>> class d:
pass
>>> type(d)
<class 'type'>
We can create classes, which inherit from the class "type". So, a metaclass is a subclass
of the class "type".
Instead of only one argument, type can be called with three parameters:
type(classname, superclasses, attributes_dict)
If type is called with three arguments, it will return a new type object. This provides us
with a dynamic form of the class statement.
"classname" is a string defining the class name and becomes the name attribute;
"superclasses" is a list or tuple with the superclasses of our class. This list or tuple will
become the bases attribute;
the attributes_dict is a dictionary, functioning as the namespace of our class. It contains
the definitions for the class body and it becomes the dict attribute.
class A:
pass
x = A()
print(type(x))
<class '__main__.A'>
Output:
22000
PRIVACY
Attributes in Python are, by default, "public" all the time, accessible by both code within
the module and modules that import the module containing the class.
Many OO languages provide some level of privacy for the data and provide only accessor
functions to provide access to the values. This is known as implementation hiding and is
a key component to the encapsulation of the object.
Most OO languages provide "access specifiers" to restrict access to member functions.
Access specifiers are used to restrict or control the accessibility of class resources, by
declaring them as public, private and protected.
In python we follow some convention to declare members as public, private and
protected.
There are no such keywords like public, protected and private in python like other OOP
languages.
Public:
Public members are available publicly, means can be accessed from any where.
By default, every member of class is public in Python.
Demo_public program:
class Person:
def __init__(self,name,age):
self.name = name
self.age = age
def display(self):
print("Age is:",self.age)
p1 = Person("ABC",50)
p1.display()
print("Age is:",p1.name)
Output:
Age is: 50
Age is: ABC
p1 = Person("ABC",50)
p1.__display()
print("Age is:",p1.__name)
Output:
Traceback (most recent call last):
File "C:\Python38\OOPS\public.py", line 9, in <module>
p1.__display()
AttributeError: 'Person' object has no attribute '__display'
Demo_name_mangling Program:
class Person:
def __init__(self,name,age):
self.__name = name #private instance attribute
self.__age = age #private instance attribute
def __display(self):
print("Age is:",self.__age)
p1 = Person("ABC",50)
print("Age is:",p1._Person__age)
Output:
Age is: 50
p1 = Person("ABC",50)
print("Age is:",p1._age)
p1._display()
Output:
Age is: 50
Person name is: ABC
DECORATORS
Python has an interesting feature called decorators to add functionality to an existing
code.
A python decorator is a function that takes in a function, add some functionality to it and
returns the original function.
So decorators are very powerful and useful tool in Python since it allows programmers to
modify the behavior of function or class.
It allow us to wrap another function in order to extend the behavior of the wrapped
function, without permanently modifying it.
As stated above the decorators are used to modify the behavior of function or class.
This is also called metaprogramming because a part of the program tries to modify
another part of the program at compile time.
In Decorators, functions are taken as the argument into another function and then called
inside the wrapper function.
Output:
Employee Name is: John
Demo Program:
def dollar(func):
def inner(arg):
print("$" * 35)
func(arg)
print("$" * 35)
return inner
def star(func):
def inner(arg):
print("*" * 35)
func(arg)
print("*" * 35)
return inner
@dollar
@star
def printer(msg):
print(msg)
printer("This is chaining in decorators")
Output:
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
***********************************
This is chaining in decorators
***********************************
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
The order in which we chain decorators matter. If we had reversed the order as,
@star
@dollar
def printer(msg):
print(msg)
Output:
***********************************
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
This is chaining in decorators
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
***********************************