0% found this document useful (0 votes)
9 views10 pages

Object Oriented+Programming+(OOP)+in+Python

Uploaded by

saumyojyotid
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
9 views10 pages

Object Oriented+Programming+(OOP)+in+Python

Uploaded by

saumyojyotid
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 10

Object-Oriented Programming (OOP) in

Python
We’ll now explore how to organize and structure your Python code using objects,
making it more manageable, reusable, and easier to understand.

1. What is OOP Anyway?

Imagine you’re building with LEGOs. Instead of just having a pile of individual
bricks (like in procedural programming), OOP lets you create pre-assembled units –
like a car, a house, or a robot. These units have specific parts (data) and things they
can do (actions).

That’s what OOP is all about. It’s a way of programming that focuses on creating
“objects.” An object is like a self-contained unit that bundles together:

• Data (Attributes): Information about the object. For a car, this might be its
color, model, and speed.
• Actions (Methods): Things the object can do. A car can accelerate, brake, and
turn.

Why Bother with OOP?

OOP offers several advantages:

• Organization: Your code becomes more structured and easier to navigate.


Large projects become much more manageable.
• Reusability: You can use the same object “blueprints” (classes) multiple times,
saving you from writing the same code over and over.
• Easier Debugging: When something goes wrong, it’s often easier to pinpoint
the problem within a specific, self-contained object.
• Real-World Modeling: OOP allows you to represent real-world things and
their relationships in a natural way.
The Four Pillars of OOP

OOP is built on four fundamental principles:

1. Abstraction: Think of driving a car. You use the steering wheel, pedals, and
gearshift, but you don’t need to know the complex engineering under the
hood. Abstraction means hiding complex details and showing only the
essential information to the user.
2. Encapsulation: This is like putting all the car’s engine parts inside a protective
casing. Encapsulation bundles data (attributes) and the methods that operate
on that data within a class. This protects the data from being accidentally
changed or misused from outside the object. It controls access.
3. Inheritance: Imagine creating a “SportsCar” class. Instead of starting from
scratch, you can build it upon an existing “Car” class. The “SportsCar” inherits
all the features of a “Car” (like wheels and an engine) and adds its own special
features (like a spoiler). This promotes code reuse and reduces redundancy.
4. Polymorphism: “Poly” means many, and “morph” means forms. This means
objects of different classes can respond to the same “message” (method call)
in their own specific way. For example, both a “Dog” and a “Cat” might have a
make_sound() method. The dog will bark, and the cat will meow – same
method name, different behavior.

2. Classes and Objects: The Blueprint and the Building

• Class: Think of a class as a blueprint or a template. It defines what an object


will be like – what data it will hold and what actions it can perform. It doesn’t
create the object itself, just the instructions for creating it. It’s like an
architectural plan for a house.

• Object (Instance): An object is a specific instance created from the class


blueprint. If “Car” is the class, then your red Honda Civic is an object (an
instance) of the “Car” class. Each object has its own unique set of data. It’s like
the actual house built from the architectural plan.

Let’s see this in Python:

class Dog: # We define a class called "Dog"


species = "Canis familiaris" # A class attribute (shared by all Dogs)
def __init__(self, name, breed): # The constructor (explained later)
self.name = name # An instance attribute to store the dog's name
self.breed = breed # An instance attribute to store the dog's breed

def bark(self): # A method (an action the dog can do)


print(f"{self.name} says Woof!")

# Now, let's create some Dog objects:


my_dog = Dog("Buddy", "Golden Retriever") # Creating an object called my_dog
another_dog = Dog("Lucy", "Labrador") # Creating another object

# We can access their attributes:


print(my_dog.name) # Output: Buddy
print(another_dog.breed) # Output: Labrador

# And make them perform actions:


my_dog.bark() # Output: Buddy says Woof!
print(Dog.species) # Output: Canis familiaris

• self Explained: Inside a class, self is like saying “this particular object.” It’s
a way for the object to refer to itself. It’s always the first parameter in a
method definition, but Python handles it automatically when you call the
method. You don’t type self when calling the method; Python inserts it for
you.

• Class vs. Instance Attributes:

