In programming languages, mainly there are two approaches that are used to write program or code.
The procedure we are following till now is the “Procedural Programming” approach. So, in this session, we will learn about Object Oriented Programming (OOP). The basic idea of object-oriented programming (OOP) in Python is to use classes and objects to represent real-world concepts and entities.
A class is a blueprint or template for creating objects. It defines the properties and methods that an object of that class will have. Properties are the data or state of an object, and methods are the actions or behaviors that an object can perform.
An object is an instance of a class, and it contains its own data and methods. For example, you could create a class called “Person” that has properties such as name and age, and methods such as speak() and walk(). Each instance of the Person class would be a unique object with its own name and age, but they would all have the same methods to speak and walk.
encapsulation, which means that the internal state of an object is hidden and can only be accessed or modified through the object’s methods. This helps to protect the object’s data and prevent it from being modified in unexpected ways.
inheritance, which allows new classes to be created that inherit the properties and methods of an existing class. This allows for code reuse and makes it easy to create new classes that have similar functionality to existing classes.
Polymorphism is also supported in Python, which means that objects of different classes can be treated as if they were objects of a common class. This allows for greater flexibility in code and makes it easier to write code that can work with multiple types of objects.
Abstraction is The process of hiding the complex implementation details and showing only the necessary features of an object. It helps in reducing programming complexity and effort.
In summary, OOP in Python allows developers to model real-world concepts and entities using classes and objects, encapsulate data, reuse code through inheritance, and write more flexible code through polymorphism.
A class is a blueprint or a template for creating objects, providing initial values for state (member variables or attributes), and implementations of behavior (member functions or methods). The user-defined objects are created using the class keyword.
Let us now create a class using the class keyword.
class Details:
name = “Rohan”
age = 20
Object is the instance of the class used to access the properties of the class Now lets create an object of the class.
obj1 = Details()
class Details:
name = “Rohan”
age = 20
obj1 = Details()
print(obj1.name)
print(obj1.age)
Rohan
20
The self parameter is a reference to the current instance of the class, and is used to access variables that belongs to the class.
It must be provided as the extra parameter inside the method definition.
class Details: name = “Rohan” age = 20
def desc(self):
print(“My name is”, self.name, “and I’m”, self.age, “years old.”)
obj1 = Details()
obj1.desc()
My name is Rohan and I’m 20 years old.
A constructor is a special method in a class used to create and initialize an object of a class. There are different types of constructors. Constructor is invoked automatically when an object of a class is created.
A constructor is a unique function that gets called automatically when an object is created of a class. The main purpose of a constructor is to initialize or assign values to the data members of that class. It cannot return any value other than None.
def init (self):
#initializations
“init” is one of the reserved functions in Python. In Object Oriented Programming, it is known as a constructor.
When the constructor accepts arguments along with self, it is known as parameterized constructor. These arguments can be used inside the class to assign the values to the data members.
class Details:
def init (self, animal, group): self.animal = animal self.group = group
obj1 = Details(“Crab”, “Crustaceans”)
print(obj1.animal, “belongs to the”, obj1.group, “group.”)
Crab belongs to the Crustaceans group.
When the constructor doesn’t accept any arguments from the object and has only one argument, self, in the constructor, it is known as a Default constructor.
class Details:
def init (self):
print(“animal Crab belongs to Crustaceans group”) obj1=Details()
animal Crab belongs to Crustaceans group
Python decorators are a powerful and versatile tool that allow you to modify the behavior of functions and methods. They are a way to extend the functionality of a function or method without modifying its source code.
A decorator is a function that takes another function as an argument and returns a new function that modifies the behavior of the original function. The new function is often referred to as a “decorated” function. The basic syntax for using a decorator is the following:
@decorator_function
def my_function():
pass
The @decorator_function notation is just a shorthand for the following code:
def my_function():
pass
my_function = decorator_function(my_function)
Decorators are often used to add functionality to functions and methods, such as logging, memoization, and access control.
One common use of decorators is to add logging to a function. For example, you could use a decorator to log the arguments and return value of a function each time it is called:
import logging
def log_function_call(func):
def decorated(*args, **kwargs):
logging.info(f”Calling {func. name } with args={args}, kwargs={kwargs}”)
result = func(*args, **kwargs) logging.info(f”{func. name } returned {result}”) return result
return decorated
@log_function_call
def my_function(a, b): return a + b
In this example, the log_function_call decorator takes a function as an argument and returns a new function that logs the function call before and after the original function is called.
Decorators are a powerful and flexible feature in Python that can be used to add functionality to functions and methods without modifying their source code. They are a great tool for separating concerns, reducing code duplication, and making your code more readable and maintainable.
In conclusion, python decorators are a way to extend the functionality of functions and methods, by modifying its behavior without modifying the source code. They are used for a variety of purposes, such as logging, memoization, access control, and more. They are a powerful tool that can be used to make your code more readable, maintainable, and extendable.
Getters in Python are methods that are used to access the values of an object’s properties. They are used to return the value of a specific property, and are typically defined using the @property decorator. Here is an example of a simple class with a getter method:
class MyClass:
def init (self, value): self._value = value
@property
def value(self): return self._value
In this example, the MyClass class has a single property, _value, which is initialized in the init method.
The value method is defined as a getter using the @property decorator, and is used to return the value of the
_value property.
To use the getter, we can create an instance of the MyClass class, and then access the value property as if it were an attribute:
>>> obj = MyClass(10)
>>> obj.value 10
It is important to note that the getters do not take any parameters and we cannot set the value through getter method.For that we need setter method which can be added by decorating method with @property_name.setter Here is an example of a class with both getter and setter:
class MyClass:
def init (self, value): self._value = value
@property
def value(self): return self._value
@value.setter
def value(self, new_value): self._value = new_value
We can use setter method like this:
>>> obj = MyClass(10)
>>> obj.value = 20
>>> obj.value 20
In conclusion, getters are a convenient way to access the values of an object’s properties, while keeping the internal representation of the property hidden. This can be useful for encapsulation and data validation.
When a class derives from another class. The child class will inherit all the public and protected properties and methods from the parent class. In addition, it can have its own properties and methods,this is called as inheritance.
class BaseClass:
#Body of base class
class DerivedClass(BaseClass):
#Body of derived class
Derived class inherits features from the base class where new features can be added to it. This results in re- usability of code.
Single inheritance enables a derived class to inherit properties from a single parent class, thus enabling code reusability and the addition of new features to existing code.
class Parent:
def func1(self):
print(“This function is in parent class.”)
class Child(Parent): def func2(self):
print(“This function is in child class.”)
object = Child() object.func1() object.func2()
This function is in parent class.
This function is in child class.
When a class can be derived from more than one base class this type of inheritance is called multiple inheritances. In multiple inheritances, all the features of the base classes are inherited into the derived class.
class Mother:
mothername = “”
def mother(self): print(self.mothername)
class Father:
fathername = “”
def father(self): print(self.fathername)
class Son(Mother, Father): def parents(self):
print(“Father name is :”, self.fathername) print(“Mother :”, self.mothername)
s1 = Son() s1.fathername = “Mommy” s1.mothername = “Daddy” s1.parents()
Father name is :
Mommy Mother : Daddy
In multilevel inheritance, features of the base class and the derived class are further inherited into the new derived class. This is similar to a relationship representing a child and a grandfather.
class Grandfather:
def init (self, grandfathername): self.grandfathername = grandfathername
class Father(Grandfather):
def init (self, fathername, grandfathername): self.fathername = fathername
Grandfather. init (self, grandfathername) class Son(Father):
def init (self, sonname, fathername, grandfathername): self.sonname = sonname
Father. init (self, fathername, grandfathername)
def print_name(self):
print(‘Grandfather name :’, self.grandfathername) print(“Father name :”, self.fathername) print(“Son name :”, self.sonname)
s1 = Son(‘Prince’, ‘Rampal’, ‘Lal mani’) print(s1.grandfathername) s1.print_name()
Lal mani
Grandfather name : Lal mani Father name : Rampal
Son name : Prince
When more than one derived class are created from a single base this type of inheritance is called hierarchical inheritance. In this program, we have a parent (base) class and two child (derived) classes.
class Parent:
def func1(self):
print(“This function is in parent class.”)
class Child1(Parent): def func2(self):
print(“This function is in child 1.”)
class Child2(Parent): def func3(self):
print(“This function is in child 2.”)
object1 = Child1() object2 = Child2() object1.func1() object1.func2() object2.func1() object2.func3()
Inheritance consisting of multiple types of inheritance is called hybrid inheritance.
class School:
def func1(self):
print(“This function is in school.”)
class Student1(School): def func2(self):
print(“This function is in student 1. “)
class Student2(School): def func3(self):
print(“This function is in student 2.”)
class Student3(Student1, School): def func4(self):
print(“This function is in student 3.”)
object = Student3() object.func1() object.func2()
This function is in school.
This function is in student 1.
Access specifiers or access modifiers in python programming are used to limit the access of class variables and class methods outside of class while implementing the concepts of inheritance.
Let us see the each one of access specifiers in detail:
class Student:
# constructor is defined
def init (self, age, name):
self.age = age # public variable
self.name = name # public variable
obj = Student(21,”Harry”) print(obj.age) print(obj.name)
21
Harry
By definition, Private members of a class (variables or methods) are those members which are only accessible inside the class. We cannot use private members outside of class.
In Python, there is no strict concept of “private” access modifiers like in some other programming languages. However, a convention has been established to indicate that a variable or method should be considered private by prefixing its name with a double underscore ( ). This is known as a “weak internal use indicator” and it is a convention only, not a strict rule. Code outside the class can still access these “private” variables and methods, but it is generally understood that they should not be accessed or modified.
class Student:
def init (self, age, name):
self. age = age # An indication of private variable
def funName(self): # An indication of private function
self.y = 34
print(self.y)
class Subject(Student): pass
obj = Student(21,”Harry”) obj1 = Subject
# calling by object of class Student
print(obj. age) print(obj. funName())
# calling by object of class Subject
print(obj1. age) print(obj1. funName())
AttributeError Traceback (most recent call last) Input In [10], in <cell line: 16>()
13 obj1 = Subject
15 # calling by object of class Student
—> 16 print(obj. age)
17 print(obj. funName())
19 # calling by object of class Subject AttributeError: ‘Student’ object has no attribute ‘ age’
Private members of a class cannot be accessed or inherited outside of class. If we try to access or to inherit the properties of private members to child class (derived class). Then it will show the error.
Name mangling in Python is a technique used to protect class-private and superclass-private attributes from being accidentally overwritten by subclasses. Names of class-private and superclass-private attributes are transformed by the addition of a single leading underscore and a double leading underscore respectively.
class MyClass:
def init (self):
self._nonmangled_attribute = “I am a nonmangled attribute” self. mangled_attribute = “I am a mangled attribute”
my_object = MyClass()
print(my_object._nonmangled_attribute) # Output: I am a nonmangled attribute print(my_object. mangled_attribute) # Throws an AttributeError print(my_object._MyClass mangled_attribute) # Output: I am a mangled attribute
In object-oriented programming (OOP), the term “protected” is used to describe a member (i.e., a method or attribute) of a class that is intended to be accessed only by the class itself and its subclasses. In Python, the convention for indicating that a member is protected is to prefix its name with a single underscore (_). For example, if a class has a method called _my_method, it is indicating that the method should only be accessed by the class itself and its subclasses.
It’s important to note that the single underscore is just a naming convention, and does not actually provide any protection or restrict access to the member. The syntax we follow to make any variable protected is to write variable name followed by a single underscore (_) ie. _varName.
class Student:
def init (self): self._name = “Harry”
def _funName(self): # protected method
return “CodeWithHarry”
class Subject(Student): #inherited class
pass
obj = Student() obj1 = Subject()
# calling by object of Student class print(obj._name) print(obj._funName())
# calling by object of Subject class print(obj1._name) print(obj1._funName())
Harry
CodeWithHarry
Harry
CodeWithHarry
In Python, variables can be defined at the class level or at the instance level. Understanding the difference between these types of variables is crucial for writing efficient and maintainable code.
Class variables are defined at the class level and are shared among all instances of the class. They are defined outside of any method and are usually used to store information that is common to all instances of the class. For example, a class variable can be used to store the number of instances of a class that have been created.
class MyClass: class_variable = 0
def init (self):
MyClass.class_variable += 1
def print_class_variable(self): print(MyClass.class_variable)
obj1 = MyClass() obj2 = MyClass()
obj1.print_class_variable() # Output: 2
obj2.print_class_variable() # Output: 2
2
2
In the example above, the class_variable is shared among all instances of the class MyClass. When we create new instances of MyClass, the value of class_variable is incremented. When we call the print_class_variable method on obj1 and obj2, we get the same value of class_variable.
Instance variables are defined at the instance level and are unique to each instance of the class. They are defined inside the init method and are usually used to store information that is specific to each instance of the class. For example, an instance variable can be used to store the name of an employee in a class that represents an employee.
class MyClass:
def init (self, name): self.name = name
def print_name(self): print(self.name)
obj1 = MyClass(“John”) obj2 = MyClass(“Jane”)
obj1.print_name() # Output: John
obj2.print_name() # Output: Jane
John
Jane
In the example above, each instance of the class MyClass has its own value for the name variable. When we call the print_name method on obj1 and obj2, we get different values for name.
In summary, class variables are shared among all instances of a class and are used to store information that is common to all instances. Instance variables are unique to each instance of a class and are used to store information that is specific to each instance. Understanding the difference between class variables and instance variables is crucial for writing efficient and maintainable code in Python.
It’s also worth noting that, in python, class variables are defined outside of any methods and don’t need to be explicitly declared as class variable. They are defined in the class level and can be accessed via classname.varibale_name or self.class.variable_name. But instance variables are defined inside the methods and need to be explicitly declared as instance variable by using self.variable_name.
In Python, classes are a way to define custom data types that can store data and define functions that can manipulate that data. One type of function that can be defined within a class is called a “method.” In this blog post, we will explore what Python class methods are, why they are useful, and how to use them.
A class method is a type of method that is bound to the class and not the instance of the class. In other words, it operates on the class as a whole, rather than on a specific instance of the class. Class methods are defined using the “@classmethod” decorator, followed by a function definition. The first argument of the function is always “cls,” which represents the class itself.
Class methods are useful in several situations. For example, you might want to create a factory method that creates instances of your class in a specific way. You could define a class method that creates the instance and returns it to the caller. Another common use case is to provide alternative constructors for your class. This can be useful if you want to create instances of your class in multiple ways, but still have a consistent interface for doing so.
To define a class method, you simply use the “@classmethod” decorator before the method definition. The first argument of the method should always be “cls,” which represents the class itself. Here is an example of how to define a class method:
class ExampleClass: @classmethod
def factory_method(cls, argument1, argument2): return cls(argument1, argument2)
In this example, the “factory_method” is a class method that takes two arguments, “argument1” and “argument2.” It creates a new instance of the class “ExampleClass” using the “cls” keyword, and returns the new instance to the caller.
It’s important to note that class methods cannot modify the class in any way. If you need to modify the class, you should use a class level variable instead.
Python class methods are a powerful tool for defining functions that operate on the class as a whole, rather than on a specific instance of the class. They are useful for creating factory methods, alternative constructors, and other types of methods that operate at the class level. With the knowledge of how to define and use class methods, you can start writing more complex and organized code in Python.
In object-oriented programming, the term “constructor” refers to a special type of method that is automatically executed when an object is created from a class. The purpose of a constructor is to initialize the object’s attributes, allowing the object to be fully functional and ready to use.
However, there are times when you may want to create an object in a different way, or with different initial values, than what is provided by the default constructor. This is where class methods can be used as alternative constructors.
A class method belongs to the class rather than to an instance of the class. One common use case for class methods as alternative constructors is when you want to create an object from data that is stored in a different format, such as a string or a dictionary. For example, consider a class named “Person” that has two attributes: “name” and “age”. The default constructor for the class might look like this:
class Person:
def init (self, name, age): self.name = name
self.age = age
But what if you want to create a Person object from a string that contains the person’s name and age, separated by a comma? You can define a class method named “from_string” to do this:
class Person:
def init (self, name, age): self.name = name
self.age = age
@classmethod
def from_string(cls, string): name, age = string.split(‘,’) return cls(name, int(age))
Now you can create a Person object from a string like this:
person = Person.from_string(“John Doe, 30”)
Another common use case for class methods as alternative constructors is when you want to create an object with a different set of default values than what is provided by the default constructor. For example, consider a class named “Rectangle” that has two attributes: “width” and “height”. The default constructor for the class might look like this:
class Rectangle:
def init (self, width, height): self.width = width
self.height = height
But what if you want to create a Rectangle object with a default width of 10 and a default height of 5? You can define a class method named “square” to do this:
class Rectangle:
def init (self, width, height): self.width = width
self.height = height
@classmethod
def square(cls, size): return cls(size, size)
Now you can create a square rectangle like this:
rectangle = Rectangle.square(10)
We must look into dir(), dict() and help() attribute/methods in python. They make it easy for us to understand how classes resolve various functions and executes code. In Python, there are three built-in functions that are commonly used to get information about objects: dir(), dict, and help(). Let’s take a look at each of them:
dir(): The dir() function returns a list of all the attributes and methods (including dunder methods) available for an object. It is a useful tool for discovering what you can do with an object. Example:
x = [1, 2, 3]
dir(x)
dict: The dict attribute returns a dictionary representation of an object’s attributes. It is a useful tool for introspection. Example:
>> class Person:
def init (self, name, age): self.name = name
self.age = age
>>> p = Person(“John”, 30)
>>> p. dict
{‘name’: ‘John’, ‘age’: 30}
help(): The help() function is used to get help documentation for an object, including a description of its attributes and methods.
Example:
help(str)
Help on class str in module builtins:
class str(object)
str(object=”) -> str
str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or
errors is specified, then the object must expose a data buffer
that will be decoded using the given encoding and error handler.
Otherwise, returns the result of object. str () (if defined)
or repr(object).
encoding defaults to sys.getdefaultencoding().
errors defaults to ‘strict’.
Methods defined here:
add (self, value, /)
Return self+value.
contains (self, key, /)
Return key in self.
eq (self, value, /)
Return self==value.
format (self, format_spec, /)
Return a formatted version of the string as described by format_spec.
ge (self, value, /)
Return self>=value.
getattribute (self, name, /)
Return getattr(self, name).
getitem (self, key, /)
Return self[key].
getnewargs (…)
gt (self, value, /)
Return self>value.
hash (self, /)
Return hash(self).
iter (self, /)
Implement iter(self).
le (self, value, /)
Return self<=value.
len (self, /)
Return len(self).
lt (self, value, /)
Return self<value.
mod (self, value, /)
Return self%value.
mul (self, value, /)
Return self*value.
ne (self, value, /)
Return self!=value.
repr (self, /)
Return repr(self).
rmod (self, value, /)
Return value%self.
rmul (self, value, /)
Return value*self.
sizeof (self, /)
Return the size of the string in memory, in bytes.
str (self, /)
Return str(self).
capitalize(self, /)
Return a capitalized version of the string.
More specifically, make the first character have upper case and the rest lower
case.
casefold(self, /)
Return a version of the string suitable for caseless comparisons.
center(self, width, fillchar=’ ‘, /)
Return a centered string of length width.
Padding is done using the specified fill character (default is a space).
count(…)
S.count(sub[, start[, end]]) -> int
Return the number of non-overlapping occurrences of substring sub in
string S[start:end]. Optional arguments start and end are
interpreted as in slice notation.
encode(self, /, encoding=’utf-8′, errors=’strict’)
Encode the string using the codec registered for encoding.
encoding
The encoding in which to encode the string.
errors
The error handling scheme to use for encoding errors.
The default is ‘strict’ meaning that encoding errors raise a
UnicodeEncodeError. Other possible values are ‘ignore’, ‘replace’ and
‘xmlcharrefreplace’ as well as any other name registered with
codecs.register_error that can handle UnicodeEncodeErrors.
endswith(…)
S.endswith(suffix[, start[, end]]) -> bool
Return True if S ends with the specified suffix, False otherwise.
With optional start, test S beginning at that position.
With optional end, stop comparing S at that position.
suffix can also be a tuple of strings to try.
expandtabs(self, /, tabsize=8)
Return a copy where all tab characters are expanded using spaces.
If tabsize is not given, a tab size of 8 characters is assumed.
find(…)
S.find(sub[, start[, end]]) -> int
Return the lowest index in S where substring sub is found,
such that sub is contained within S[start:end]. Optional
arguments start and end are interpreted as in slice notation.
Return -1 on failure.
format(…)
S.format(*args, **kwargs) -> str
Return a formatted version of S, using substitutions from args and kwargs.
The substitutions are identified by braces (‘{‘ and ‘}’).
format_map(…)
S.format_map(mapping) -> str
Return a formatted version of S, using substitutions from mapping.
The substitutions are identified by braces (‘{‘ and ‘}’).
index(…)
S.index(sub[, start[, end]]) -> int
Return the lowest index in S where substring sub is found,
such that sub is contained within S[start:end]. Optional
arguments start and end are interpreted as in slice notation.
Raises ValueError when the substring is not found.
isalnum(self, /)
The super() keyword in Python is used to refer to the parent class. It is especially useful when a class inherits from multiple parent classes and you want to call a method from one of the parent classes.
When a class inherits from a parent class, it can override or extend the methods defined in the parent class. However, sometimes you might want to use the parent class method in the child class. This is where the super() keyword comes in handy.
Here’s an example of how to use the super() keyword in a simple inheritance scenario:
class ParentClass:
def parent_method(self):
print(“This is the parent method.”)
class ChildClass(ParentClass): def child_method(self):
print(“This is the child method.”) super().parent_method()
child_object = ChildClass() child_object.child_method()
Hierarchical Inheritance is a type of inheritance in Object-Oriented Programming where multiple subclasses inherit from a single base class. In other words, a single base class acts as a parent class for multiple subclasses. This is a way of establishing relationships between classes in a hierarchical manner.
class Animal:
def init (self, name): self.name = name
def show_details(self): print(“Name:”, self.name)
class Dog(Animal):
def init (self, name, breed): Animal. init (self, name) self.breed = breed
def show_details(self): Animal.show_details(self) print(“Species: Dog”) print(“Breed:”, self.breed)
class Cat(Animal):
def init (self, name, color): Animal. init (self, name) self.color = color
def show_details(self): Animal.show_details(self) print(“Species: Cat”) print(“Color:”, self.color)
In the above code, the Animal class acts as the base class for two subclasses, Dog and Cat. The Dog class and the Cat class inherit the attributes and methods of the Animal class. However, they can also add their own unique attributes and methods.
dog = Dog(“Max”, “Golden Retriever”) dog.show_details()
cat = Cat(“Luna”, “Black”) cat.show_details()
Name: Max Species: Dog
Breed: Golden Retriever Name: Luna
Species: Cat Color: Black
As we can see from the outputs, the Dog and Cat classes have inherited the attributes and methods of the Animal class, and have also added their own unique attributes and methods.
In conclusion, hierarchical inheritance is a way of establishing relationships between classes in a hierarchical manner. It allows multiple subclasses to inherit from a single base class, which helps in code reuse and organization of code in a more structured manner.
