Information Infrastructure II Python Notes
Information Infrastructure II Python Notes
# Output:
# Make: Toyota
# Model: Camry
In the above example, we have defined a Car class with a constructor method and a display_info
method. We then created an object of the Car class and called the display_info method on it to
display its information.
OOP in Python allows for code reusability, modularity, and easier maintenance of code. It is
widely used in building large and complex applications as it helps to manage the complexity
and improve code organization.
Object-oriented programming (OOP) is a programming paradigm that is based on the concept
of "objects", which can contain data in the form of attributes and code in the form of methods.
In Python, OOP is supported and encouraged, and it is used to create reusable and modular
code.
In OOP, a class is used to create objects, and an object is an instance of a class. Classes can
have attributes (variables) and methods (functions), and they can also inherit attributes and
methods from other classes.
Here's an example of a simple class and object in Python:
python
class Car:
def __init__(self, brand, model):
self.brand = brand
self.model = model
def drive(self):
print(f"The {self.brand} {self.model} is driving.")
def make_sound(self):
print("Animal making sound.")
def bark(self):
print(f"{self.name} says Woof!")
2. Objects:
- An object is an instance of a class.
- When you create an object, you are creating a specific instance of that class, with its own
set of attributes and methods.
Example:
python
my_dog = Dog("Buddy", 3)
my_dog.bark() # Output: Buddy says Woof!
3. Attributes and Methods:
- Attributes are the data elements of the object. They are defined within the class and are
accessed using dot notation with the object.
- Methods are the functions defined within the class and are used to perform operations on
the object's data.
Example:
python
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius**2
The understanding of classes and objects in Python allows the programmer to create modular,
reusable, and organized code, making it easier to manage and extend software projects. This is
one of the key principles of OOP, and Python provides an excellent platform for implementing
this paradigm
b) Attributes and Methods
Object-oriented programming (OOP) is a programming paradigm that allows for the creation
of objects that have both attributes and methods. Attributes are the characteristics or properties
of an object, while methods are functions that can be performed on an object.
In Python, attributes and methods are defined within a class, which serves as a blueprint for
creating objects. Let's take a look at how attributes and methods are defined and used in Python.
Attributes:
Attributes in Python can be either instance attributes or class attributes. Instance attributes are
specific to each individual object and can vary from one object to another. They are defined
within the _init_ method of a class and are accessed using the dot notation.
For example, let's define a class called "Car" with two instance attributes - "color" and "model".
class Car:
def _init_(self, color, model):
self.color = color
self.model = model
Now, we can create objects of the Car class and assign values to its attributes.
print(Car.wheels) # output: 4
Methods:
Methods in Python are functions that are defined within a class and can be called on objects of
that class. They can be either instance methods or class methods.
Instance methods are defined with the self parameter, which refers to the current object. These
methods can access and modify the attributes of an object.
class Car:
def _init_(self, color, model):
self.color = color
self.model = model
def drive(self):
print("Driving the", self.model)
@classmethod
def get_wheels(cls):
print("This car has", cls.wheels, "wheels")
Methods:
- Methods are functions defined within a class that define the behavior or actions a class object
can perform.
- In Python, methods can be instance methods (operate on specific instances of the class) or
class methods (operate on the class itself).
- The `self` parameter in instance methods refers to the specific instance of the object on which
the method is being called.
- Example:
python
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def display_info(self): # Instance method
print(f"{self.year} {self.make} {self.model}")
@staticmethod
def info(): # Class method
print("This is a car class")
In OOP, classes can have multiple attributes and methods to represent the characteristics and
behaviors of the objects they create. When an object is created from a class, it inherits the
attributes and methods defined in that class. These attributes and methods can then be accessed
and called using the dot notation (`object.attribute` or `object.method()`).
c) The `self` keyword
In Python, the self keyword is used to refer to the current instance of a class. It is the first
parameter in a method definition and is automatically passed in when calling the method on an
object.
For example, let's define a class called "Person" with an instance method called "greet".
class Person:
def greet(self):
print("Hello, my name is", self.name)
Now, we can create objects of the Person class and call the greet method on them.
person1 = Person()
person1.name = "John"
person1.greet() # output: Hello, my name is John
In this example, the self keyword refers to the object "person1", and allows us to access its
attribute "name" within the greet method.
The self keyword is not limited to just instance methods, it can also be used in class methods
and static methods. In class methods, it refers to the class itself, while in static methods it does
not have any specific purpose.
It is important to note that the self keyword can be named anything else, but it is a convention
in Python to use "self" as it makes the code more readable and consistent.
In summary, the self keyword is a crucial part of object-oriented programming in Python as it
allows for the manipulation of object attributes and methods within the class definition.
In Python, the `self` keyword is used within the definition of instance methods to refer to the
specific instance of the class on which the method is being called. When a method is called on
an object, the object itself is automatically passed as the first argument to the method. This
convention allows the method to access and manipulate the attributes of the specific instance.
Here's an example to illustrate the use of `self` in Python:
python
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def calculate_area(self):
return self.length * self.width
# Create an object
rect = Rectangle(5, 3)
# Assign attributes
rect.color = "red"
# Call methods
print("The area of the rectangle is", rect.calculate_area()) # output: The area of the rectangle is
15
In this example, we defined a class called "Rectangle" with two attributes (length and width)
and one method (calculate_area). Then, we created an object of this class called "rect" and
assigned values to its attributes. Finally, we called the calculate_area method on the object and
printed the result.
In conclusion, creating and using objects in object-oriented programming in Python allows us
to create multiple instances of a class with different attributes and behaviors, making our code
more organized and efficient.
In Python, object-oriented programming (OOP) is based on the concept of creating classes to
define new types of objects, and then creating instances of those classes to work with. Here's
an example to illustrate creating and using objects in Python:
python
# Defining a simple class
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
def bark(self):
print(f"{self.name} says woof!")
def get_name(self):
return self._name
def get_age(self):
return self.__age
# Create an object
person = Person("John", 25)
def get_model(self):
return self.model
In this example, the `brand` and `model` variables are considered public and can be accessed
and modified directly from outside the Car class.
2. Private Access: In Python, attributes and methods can be indicated as private by prefixing
them with double underscores "__". This doesn't make them completely private, but the
interpreter name-mangles the attribute names, making them harder to access from outside the
class.
python
class Car:
def __init__(self, brand, model):
self.__brand = brand # private variable
self.__model = model # private variable
def get_model(self):
return self.__model
In this example, the `_brand` and `_model` variables are considered private and should not be
accessed or modified directly from outside the Car class. They can still be accessed using name
mangling, but it's not a recommended practice.
It's important to note that in Python, the use of single underscores "_" is generally used as a
convention to indicate that an attribute or method is protected or private, and it should be treated
as such, but it doesn't actually enforce encapsulation in the same way as languages with strict
access modifiers. It's a matter of convention and best practice to respect the privacy of attributes
and methods.
ii. Getter and Setter Methods
Getter and setter methods are used in encapsulation to provide controlled access to private
attributes of a class. These methods are also known as accessor and mutator methods,
respectively.
Getter methods are used to retrieve the value of a private attribute. They typically have the
prefix "get" followed by the attribute name. For example, a getter method for a private attribute
"age" would be named "get_age()". Getter methods do not take any parameters and return the
value of the attribute.
Setter methods, on the other hand, are used to modify the value of a private attribute. They
typically have the prefix "set" followed by the attribute name. For example, a setter method for
a private attribute "age" would be named "set_age()". Setter methods take in a parameter that
represents the new value for the attribute and set it accordingly.
The use of getter and setter methods in encapsulation allows for controlled access to private
attributes, ensuring that they are not accidentally modified from outside the class. This also
allows for validation and manipulation of data before setting it as the value of an attribute.
Additionally, getter and setter methods can also be used to implement additional logic or
functionality, making them a powerful tool in encapsulation.
In Python, encapsulation can be achieved using getter and setter methods to control access to
attributes of a class. Getter methods are used to retrieve the values of private attributes, and
setter methods are used to modify the values of private attributes. Here's an example of how
getter and setter methods can be implemented in Python:
python
class Car:
def __init__(self, brand, model):
self.__brand = brand # private variable
self.__model = model # private variable
# Getter methods
def get_brand(self):
return self.__brand
def get_model(self):
return self.__model
# Setter methods
def set_brand(self, brand):
self.__brand = brand
my_car.set_brand("Honda")
my_car.set_model("Accord")
print(my_car.get_brand()) # Output: Honda
print(my_car.get_model()) # Output: Accord
Using getter and setter methods allows the class to encapsulate its internal implementation
details and provides a controlled way to access and modify the private attributes. This helps in
maintaining the integrity of the class and adhering to the principles of encapsulation.
b) Inheritance
Inheritance is a fundamental concept in Object-Oriented Programming (OOP) that allows a
class to inherit attributes and methods from another class. The class that inherits from another
class is called the child class or subclass, and the class that is being inherited from is called the
parent class or superclass.
In Python, inheritance is implemented using the keyword "class" followed by the name of the
child class, a set of parentheses, and the name of the parent class. For example:
class ChildClass(ParentClass):
# class body
This creates a child class that inherits all the attributes and methods of the parent class. The
child class can then add its own unique attributes and methods, or override existing ones from
the parent class.
One of the main benefits of inheritance is code reusability. Instead of writing the same code in
multiple classes, we can define it once in a parent class and have all child classes inherit it.
This not only saves time and effort but also makes our code more organized and easier to
maintain.
Another benefit of inheritance is the ability to create specialized classes. We can have a general
parent class with common attributes and methods, and then create more specific child classes
that inherit from it and add their own unique features. This allows for a more efficient and
flexible design of our code.
In addition to inheriting attributes and methods, child classes can also access and use the
attributes and methods of their parent class. This is achieved using the "super()" function, which
allows us to call the parent class's constructor or methods within the child class.
Overall, inheritance is a powerful tool in OOP that promotes code reusability, flexibility, and
organization. It is widely used in Python and other programming languages to create efficient
and maintainable code.
Inheritance is a fundamental concept in object-oriented programming (OOP) that allows a new
class to inherit properties and behaviors from an existing class. In Python, inheritance is
implemented using the following syntax:
python
class ParentClass:
# Parent class attributes and methods
class ChildClass(ParentClass):
# Child class attributes and methods
In this example, `ChildClass` is inheriting from `ParentClass`. The `ParentClass` is also
referred to as the base class or superclass, while the `ChildClass` is the derived class or
subclass.
When a class inherits from another class, it automatically gains all the attributes and methods
of the parent class. This allows code reuse and helps in creating a hierarchy of classes with
shared functionality.
Here's a simple example of inheritance in Python:
python
class Animal:
def __init__(self, name):
self.name = name
def make_sound(self):
pass
class Dog(Animal):
def make_sound(self):
print(f"{self.name} says Woof!")
class Cat(Animal):
def make_sound(self):
print(f"{self.name} says Meow!")
dog = Dog("Buddy")
dog.make_sound() # Output: Buddy says Woof!
cat = Cat("Whiskers")
cat.make_sound() # Output: Whiskers says Meow!
In this example, the `Animal` class is the base class, and the `Dog` and `Cat` classes are derived
from it. Both `Dog` and `Cat` classes inherit the `name` attribute and have their own
implementation of the `make_sound` method.
Inheritance allows for code reusability, promotes modularity, and helps in creating a more
organized and maintainable codebase. It also enables polymorphism, which allows objects of
different classes to be treated as objects of a common parent class.
i. Extending Classes
Inheritance in Python also allows us to extend classes, which means adding new attributes and
methods to a child class without modifying the parent class. This is achieved by defining new
attributes and methods in the child class, which will be available to instances of that class.
For example, let's say we have a parent class called "Animal" with attributes such as name and
age, and methods such as eat() and sleep(). We can then create a child class called "Dog" that
inherits from "Animal" and adds its own unique attribute "breed" and method "bark()". This
way, instances of the "Dog" class will have access to all the attributes and methods of the
"Animal" class, as well as its own unique features.
Extending classes in inheritance allows for a more modular and scalable approach to coding.
We can have a base class with common attributes and methods, and then extend it to create
more specialized classes with additional features. This makes our code more flexible and
adaptable to different use cases.
Furthermore, extending classes can also help with code maintenance. If we need to make
changes to a specific feature in one of our child classes, we can do so without affecting the rest
of the codebase. This promotes code modularity and makes it easier to debug and fix issues.
In conclusion, extending classes in inheritance is a powerful feature in Python that allows for
code reusability, flexibility, and maintainability. It is a key concept in OOP that helps us create
efficient and organized code.
Extending classes in inheritance in Python involves adding new attributes and methods to the
derived class while still having access to the attributes and methods of the base class.
Here's an example of extending classes in inheritance in Python:
python
class Animal:
def __init__(self, name):
self.name = name
def make_sound(self):
pass
class Dog(Animal):
def make_sound(self):
print(f"{self.name} says Woof!")
def wag_tail(self):
print(f"{self.name} is wagging tail")
dog = Dog("Buddy")
dog.make_sound() # Output: Buddy says Woof!
dog.wag_tail() # Output: Buddy is wagging tail
In the example above, the `Dog` class extends the `Animal` class by adding a new method
`wag_tail`. The `Dog` class inherits the `name` attribute and `make_sound` method from the
`Animal` class and adds the `wag_tail` method, making it an extension of the base `Animal`
class.
By extending classes through inheritance in Python, you can build on the functionality of
existing classes, add new features, and create more specialized classes while still reusing the
code from the base class. This reduces code duplication and makes the code more maintainable
and readable.
ii. Method Overriding
Method overriding is a feature in object-oriented programming that allows a subclass to provide
a different implementation of a method that is already defined in its superclass. This means that
the subclass can redefine the behavior of a method inherited from its superclass.
In Python, method overriding is achieved by simply defining a method with the same name
and signature as the method in the superclass. When an object of the subclass calls this method,
the overridden method in the subclass will be executed instead of the one in the superclass.
Let's look at an example to understand how method overriding works in Python:
class Animal:
def make_sound(self):
print("Animal makes a sound")
class Cat(Animal):
def make_sound(self):
print("Meow")
cat = Cat()
cat.make_sound() # Output: Meow
In this example, we have a superclass Animal with a method make_sound() that prints "Animal
makes a sound". We then create a subclass Cat which inherits from Animal and overrides the
make_sound() method to print "Meow" instead.
When we create an object of Cat and call the make_sound() method, the overridden method in
the subclass will be executed, and we will get the output "Meow" instead of "Animal makes a
sound".
One important thing to note is that when overriding a method, the number and type of
parameters must be the same as the method in the superclass. This ensures that the method can
be called in the same way from both the superclass and the subclass.
class Animal:
def make_sound(self, sound):
print("Animal makes a", sound)
class Cat(Animal):
def make_sound(self, sound):
print("Meow")
cat = Cat()
cat.make_sound("purr") # Output: Meow
In this example, we have added a parameter sound to the make_sound() method in both the
superclass and the subclass. This allows us to pass in a sound as an argument when calling the
method. However, since the method is overridden in the subclass, the "Animal makes a" part
of the output is not printed, and we only get "Meow" as the output.
In conclusion, method overriding in inheritance in Python allows subclasses to provide their
own implementation of a method inherited from their superclass. This provides flexibility and
allows for different behaviors to be defined for different subclasses, making it a powerful
feature in object-oriented programming.
Method overriding in inheritance in Python allows a subclass to provide a specific
implementation of a method that is already defined in its superclass. When the subclass has a
method with the same name and signature as a method in its superclass, the subclass method
overrides the superclass method.
Here's an example of method overriding in inheritance in Python:
python
class Animal:
def make_sound(self):
print("Generic animal sound")
class Dog(Animal):
def make_sound(self):
print("Woof!")
class Cat(Animal):
def make_sound(self):
print("Meow!")
animal = Animal()
animal.make_sound() # Output: Generic animal sound
dog = Dog()
dog.make_sound() # Output: Woof!
cat = Cat()
cat.make_sound() # Output: Meow!
In the example above, the `Dog` and `Cat` classes override the `make_sound` method that is
defined in the `Animal` class. When calling `make_sound` on an instance of `Dog` or `Cat`,
the overridden method in the respective subclass is executed instead of the method in the
superclass.
Method overriding allows subclasses to provide their own specific implementations of
methods, allowing for polymorphic behavior. This can be useful for creating specialized
behavior for subclasses while still maintaining a common interface through inheritance.
By using method overriding in inheritance, you can create more flexible and extensible code
in Python, where different subclasses can provide their own behavior for methods defined in
the superclass.
iii. Super() Function
The super() function in Python is used to access methods and properties from a superclass in a
subclass. It is often used in inheritance to call the constructor of the superclass or to access
methods and properties that have been overridden in the subclass.
Let's look at an example to understand how the super() function works in inheritance:
class Animal:
def _init_(self, name):
self.name = name
class Cat(Animal):
def _init_(self, name, color):
super()._init_(name)
self.color = color
class Cat(Animal):
def make_sound(self):
super().make_sound()
print("Meow")
cat = Cat()
cat.make_sound() # Output: Animal makes a sound
# Meow
In this example, we have a superclass Animal with a method make_sound() that prints "Animal
makes a sound". We then create a subclass Cat which overrides the make_sound() method to
also print "Meow".
In the overridden method of the subclass, we use the super() function to call the make_sound()
method of the superclass. This allows us to first print "Animal makes a sound" and then add on
"Meow" to the output.
In conclusion, the super() function in inheritance in Python allows us to access methods and
properties from a superclass in a subclass. It is especially useful when we want to call the
constructor of the superclass or access methods and properties that have been overridden in the
subclass.
In Python, the `super()` function is used in inheritance to access methods and properties from
a parent class. It allows a subclass to invoke methods from its superclass, enabling the subclass
to extend or customize the behavior of the inherited methods.
The `super()` function is particularly useful when working with multiple inheritance, as it helps
to resolve the order in which methods are called in the inheritance hierarchy.
Here's an example of using the `super()` function in Python inheritance:
python
class Animal:
def __init__(self, name):
self.name = name
def make_sound(self):
print("Generic animal sound")
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name)
self.breed = breed
def make_sound(self):
super().make_sound()
print("Woof!")
In this example, the `Dog` class inherits from the `Animal` class. Inside the `_init` method of
the `Dog` class, the `super()` function is used to call the `init_` method of the superclass
(`Animal`), ensuring that the `name` property is initialized. Similarly, the
`super().make_sound()` line in the `make_sound` method of the `Dog` class invokes the
`make_sound` method from the `Animal` class before adding the specific sound for a dog.
By using the `super()` function, the code becomes more maintainable, as it allows for proper
method resolution order and facilitates the cooperative invocation of methods from parent
classes in the inheritance hierarchy.
Overall, the `super()` function is a powerful tool in object-oriented programming in Python that
is essential for working with inheritance and creating flexible, reusable code.
c) Polymorphism
Polymorphism in object-oriented programming refers to the ability of objects to take on
different forms or behaviors depending on their context. This means that an object can have
different types or behaviors at different points in time, allowing for more flexibility and
reusability in code.
In Python, polymorphism is achieved through two main mechanisms: method overriding and
method overloading.
Method overriding is when a subclass redefines or overrides a method from its superclass. This
allows the subclass to have its own implementation of the method, while still inheriting the
other properties and methods from the superclass. This is useful when we want to customize
the behavior of a method for a specific subclass.
Method overloading, on the other hand, is when a class has multiple methods with the same
name but different parameters. This allows for more flexibility in calling methods, as different
arguments can be passed in to achieve different results. However, Python does not support
method overloading by default, so developers often use other techniques such as using default
arguments or variable arguments to achieve similar results.
Here's an example of polymorphism in action:
class Animal:
def make_sound(self):
print("Animal makes a sound")
class Cat(Animal):
def make_sound(self):
print("Meow")
class Dog(Animal):
def make_sound(self):
print("Woof")
In this example, we have a superclass Animal with a method make_sound() that prints "Animal
makes a sound". We then create two subclasses Cat and Dog which both override the
make_sound() method with their own implementations.
We then create a list of animals containing objects of both Cat and Dog classes. When we loop
through this list and call the make_sound() method on each object, we get different outputs
depending on the type of object. This is polymorphism in action, where the same method is
behaving differently based on the context of the object.
In conclusion, polymorphism in object-oriented programming in Python allows for more
flexibility and reusability in code by allowing objects to have different forms or behaviors at
different points in time. This is achieved through method overriding and method overloading,
which allow for customization and flexibility in calling methods.
Polymorphism is a key concept in object-oriented programming that allows objects to be
treated as instances of their parent class, even when they are actually instances of a subclass.
This means that different classes can be used interchangeably, as long as they are related
through inheritance.
In Python, polymorphism is achieved through method overriding and method overloading.
Method overriding allows a subclass to provide a specific implementation of a method that is
already defined in its parent class. When an instance of the subclass calls the overridden
method, the subclass's implementation is executed instead of the parent class's implementation.
Method overloading, on the other hand, is the ability to define multiple methods with the same
name but different parameters within a class. However, Python does not support method
overloading in the traditional sense, as it does not provide a way to define multiple methods
with the same name but different parameters. Instead, Python achieves method overloading
through default parameter values or variable-length argument lists.
Here's an example of polymorphism using method overriding in Python:
python
class Animal:
def make_sound(self):
print("Generic animal sound")
class Dog(Animal):
def make_sound(self):
print("Woof!")
class Cat(Animal):
def make_sound(self):
print("Meow!")
def animal_sound(animal):
animal.make_sound()
In this example, the `animal_sound` function accepts an instance of the `Animal` class and
calls its `make_sound` method. When the function is called with instances of the `Dog` and
`Cat` classes, the specific implementations of `make_sound` in the subclasses are executed,
demonstrating polymorphism.
Overall, polymorphism in Python allows for flexibility and reusability in object-oriented
programming by enabling different classes to be used interchangeably, as long as they are
related through inheritance.
i. Method Overloading (using varied inputs)
Method overloading in polymorphism refers to the ability for a class to have multiple methods
with the same name but different parameters. This allows for more flexibility in calling
methods, as different inputs can be passed in to achieve different results.
In Python, method overloading is not supported by default, as the interpreter does not
differentiate between methods with the same name. However, there are some techniques that
can be used to achieve method overloading in Python, such as using default arguments or
variable arguments.
Here's an example of method overloading using default arguments:
class Calculator:
def add(self, x, y):
return x + y
calc = Calculator()
print(calc.add(2, 3)) # Output: 5
print(calc.add(2, 3, 4)) # Output: 9
In this example, we have a Calculator class with two add() methods. The first method takes in
two parameters x and y and returns their sum. The second method also takes in two parameters
x and y, but has an additional default parameter z which is set to 0. This allows us to call the
add() method with either two or three arguments, and the appropriate method will be executed
based on the number of arguments passed in.
Another way to achieve method overloading in Python is by using variable arguments:
class Calculator:
def add(self, *args):
result = 0
for num in args:
result += num
return result
calc = Calculator()
print(calc.add(2, 3)) # Output: 5
print(calc.add(2, 3, 4)) # Output: 9
In this example, the add() method takes in a variable number of arguments using the *args
syntax. This allows us to pass in any number of arguments, and the method will add them all
together and return the result.
In conclusion, method overloading in polymorphism in Python allows for more flexibility in
calling methods by allowing a class to have multiple methods with the same name but different
parameters. This can be achieved using techniques such as default arguments or variable
arguments.
In traditional object-oriented programming languages like Java or C++, method overloading
refers to the ability to define multiple methods with the same name but different parameters
within a class. However, Python does not support method overloading in the same way. Python
does not provide a direct way to define multiple methods with the same name but different
parameters.
However, Python achieves a form of method overloading through the use of default parameter
values and variable-length argument lists.
1. Method Overloading using Default Parameter Values:
In Python, you can achieve method overloading by providing default parameter values. For
example:
python
class MyClass:
def my_method(self, param1, param2=None):
if param2 is None:
print(param1)
else:
print(param1 + param2)
In this example, the `my_method` function can be called with either one or two parameters. If
only one parameter is provided, the method prints the value of `param1`. If two parameters are
provided, the method adds them together and prints the result.
2. Method Overloading using Variable-Length Argument Lists:
You can also achieve method overloading using variable-length argument lists in Python. For
example:
python
class Sample:
def add(self, *args):
sum = 0
for arg in args:
sum += arg
print(sum)
In this example, the `add` method accepts a variable number of arguments using the `*args`
syntax. This allows the method to be called with different numbers of arguments and the
method processes the arguments accordingly.
In both of the examples above, Python achieves a form of method overloading by allowing a
single method to handle different numbers of parameters using default parameter values or
variable-length argument lists, thus demonstrating polymorphism within the context of method
overloading.
ii. Polymorphic Behavior in Classes
Polymorphic behavior in classes refers to the ability for objects of different classes to respond
to the same method in different ways. This is a key aspect of polymorphism in object-oriented
programming, as it allows for code reuse and flexibility in designing and implementing classes.
In Python, polymorphic behavior can be achieved through inheritance and method overriding.
Inheritance allows a child class to inherit attributes and methods from a parent class, while
method overriding allows the child class to redefine a method inherited from the parent class.
Here's an example of polymorphic behavior using inheritance and method overriding:
class Animal:
def speak(self):
print("I am an animal.")
class Dog(Animal):
def speak(self):
print("I am a dog.")
class Cat(Animal):
def speak(self):
print("I am a cat.")
dog = Dog()
dog.speak() # Output: I am a dog.
cat = Cat()
cat.speak() # Output: I am a cat.
In this example, we have an Animal class with a speak() method that prints "I am an animal."
The Dog and Cat classes inherit from the Animal class and also have a speak() method, but
they override the method to print "I am a dog." and "I am a cat." respectively.
Now, if we have a list of different animals, we can call the speak() method on each of them and
they will respond differently based on their specific implementation of the method:
animals = [Animal(), Dog(), Cat()]
for animal in animals:
animal.speak()
# Output:
# I am an animal.
# I am a dog.
# I am a cat.
This is an example of polymorphic behavior, where objects of different classes are able to
respond to the same method in different ways.
In conclusion, polymorphic behavior in classes is a key aspect of polymorphism in object-
oriented programming in Python. It allows for code reuse and flexibility in designing and
implementing classes by using inheritance and method overriding to achieve different
behaviors for objects of the same class.
Polymorphic behavior in object-oriented programming refers to the ability of different classes
to be treated as instances of a common superclass, and for their methods to be called using a
common interface. This allows for flexibility and reusability of code, as well as enabling the
implementation of methods specific to each subclass while maintaining a common interface.
class Dog(Animal):
def make_sound(self):
print("Woof!")
class Cat(Animal):
def make_sound(self):
print("Meow!")
class Shape(ABC):
@abstractmethod
def calculate_area(self):
pass
class Rectangle(Shape):
def _init_(self, length, width):
self.length = length
self.width = width
def calculate_area(self):
return self.length * self.width
class Circle(Shape):
def _init_(self, radius):
self.radius = radius
def calculate_area(self):
return 3.14 * (self.radius ** 2)
# Cannot instantiate an abstract class
shape = Shape() # Output: TypeError: Can't instantiate abstract class Shape with abstract
methods calculate_area
print(rectangle.calculate_area()) # Output: 50
print(circle.calculate_area()) # Output: 28.26
In this example, we have an abstract class Shape with an abstract method calculate_area(). This
method is meant to be implemented by its child classes, Rectangle and Circle. These child
classes provide their own implementation of the calculate_area() method based on their specific
attributes.
By using an abstract class, we are able to hide the implementation details of the calculate_area()
method and only expose the necessary information to the user. This allows for a simpler and
more intuitive interface for interacting with objects.
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius * self.radius
class Rectangle(Shape):
def __init__(self, length, width):
self.length = length
self.width = width
def area(self):
return self.length * self.width
# Create instances of Circle and Rectangle
circle = Circle(5)
rectangle = Rectangle(4, 6)
# Call the area method for each instance
print(circle.area()) # Output: 78.5
print(rectangle.area()) # Output: 24
In the above example, the `Shape` class serves as an abstract base class with an abstract method
called `area`. The `Circle` and `Rectangle` classes inherit from the `Shape` class and provide
their own implementation of the `area` method. This allows the user to interact with different
types of shapes without needing to understand the specific details of how the `area` method is
implemented for each shape, demonstrating the concept of abstraction in Python's object-
oriented programming.
i. Abstract Classes and Methods
Abstract classes and methods are key components of abstraction in object-oriented
programming in Python. An abstract class is a class that cannot be instantiated and must be
inherited by a child class. It serves as a template for creating specific types of objects, but it
does not provide the actual implementation.
On the other hand, an abstract method is a method without an implementation in an abstract
class. It is meant to be implemented by its child classes, providing different implementations
based on their specific attributes.
In the example above, the Shape class is an abstract class with the calculate_area() method as
an abstract method. This method is implemented differently in the Rectangle and Circle classes,
which inherit from the Shape class.
One of the main benefits of using abstract classes and methods is that they allow for code
reusability. By defining common methods and attributes in an abstract class, we can avoid
writing repetitive code in each child class. This also makes it easier to make changes or add
new functionality in the future.
Another benefit is that it promotes encapsulation, which is another important principle in
object-oriented programming. By hiding the implementation details of a class, we are able to
protect the data and prevent it from being modified directly by external code.
In summary, abstract classes and methods are essential components of abstraction in object-
oriented programming in Python. They allow for code reusability, promote encapsulation, and
provide a simpler and more intuitive interface for interacting with objects.
In Python, abstract classes and methods are used to implement abstraction in object-oriented
programming. Abstract classes are classes that cannot be instantiated and serve as a blueprint
for other classes to inherit from. Abstract methods are methods within abstract classes that have
no implementation and must be overridden by concrete subclasses.
Python provides the `abc` module, which contains the `ABC` class and `abstractmethod`
decorator that can be used to create abstract classes and methods.
Below is an example of using abstract classes and methods in Python to implement abstraction:
python
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius * self.radius
class Rectangle(Shape):
def __init__(self, length, width):
self.length = length
self.width = width
def area(self):
return self.length * self.width
In the context of frameworks, abstract classes and methods are used to define common
interfaces and behaviors that can be shared by different components of the framework. This
promotes code reusability and helps to keep the codebase organized and maintainable.
Additionally, abstract classes and methods provide a level of abstraction that makes it easier
for developers to use the framework without having to understand its internal workings. This
simplifies the development process and allows for faster and more efficient development.
Furthermore, abstract classes and methods also help to ensure consistency and standardization
within the framework. By defining a set of abstract classes and methods, the framework
establishes a clear structure and guidelines for developers to follow. This helps to maintain a
unified codebase and makes it easier for new developers to understand and contribute to the
framework.
In conclusion, abstract classes and methods are essential in framework design in abstraction in
object-oriented programming in Python. They promote code reusability, simplify development,
ensure consistency, and provide a clear structure for the framework.
Abstract classes and methods play a crucial role in framework design when implementing
abstraction in object-oriented programming in Python. Here are some key points highlighting
their importance:
@classmethod
def increment_count(cls):
cls.count += 1
MyClass.increment_count()
print(MyClass.count) # Output: 1
2. Static Methods:
- A static method is a method that does not receive an implicit first argument and does not
operate on class or instance data.
- It is defined using the `@staticmethod` decorator before the method definition.
- Unlike instance methods and class methods, static methods do not have access to the instance
or the class. They behave like regular functions but are defined within a class for organizational
purposes.
- Static methods are often used for utility functions that logically belong to the class but do not
require access to instance or class variables.
Example of a static method in Python:
python
class MathOperations:
@staticmethod
def add(x, y):
return x + y
@property
def radius(self):
return self._radius
c = Circle(5)
print(c.radius) # Output: 5
2. @<attribute_name>.setter:
The `@<attribute_name>.setter` decorator is used to define a setter method that allows
modification of the attribute.
Example:
python
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
self._radius = value
c = Circle(5)
c.radius = 7
print(c.radius) # Output: 7
3. @<attribute_name>.deleter:
The `@<attribute_name>.deleter` decorator is used to define a deleter method that allows the
deletion of the attribute.
Example:
python
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
@radius.deleter
def radius(self):
del self._radius
c = Circle(5)
del c.radius
# Attribute 'radius' is now deleted
In summary, property decorators in Python provide a way to define properties with getter,
setter, and deleter methods within a class, allowing for controlled access to and manipulation
of attributes. This can be particularly useful for implementing data encapsulation and
maintaining control over class attributes in object-oriented programming.
In Python, property decorators are a way to define getters, setters, and deleters for a class
property. They are used to create managed attributes, allowing for controlled access and
manipulation of class attributes.
The property decorator is used to define properties in a class. It allows for defining a method
that will be called when accessing, setting, or deleting the property. This provides a way to
encapsulate data and ensure that it is accessed and mutated in a controlled manner.
Here's a simple example of how property decorators can be used in Python:
python
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value
@radius.deleter
def radius(self):
del self._radius
In the above example, the `@property` decorator is used to define the `radius` property, and
the `@radius.setter` and `@radius.deleter` decorators are used to define the setter and deleter
methods for the `radius` property.
Property decorators are useful for creating managed attributes with custom validation,
calculations, or side effects when accessing, setting, or deleting the property. They allow for
clean and readable code, and provide a way to encapsulate attribute access within a class.
c) Composition vs. Inheritance
In object-oriented programming, both composition and inheritance are important concepts for
building relationships between classes. Let's take a look at how they differ and their use in
Python.
1. Inheritance:
Inheritance is a mechanism where a class inherits properties and methods from another class,
known as the parent or base class. The class that inherits the properties is called the child or
derived class. In Python, inheritance is achieved using the syntax:
python
class ParentClass:
# properties and methods
class ChildClass(ParentClass):
# properties and methods
The child class can access and modify the properties and methods of the parent class.
Inheritance promotes code reusability and is often used when a class "is a" type of relationship
with another class.
2. Composition:
Composition is a design principle that describes a relationship between classes where one class
contains an instance of another class. Unlike inheritance, where the child class becomes a type
of the parent class, composition allows for a "has a" relationship between classes. In Python,
composition is implemented by creating an instance of another class within a class.
python
class ClassA:
# properties and methods
class ClassB:
def __init__(self):
self.class_a_instance = ClassA()
Composition promotes flexibility and reusability by allowing for more loosely coupled classes.
It also enables changes in the behavior of the composed class without affecting the client class.
Comparison:
- Inheritance creates an "is-a" relationship, whereas composition creates a "has-a" relationship.
- Inheritance can lead to a tightly coupled code, while composition often results in more flexible
and modular code.
- Inheritance can be useful when modeling a clear hierarchy of related classes, while
composition is useful when building classes with interchangeable components or parts.
In Python, both inheritance and composition are commonly used to build complex software
systems. The choice between them depends on the specific requirements of the system and the
relationships between the classes. It's also important to note that Python supports multiple
inheritance but favors composition over complex inheritance hierarchies.
d) Magic/Dunder Methods (__init__, __str__, etc.)
In Python, magic or dunder (double underscore) methods are special methods that have double
underscores at the beginning and end of their names. These methods are used to perform
various operations and are automatically invoked by the Python interpreter in specific
circumstances. Let's take a look at some commonly used magic methods in Python's object-
oriented programming.
1. `_init_`:
The `_init_` method is used to initialize an object's state. It is called automatically when an
object is created and is used to initialize the object's attributes.
python
class MyClass:
def __init__(self, param1, param2):
self.param1 = param1
self.param2 = param2
2. `_str_`:
The `_str_` method is used to return a string representation of an object. It is called when the
`str()` function is used or when an object is printed.
python
class MyClass:
def __init__(self, param1, param2):
self.param1 = param1
self.param2 = param2
def __str__(self):
return f"MyClass: param1={self.param1}, param2={self.param2}"
3. `_repr_`:
The `_repr_` method is used to return a string representation of the object for debugging
purposes. It is called when the `repr()` function is used or when an object is printed in the
interpreter.
python
class MyClass:
def __init__(self, param1, param2):
self.param1 = param1
self.param2 = param2
def __repr__(self):
return f"MyClass(param1={self.param1}, param2={self.param2})"
4. `_len_`:
The `_len_` method is used to return the length of an object. It is called when the `len()`
function is used.
python
class MyList:
def __init__(self, data):
self.data = data
def __len__(self):
return len(self.data)
These are just a few examples of the many magic methods available in Python. Magic methods
allow for custom behavior to be defined for objects in Python, making them an essential part
of object-oriented programming in the language.
e) Design Patterns in Python
Design patterns are reusable solutions to commonly occurring problems in software design.
They provide a way to create more maintainable and scalable code by following best practices
and proven solutions. In Python, design patterns can be implemented using object-oriented
programming (OOP) concepts such as classes, objects, inheritance, and polymorphism. Here
are some commonly used design patterns in Python OOP:
1. Singleton Pattern:
The Singleton pattern ensures that a class has only one instance and provides a global point of
access to that instance. In Python, this can be achieved using a class variable to store the
instance and a static method to access it.
python
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
2. Factory Pattern:
The Factory pattern provides an interface for creating objects without specifying their concrete
classes. In Python, this can be implemented using a factory method that creates and returns
objects based on certain criteria.
python
class Vehicle:
def drive(self):
pass
class Car(Vehicle):
def drive(self):
print("Driving a car")
class Bike(Vehicle):
def drive(self):
print("Riding a bike")
class VehicleFactory:
def create_vehicle(self, vehicle_type):
if vehicle_type == "car":
return Car()
elif vehicle_type == "bike":
return Bike()
3. Observer Pattern:
The Observer pattern defines a one-to-many dependency between objects so that when one
object changes state, all its dependents are notified and updated automatically. In Python, this
can be achieved using the built-in `property` function and custom setter and getter methods.
python
class Subject:
def __init__(self):
self._observers = []
class Observer:
def update(self, value):
pass
These are just a few examples of design patterns in Python's object-oriented programming.
Understanding and implementing design patterns can lead to more organized and maintainable
code in Python applications. However, it's important to carefully consider the context and
requirements of a particular problem before applying a design pattern.
4. OOP in Data Science PRESENTATION
a) Designing Classes for Data Handling (e.g., DataFrames, Datasets)
Designing classes for data handling in object-oriented programming in Python involves
creating custom classes to represent and manipulate data structures such as DataFrames,
Datasets, or any other custom data types. This approach allows you to encapsulate data and
related operations into a single unit, making it easier to manage and manipulate the data.
Here's an example of designing a custom class for handling data using Python's pandas library
to work with DataFrames:
python
import pandas as pd
class CustomDataFrame:
def __init__(self, data, columns=None):
if columns is not None:
self.data = pd.DataFrame(data, columns=columns)
else:
self.data = pd.DataFrame(data)
def info(self):
return self.data.info()
def describe(self):
return self.data.describe()
class DataPreprocessor:
def __init__(self, data):
self.data = data
def preprocess_data(self):
# Add code to preprocess the data (e.g., cleaning, transforming)
pass
class FeatureExtractor:
def __init__(self, preprocessed_data):
self.preprocessed_data = preprocessed_data
def extract_features(self):
# Add code to extract features from the preprocessed data
pass
class ModelTrainer:
def __init__(self, features, labels):
self.features = features
self.labels = labels
def train_model(self):
# Add code to train a machine learning model using the features and labels
pass
class DataPipeline:
def __init__(self, file_path):
self.file_path = file_path
self.pipeline = []
def run_pipeline(self):
data = None
for stage in self.pipeline:
if data is None:
stage_input = self.file_path
else:
stage_input = data
data = stage(stage_input)
return data
# Example usage of the custom data processing pipeline
data_pipeline = DataPipeline('data.csv')
data_pipeline.add_stage(DataReader)
data_pipeline.add_stage(DataPreprocessor)
data_pipeline.add_stage(FeatureExtractor)
data_pipeline.add_stage(ModelTrainer)
processed_data = data_pipeline.run_pipeline()
In this example, we have created different classes for each stage of the data processing pipeline,
including `DataReader`, `DataPreprocessor`, `FeatureExtractor`, and `ModelTrainer`. We have
then encapsulated these stages into a `DataPipeline` class, where you can dynamically add and
run different stages of the pipeline.
Using this approach, you can easily extend or modify the data processing pipeline by creating
new stages or modifying existing ones. This allows for flexible, reusable, and organized code
for data processing tasks in Python using OOP.
c) Implementing OOP with Popular Data Science Libraries (pandas, scikit-learn,
etc.)
Implementing OOP with popular data science libraries such as pandas and scikit-learn in
Python involves creating custom classes and methods that encapsulate the functionalities
offered by these libraries. This approach allows you to build modular, reusable, and
maintainable code for data science tasks.
Here's an example of implementing OOP with pandas and scikit-learn in Python:
python
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
class DataProcessor:
def __init__(self, file_path):
self.file_path = file_path
def read_data(self):
# Add code to read data using pandas
return pd.read_csv(self.file_path)
class ModelBuilder:
def __init__(self, data):
self.data = data
def build_model(self):
features = self.data.drop('target', axis=1)
labels = self.data['target']
X_train, X_test, y_train, y_test = train_test_split(features, labels, test_size=0.2,
random_state=42)
y_pred = model.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
model_builder = ModelBuilder(preprocessed_data)
model, accuracy = model_builder.build_model()
print("Accuracy:", accuracy)
In this example, we have created two custom classes `DataProcessor` and `ModelBuilder` that
encapsulate the functionalities of reading data with pandas, preprocessing the data, and
building a machine learning model with scikit-learn. By using OOP, we can create modular
and reusable code to handle data processing and model building tasks in a structured manner.
This approach allows you to extend or modify the functionality of your data processing and
model building tasks by creating new methods or modifying existing ones. It also promotes
code organization and maintainability for data science tasks in Python using popular libraries
such as pandas and scikit-learn.
5. Case Studies PRESENTATION
a) Building a Data Pre-processing Framework using OOP
Building a data pre-processing framework using Object-Oriented Programming (OOP) in
Python allows for creating a modular and flexible system for handling various data pre-
processing tasks. Here's an example of how to build a simple data pre-processing framework
using OOP in Python:
python
import pandas as pd
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
class DataPreprocessor:
def __init__(self, data):
self.data = data
def scale_numeric_features(self):
# Scale numeric features using StandardScaler from scikit-learn
scaler = StandardScaler()
numeric_data = self.data.select_dtypes(include=['number'])
self.data.loc[:, numeric_data.columns] = scaler.fit_transform(numeric_data)
def encode_categorical_features(self):
# Encode categorical features using OneHotEncoder from scikit-learn
categorical_data = self.data.select_dtypes(include=['object'])
encoder = OneHotEncoder()
encoded_data = encoder.fit_transform(categorical_data).toarray()
self.data = pd.concat([self.data, pd.DataFrame(encoded_data,
columns=encoder.get_feature_names(categorical_data.columns))], axis=1)
def get_preprocessed_data(self):
return self.data
preprocessor = DataPreprocessor(data)
preprocessor.handle_missing_values(strategy='mean')
preprocessor.scale_numeric_features()
preprocessor.encode_categorical_features()
preprocessed_data = preprocessor.get_preprocessed_data()
print(preprocessed_data)
class CustomLinearRegressionModel:
def __init__(self):
self.model = LinearRegression()
model = CustomLinearRegressionModel()
model.train(X_train, y_train)
y_pred = model.predict(X_test)
mse = model.evaluate(X_test, y_test)
print(f"Mean Squared Error: {mse}")
visualizer.plot_line_chart('x', 'y')
visualizer.plot_bar_chart('x', 'y')
visualizer.plot_histogram('y')
In this example, we've created a `DataVisualizer` class that encapsulates various data
visualization methods using the Matplotlib library. The class contains methods for plotting line
charts, bar charts, and histograms, each taking the necessary data and labels as input.
By using OOP principles, we can create a reusable and modular toolkit for data visualization.
This allows us to easily extend and modify the toolkit by adding new visualization methods,
handling different types of input data, and customizing the visualization parameters.
Furthermore, encapsulating the visualization functionalities within a class promotes code
organization, reusability, and maintainability, making it easier to collaborate and share the
toolkit across different projects and teams.
6. Best Practices and Advanced Topics PRESENTATION
a) SOLID Principles in OOP for Python
The SOLID principles are a set of five object-oriented programming (OOP) design principles
that help developers create more maintainable, flexible, and scalable software. Here's how
these principles can be applied to Python:
1. Single Responsibility Principle (SRP):
This principle states that a class should have only one reason to change. In Python, you can
apply SRP by ensuring that each class and method has a single responsibility and does not have
too many dependencies.
Example:
python
class DataManager:
def read_data(self, file_path):
# method to read data from a file
pass
def process_data(self, data):
# method to process the data
pass
class Sparrow(Bird):
def fly(self):
# implementation for flying
pass
class CanFly(ABC):
@abstractmethod
def fly(self):
pass
class CanSwim(ABC):
@abstractmethod
def swim(self):
pass
5. Dependency Inversion Principle (DIP):
DIP advocates that high-level modules should not depend on low-level modules but should
depend on abstractions. In Python, you can use dependency injection to achieve this, allowing
you to inject dependencies as parameters.
Example:
python
class Logger:
def log(self, message):
pass
class DataManager:
def __init__(self, logger):
self.logger = logger
Example:
python
class Calculator:
def divide(self, x, y):
if y == 0:
raise ZeroDivisionError("Cannot divide by zero")
return x / y
calculator = Calculator()
try:
result = calculator.divide(10, 0)
except ZeroDivisionError as e:
print("Error:", e)
4. Using the `finally` Block:
You can use the `finally` block to ensure that certain code is always executed, regardless of
whether an exception is raised.
Example:
python
try:
# Code that may raise an exception
file = open("example.txt", "r")
content = file.read()
except FileNotFoundError:
print("Error: File not found")
finally:
if 'file' in locals():
file.close()
By employing these error and exception handling techniques in your Python OOP code, you
can efficiently manage unexpected errors and maintain the robustness of your applications.
This approach promotes better program reliability and allows for smoother user experiences.
c) Writing Efficient and Maintainable OOP Code in Python
In Python, error and exception handling plays a crucial role in ensuring that your object-
oriented programs can gracefully handle unexpected situations and errors. Here's how you can
integrate error and exception handling into your OOP code:
1. Using Try-Except Blocks:
In Python, you can use the try-except blocks to catch and handle exceptions. This is useful
when you anticipate that a particular piece of code might raise an exception.
Example:
python
try:
# Code that may raise an exception
result = 10 / 0
except ZeroDivisionError:
# Handle the division by zero exception
print("Error: Division by zero")
Example:
python
class Calculator:
def divide(self, x, y):
if y == 0:
raise ZeroDivisionError("Cannot divide by zero")
return x / y
calculator = Calculator()
try:
result = calculator.divide(10, 0)
except ZeroDivisionError as e:
print("Error:", e)
4. Using the `finally` Block:
You can use the `finally` block to ensure that certain code is always executed, regardless of
whether an exception is raised.
Example:
python
try:
# Code that may raise an exception
file = open("example.txt", "r")
content = file.read()
except FileNotFoundError:
print("Error: File not found")
finally:
if 'file' in locals():
file.close()
By employing these error and exception handling techniques in your Python OOP code, you
can efficiently manage unexpected errors and maintain the robustness of your applications.
This approach promotes better program reliability and allows for smoother user experiences.
7: Practical Project
Students will be tasked with creating a data science project utilizing their
understanding of OOP. This could involve processing a dataset, implementing
machine learning, and visualizing results, all structured using OOP principles.