• Class Attributes: These are shared by all objects of the class. Like
species in our Dog class. All dogs belong to the same species. They are
defined outside of any method, directly within the class.
• Instance Attributes: These are specific to each individual object. name
and breed are instance attributes. Each dog has its own name and breed.
They are usually defined within the __init__ method.
3. The Constructor: Setting Things Up ( __init__ )

The __init__ method is special. It’s called the constructor. It’s automatically run
whenever you create a new object from a class.

What’s it for? The constructor’s job is to initialize the object’s attributes – to give
them their starting values. It sets up the initial state of the object.

class Dog:
def __init__(self, name, breed): # The constructor
self.name = name # Setting the name attribute
self.breed = breed # Setting the breed attribute

# When we do this:
my_dog = Dog("Fido", "Poodle") # The __init__ method is automatically called

# It's like we're saying:


# 1. Create a new Dog object.
# 2. Run the __init__ method on this new object:
# - Set my_dog.name to "Fido"
# - Set my_dog.breed to "Poodle"

You can also set default values for parameters in the constructor, making them
optional when creating an object:

class Dog:
def __init__(self, name="Unknown", breed="Mixed"):
self.name = name
self.breed = breed

dog1 = Dog() # name will be "Unknown", breed will be "Mixed"


dog2 = Dog("Rex") # name will be "Rex", breed will be "Mixed"
dog3 = Dog("Bella", "Labrador") # name will be "Bella", breed will be "Labrad
4. Inheritance: Building Upon Existing Classes

Inheritance is like a family tree. A child class (or subclass) inherits traits (attributes
and methods) from its parent class (or superclass). This allows you to create new
classes that are specialized versions of existing classes, without rewriting all the
code.

class Animal: # Parent class (superclass)


def __init__(self, name):
self.name = name

def speak(self):
print("Generic animal sound")

class Dog(Animal): # Dog inherits from Animal (Dog is a subclass of Animal)


def speak(self): # We *override* the speak method (more on this later)
print("Woof!")

class Cat(Animal): # Cat also inherits from Animal


def speak(self):
print("Meow!")

# Create objects:
my_dog = Dog("Rover")
my_cat = Cat("Fluffy")

# They both have a 'name' attribute (inherited from Animal):


print(my_dog.name) # Output: Rover
print(my_cat.name) # Output: Fluffy

# They both have a 'speak' method, but it behaves differently:


my_dog.speak() # Output: Woof!
my_cat.speak() # Output: Meow!

• super() : Inside a child class, super() lets you call methods from the parent
class. This is useful when you want to extend the parent’s behavior instead of
completely replacing it. It’s especially important when initializing the parent
class’s part of a child object.
# Calling Parent Constructor with super()
class Bird(Animal):
def __init__(self, name, wingspan):
super().__init__(name) # Call Animal's __init__ to set the name
self.wingspan = wingspan # Add a Bird-specific attribute

my_bird = Bird("Tweety", 10)


print(my_bird.name) # Output: Tweety (set by Animal's constructor)
print(my_bird.wingspan) # Output: 10 (set by Bird's constructor)

5. Polymorphism: One Name, Many Forms

Polymorphism, as we saw with the speak() method in the inheritance example,


means that objects of different classes can respond to the same method call in
their own specific way. This allows you to write code that can work with objects of
different types without needing to know their exact class.

6. Method Overriding: Customizing Inherited


Behavior

Method overriding is how polymorphism is achieved in inheritance. When a child


class defines a method with the same name as a method in its parent class, the
child’s version overrides the parent’s version for objects of the child class. This allows
specialized behavior in subclasses. The parent class’s method is still available (using
super() ), but when you call the method on a child class object, the child’s version
is executed.

7. Operator Overloading: Making Operators Work


with Your Objects

Python lets you define how standard operators (like + , - , == ) behave when
used with objects of your own classes. This is done using special methods called
“magic methods” (or “dunder methods” because they have double underscores
before and after the name).
class Point:
def __init__(self, x, y):
self.x = x
self.y = y

def __add__(self, other): # Overloading the + operator


# 'other' refers to the object on the *right* side of the +
return Point(self.x + other.x, self.y + other.y)

def __str__(self): # String representation (for print() and str())


return f"({self.x}, {self.y})"

def __eq__(self, other): # Overloading == operator


