Table of Contents
- What is Object-Oriented Programming (OOP)?
- Classes in Python: The Blueprint
- Objects: Instantiating Classes
- Attributes: Data in Objects
- 4.1 Instance Attributes
- 4.2 Class Attributes
- Methods: Behavior of Objects
- 5.1 Instance Methods
- 5.2 Class Methods
- 5.3 Static Methods
- Constructors: The
__init__Method - Encapsulation: Protecting Data
- Inheritance: Reusing and Extending Classes
- Polymorphism: One Interface, Many Implementations
- Advanced OOP Concepts
- Common Pitfalls and Best Practices
- Conclusion
- References
What is Object-Oriented Programming (OOP)?
OOP organizes code into classes (blueprints) and objects (instances of classes). It relies on four core principles:
- Encapsulation: Bundling data and methods into a class, with controlled access to data.
- Inheritance: Creating new classes (subclasses) from existing ones (superclasses) to reuse code.
- Polymorphism: Using a single interface to represent different types of objects (e.g., different classes with the same method name).
- Abstraction: Hiding complex implementation details and exposing only essential features.
Classes in Python: The Blueprint
A class is a blueprint for creating objects. It defines the attributes (data) and methods (functions) that all objects of that class will have.
Syntax of a Class
In Python, a class is defined using the class keyword, followed by the class name (by convention, in CamelCase), and a colon.
class Car:
# Class body (attributes and methods go here)
pass # Placeholder for empty class
Objects: Instantiating Classes
An object (or instance) is a concrete realization of a class. To create an object, call the class name like a function.
Example: Creating Objects
class Car:
pass
# Create objects (instances) of Car
car1 = Car()
car2 = Car()
print(type(car1)) # Output: <class '__main__.Car'>
print(car1 is car2) # Output: False (different objects)
Each object is unique, even if created from the same class.
Attributes: Data in Objects
Attributes store data associated with a class or its instances. There are two types: instance attributes and class attributes.
Instance Attributes
Instance attributes are unique to each object. They are defined inside methods (typically the constructor) using self.
Example: Instance Attributes
class Car:
def set_attributes(self, color, model):
self.color = color # Instance attribute
self.model = model # Instance attribute
# Create objects and set attributes
car1 = Car()
car1.set_attributes("red", "Tesla Model 3")
car2 = Car()
car2.set_attributes("blue", "Ford Mustang")
print(car1.color) # Output: red
print(car2.model) # Output: Ford Mustang
Class Attributes
Class attributes are shared by all instances of a class. They are defined directly in the class body (outside methods).
Example: Class Attributes
class Car:
wheels = 4 # Class attribute (shared by all cars)
def set_attributes(self, color, model):
self.color = color
self.model = model
car1 = Car()
car1.set_attributes("red", "Tesla Model 3")
car2 = Car()
car2.set_attributes("blue", "Ford Mustang")
print(car1.wheels) # Output: 4 (inherited from class)
print(car2.wheels) # Output: 4
print(Car.wheels) # Output: 4 (access via class)
Note: Modifying a class attribute via the class affects all instances. Modifying it via an instance creates a new instance attribute that shadows the class attribute.
Methods: Behavior of Objects
Methods are functions defined inside a class that describe the behavior of objects. There are three types: instance methods, class methods, and static methods.
Instance Methods
Instance methods operate on an instance of the class and always take self as the first parameter (referring to the instance itself).
Example: Instance Method
class Car:
wheels = 4
def __init__(self, color, model): # Constructor (more on this soon!)
self.color = color
self.model = model
def start_engine(self): # Instance method
return f"{self.model} engine started. Vroom!"
car1 = Car("red", "Tesla Model 3")
print(car1.start_engine()) # Output: Tesla Model 3 engine started. Vroom!
Class Methods
Class methods operate on the class itself, not instances. They use the @classmethod decorator and take cls (referring to the class) as the first parameter.
Use Case: Factory Methods
Class methods are often used to create alternative constructors.
class Car:
wheels = 4
def __init__(self, color, model):
self.color = color
self.model = model
@classmethod
def from_string(cls, car_str): # Factory method
color, model = car_str.split(",")
return cls(color.strip(), model.strip())
# Create a Car using the factory method
car2 = Car.from_string("blue, Ford Mustang")
print(car2.model) # Output: Ford Mustang
Static Methods
Static methods are utility functions that don’t depend on the class or instance. They use the @staticmethod decorator and take no mandatory parameters (self or cls).
Example: Static Method
class Car:
@staticmethod
def is_valid_model(model): # Static method (utility)
return len(model) >= 2 and model[0].isupper()
print(Car.is_valid_model("mustang")) # Output: False (starts with lowercase)
print(Car.is_valid_model("Mustang")) # Output: True
Constructors: The __init__ Method
The __init__ method (short for “initialize”) is Python’s constructor. It runs automatically when an object is created, initializing instance attributes.
Syntax:
class ClassName:
def __init__(self, param1, param2, ...):
self.attribute1 = param1
self.attribute2 = param2
# ...
Example: Using __init__
class Car:
wheels = 4
def __init__(self, color, model, year):
self.color = color
self.model = model
self.year = year # New instance attribute
def get_info(self):
return f"{self.year} {self.color} {self.model}"
car = Car("silver", "Honda Civic", 2020)
print(car.get_info()) # Output: 2020 silver Honda Civic
Encapsulation: Protecting Data
Encapsulation restricts access to sensitive data (attributes/methods) to prevent accidental modification. Python uses naming conventions to enforce this:
- Public: No leading underscores (e.g.,
color). Accessible to everyone. - Protected: Single leading underscore (e.g.,
_mileage). Intended for internal use (subclasses can access). - Private: Double leading underscores (e.g.,
__balance). “Name-mangled” to prevent accidental access.
Example: Private Attributes
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.__balance = balance # Private attribute
def deposit(self, amount):
if amount > 0:
self.__balance += amount
return f"Deposited ${amount}. New balance: ${self.__balance}"
return "Invalid deposit amount."
def get_balance(self): # Public method to access private attribute
return f"Current balance: ${self.__balance}"
account = BankAccount("Alice", 1000)
print(account.deposit(500)) # Output: Deposited $500. New balance: $1500
print(account.get_balance()) # Output: Current balance: $1500
# Trying to access __balance directly raises an error:
print(account.__balance) # AttributeError: 'BankAccount' object has no attribute '__balance'
Name Mangling: Python renames __balance to _BankAccount__balance internally. You can access it via account._BankAccount__balance, but this is discouraged.
Inheritance: Reusing and Extending Classes
Inheritance lets you create a subclass that reuses (inherits) attributes/methods from a superclass (parent class), then extends or modifies them.
Syntax:
class SubclassName(SuperclassName):
# Subclass body
Example: Single Inheritance
class Car: # Superclass
wheels = 4
def __init__(self, color, model):
self.color = color
self.model = model
def start_engine(self):
return f"{self.model} engine started."
class SportsCar(Car): # Subclass of Car
def __init__(self, color, model, top_speed):
super().__init__(color, model) # Call superclass constructor
self.top_speed = top_speed # New attribute
def start_engine(self): # Override superclass method
return f"{self.model} (top speed {self.top_speed}mph) engine roared to life!"
ferrari = SportsCar("red", "Ferrari F8", 211)
print(ferrari.start_engine()) # Output: Ferrari F8 (top speed 211mph) engine roared to life!
print(ferrari.wheels) # Output: 4 (inherited from Car)
Key Terms:
super(): Calls methods from the superclass (avoids hardcoding the superclass name).- Method Overriding: Subclass redefines a method from the superclass (e.g.,
start_engineinSportsCar).
Polymorphism: One Interface, Many Implementations
Polymorphism (Greek for “many forms”) allows objects of different classes to be treated uniformly if they share a common interface (method names).
Example: Polymorphism with Method Overriding
class Shape:
def area(self):
raise NotImplementedError("Subclasses must implement area()")
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self): # Override area()
return 3.14 * self.radius **2
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self): # Override area()
return self.side** 2
# Polymorphic function: works with any Shape
def print_area(shape):
print(f"Area: {shape.area()}")
circle = Circle(5)
square = Square(4)
print_area(circle) # Output: Area: 78.5
print_area(square) # Output: Area: 16
Here, print_area accepts any Shape subclass and calls area(), demonstrating polymorphism.
Advanced OOP Concepts
Abstract Base Classes (ABCs)
An abstract base class (ABC) defines a blueprint for subclasses, forcing them to implement specific methods. Use the abc module.
from abc import ABC, abstractmethod
class Shape(ABC): # Abstract base class
@abstractmethod # Enforce subclasses to implement area()
def area(self):
pass
class Triangle(Shape):
def __init__(self, base, height):
self.base = base
self.height = height
def area(self): # Must implement area()!
return 0.5 * self.base * self.height
triangle = Triangle(4, 5)
print(triangle.area()) # Output: 10.0
# Trying to instantiate Shape raises an error:
shape = Shape() # TypeError: Can't instantiate abstract class Shape with abstract method area
Property Decorators
Use @property to define “getters” and “setters” for attributes, making them act like public attributes while enforcing validation.
class Person:
def __init__(self, age):
self._age = age # Protected attribute
@property # Getter
def age(self):
return self._age
@age.setter # Setter
def age(self, new_age):
if new_age > 0 and new_age <= 120:
self._age = new_age
else:
raise ValueError("Age must be between 1 and 120.")
person = Person(30)
print(person.age) # Output: 30
person.age = 35 # Uses the setter
print(person.age) # Output: 35
person.age = 150 # ValueError: Age must be between 1 and 120.
Common Pitfalls and Best Practices
- Forgetting
self: Instance methods requireselfas the first parameter. - Overusing Inheritance: Prefer composition (has-a relationships) over inheritance (is-a) when possible.
- Modifying Class Attributes Accidentally: Avoid changing class attributes via instances (e.g.,
car1.wheels = 3creates an instance attribute). - Poor Naming: Use descriptive names (e.g.,
BankAccountinstead ofBA).
Conclusion
OOP is a cornerstone of Python programming, enabling modular, reusable, and maintainable code. By mastering classes, objects, encapsulation, inheritance, and polymorphism, you’ll be able to model complex systems and solve problems more effectively.
Start small: define a class for a real-world object (e.g., Book, Dog), add attributes/methods, and experiment with inheritance. The more you practice, the more intuitive OOP will become!