return self.x == other.x and self.y == other.y

p1 = Point(1, 2)
p2 = Point(3, 4)

p3 = p1 + p2 # This now works! It calls p1.__add__(p2)


print(p3) # Output: (4, 6) (This uses the __str__ method)

print(p1 == p2) # Output: False (This uses the __eq__ method)

Other useful magic methods: (You don’t need to memorize them all, but be aware
they exist!)

• __sub__ ( - ), __mul__ ( * ), __truediv__ ( / ), __eq__ ( == ), __ne__


( != ), __lt__ ( < ), __gt__ ( > ), __len__ ( len() ), __getitem__ ,
__setitem__ , __delitem__ (for list/dictionary-like behavior – allowing you
to use [] with your objects).

8. Getters and Setters: Controlling Access to


Attributes

Getters and setters are methods that you create to control how attributes of your
class are accessed and modified. They are a key part of the principle of
encapsulation. Instead of directly accessing an attribute (like
my_object.attribute ), you use methods to get and set its value. This might seem
like extra work, but it provides significant advantages.

Why use them?

• Validation: You can add checks within the setter to make sure the attribute is
set to a valid value. For example, you could prevent an age from being
negative.
• Read-Only Attributes: You can create a getter without a setter, making the
attribute effectively read-only from outside the class. This protects the
attribute from being changed accidentally.
• Side Effects: You can perform other actions when an attribute is accessed or
modified. For instance, you could update a display or log a change whenever a
value is set.
• Maintainability and Flexibility: If you decide to change how an attribute is
stored internally (maybe you switch from storing degrees Celsius to
Fahrenheit), you only need to update the getter and setter methods. You don’t
need to change every other part of your code that uses the attribute. This
makes your code much easier to maintain and modify in the future.

class Person:
def __init__(self, name, age):
self.name = name
self._age = age # Convention: _age indicates it's intended to be "pr

def get_age(self): # Getter for age


return self._age

def set_age(self, new_age): # Setter for age


if new_age >= 0 and new_age <= 150: # Validation
self._age = new_age
else:
print("Invalid age!")

person = Person("Alice", 30)


print(person.get_age()) # Output: 30

person.set_age(35)
print(person.get_age()) # Output: 35
person.set_age(-5) # Output: Invalid age!
print(person.get_age()) # Output: 35 (age wasn't changed)

The Pythonic Way: @property Decorator

Python offers a more elegant and concise way to define getters and setters using
the @property decorator. This is the preferred way to implement them in modern
Python.

class Person:
def __init__(self, name, age):
self.name = name
self._age = age # Convention: _age for "private" attributes

@property # This makes 'age' a property (the getter)


def age(self):
return self._age

@age.setter # This defines the setter for the 'age' property


def age(self, new_age):
if new_age >= 0 and new_age <= 150:
self._age = new_age
else:
print("Invalid age!")

person = Person("Bob", 40)


print(person.age) # Output: 40 (Looks like direct attribute access, but c
person.age = 45 # (Calls the setter – looks like attribute assignment)
print(person.age)
person.age = -22 #Output: Invalid age!

With @property , accessing and setting the age attribute looks like you’re
working directly with a regular attribute, but you’re actually using the getter and
setter methods behind the scenes. This combines the convenience of direct access
with the control and protection of encapsulation.

Private Variables (and the _ convention):


It’s important to understand that Python does not have truly private attributes in
the same way that languages like Java or C++ do. There’s no keyword that
completely prevents access to an attribute from outside the class.

Instead, Python uses a convention: An attribute name starting with a single


underscore ( _ ) signals to other programmers that this attribute is intended for
internal use within the class. It’s a strong suggestion: “Don’t access this directly
from outside the class; use the provided getters and setters instead.” It’s like a
“Please Do Not Touch” sign.

class MyClass:
def __init__(self):
self._internal_value = 0 # Convention: _ means "private"

def get_value(self):
return self._internal_value

obj = MyClass()
# print(obj._internal_value) # This *works*, but it's against convention
print(obj.get_value()) # This is the preferred way

While you can still access obj._internal_value directly, doing so is considered


bad practice and can lead to problems if the internal implementation of the class
changes. Always respect the underscore convention! It’s about good coding style
and collaboration.

You might also like