Object Oriented Programming (OOP) - CameronAuler/python-devops GitHub Wiki

Table of Contents

Object-Oriented Programming (OOP) is a programming paradigm that organizes code using objects and classes. It promotes code reusability, modularity, and organization by modeling real-world entities as objects.

Classes & Objects

Class Definition

A class is a blueprint for creating objects. It defines attributes (variables) and methods (functions) that describe the behavior of objects. An object is an instance of a class with its own unique data.

class Car:
    def __init__(self, brand, model):
        self.brand = brand  # Instance variable
        self.model = model  # Instance variable

# Creating objects (instances)
car1 = Car("Toyota", "Corolla")
car2 = Car("Honda", "Civic")

print(car1.brand, car1.model)  # Accessing instance attributes
print(car2.brand, car2.model)
# Output:
Toyota Corolla
Honda Civic

Instance & Class Variables

Instance Variables

Instance variables are unique to each instance (object) and are defined inside __init__ using self.

class Dog:
    def __init__(self, name):
        self.name = name  # Instance variable

dog1 = Dog("Buddy")
dog2 = Dog("Charlie")

print(dog1.name)  # Output: Buddy
print(dog2.name)  # Output: Charlie

Class Variables

Class variables are Shared across all instances and defined outside __init__ using the class name.

class Animal:
    species = "Mammal"  # Class variable

    def __init__(self, name):
        self.name = name  # Instance variable

animal1 = Animal("Lion")
animal2 = Animal("Tiger")

print(animal1.species)  # Output: Mammal
print(animal2.species)  # Output: Mammal
Animal.species = "Reptile"
print(animal1.species)  # Output: Reptile

Methods: Instance, Class, & Static

Instance Methods

Instance methods operate on instance variables and require self as the first parameter.

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

    def greet(self):  # Instance method
        return f"Hello, my name is {self.name}."

person = Person("Alice")
print(person.greet())
# Output:
Hello, my name is Alice.

Class Methods

Class methods operate on class variables and are defined using @classmethod and take cls as the first parameter.

class Employee:
    company = "TechCorp"

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

    @classmethod
    def change_company(cls, new_company):
        cls.company = new_company

Employee.change_company("NewTech")
print(Employee.company)  # Output: NewTech

Static Methods

Static methods are independent of class or instance variables and are defined using @staticmethod.

class MathUtils:
    @staticmethod
    def add(x, y):
        return x + y

print(MathUtils.add(3, 5))  # Output: 8

Inheritance

Inheritance allows a class to derive properties and methods from another class.

Single Inheritance

A child class inherits attributes and methods from a parent class.

class Vehicle:
    def __init__(self, brand):
        self.brand = brand

    def show_brand(self):
        return f"Brand: {self.brand}"

class Car(Vehicle):  # Inheriting from Vehicle
    def __init__(self, brand, model):
        super().__init__(brand)  # Call parent constructor
        self.model = model

    def show_model(self):
        return f"Model: {self.model}"

car = Car("Toyota", "Camry")
print(car.show_brand())
print(car.show_model())
# Output:
Brand: Toyota
Model: Camry

Multiple Inheritance

A class can inherit from multiple parent classes.

class Engine:
    def engine_type(self):
        return "V8 Engine"

class Wheels:
    def wheel_type(self):
        return "Alloy Wheels"

class Car(Engine, Wheels):  # Multiple Inheritance
    pass

my_car = Car()
print(my_car.engine_type())
print(my_car.wheel_type())
# Output:
V8 Engine
Alloy Wheels

Polymorphism

Polymorphism allows different classes to use the same method name but implement different behaviors.

class Dog:
    def speak(self):
        return "Woof!"

class Cat:
    def speak(self):
        return "Meow!"

def animal_sound(animal):
    return animal.speak()

dog = Dog()
cat = Cat()

print(animal_sound(dog))
print(animal_sound(cat))
# Output:
Woof!
Meow!

Encapsulation

Encapsulation restricts direct access to certain data to prevent unauthorized access.

Private Attributes

Prefix variables with double underscore (__) to make them private.

class BankAccount:
    def __init__(self, balance):
        self.__balance = balance  # Private variable

    def deposit(self, amount):
        self.__balance += amount

    def get_balance(self):
        return self.__balance  # Only accessible via method

account = BankAccount(1000)
account.deposit(500)
print(account.get_balance())  # Output: 1500
# print(account.__balance)  # This would raise an AttributeError

Magic Methods & Operator Overloading

Magic methods (dunder methods) define special behaviors for objects. __str__ returns a human-readable string representation while __repr__ is used for debugging.

__str__ & __repr__ Methods

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author

    def __str__(self):  # Readable representation
        return f"{self.title} by {self.author}"

    def __repr__(self):  # Debugging representation
        return f"Book('{self.title}', '{self.author}')"

book = Book("1984", "George Orwell")
print(book)        # Calls __str__
print(repr(book))  # Calls __repr__
# Output:
1984 by George Orwell
Book('1984', 'George Orwell')

Operator Overloading

Operator overloading allows custom objects to define how built-in operators (+, -, *, /, ==, etc.) behave when used with them. This makes objects more intuitive to work with by enabling natural syntax instead of calling explicit methods. Normally, Python does not allow arithmetic operations on custom objects unless explicitly defined. Operator overloading enables this functionality by defining special dunder methods (double underscore methods like __add__, __sub__, etc.).

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):  # Overloading the `+` operator
        return Point(self.x + other.x, self.y + other.y)

    def __sub__(self, other):  # Overloading the `-` operator
        return Point(self.x - other.x, self.y - other.y)

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

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

p3 = p1 + p2  # Calls __add__
p4 = p1 - p2  # Calls __sub__

print(p3)  # (6, 8)
print(p4)  # (-2, -2)

# Output:
(6, 8)
(-2, -2)

Overload Operators

Operator Method Description
+ __add__(self, other) Defines behavior for self + other.
- __sub__(self, other) Defines behavior for self - other.
* __mul__(self, other) Defines behavior for self * other.
/ __truediv__(self, other) Defines behavior for self / other.
// __floordiv__(self, other) Defines behavior for self // other.
% __mod__(self, other) Defines behavior for self % other.
** __pow__(self, other) Defines behavior for self ** other.
<< __lshift__(self, other) Defines behavior for self << other (bitwise left shift).
>> __rshift__(self, other) Defines behavior for self >> other (bitwise right shift).
& __and__(self, other) Defines behavior for self & other (bitwise AND).
| __or__(self, other) Defines behavior for `self
^ __xor__(self, other) Defines behavior for self ^ other (bitwise XOR).
~ __invert__(self) Defines behavior for ~self (bitwise NOT).
== __eq__(self, other) Defines behavior for self == other.
!= __ne__(self, other) Defines behavior for self != other.
< __lt__(self, other) Defines behavior for self < other.
> __gt__(self, other) Defines behavior for self > other.
<= __le__(self, other) Defines behavior for self <= other.
>= __ge__(self, other) Defines behavior for self >= other.
() __call__(self, *args, **kwargs) Defines behavior for calling an instance like a function.
[] __getitem__(self, key) Defines behavior for self[key].
[]= __setitem__(self, key, value) Defines behavior for self[key] = value.
del [] __delitem__(self, key) Defines behavior for del self[key].
len() __len__(self) Defines behavior for len(self).
repr() __repr__(self) Defines unambiguous string representation of the object.
str() __str__(self) Defines human-readable string representation.
hash() __hash__(self) Defines behavior for hash(self), needed for dict keys & sets.
bool() __bool__(self) Defines truthy or falsy behavior in conditionals.
iter() __iter__(self) Defines iterator behavior for iter(self).
next() __next__(self) Defines behavior for next(self), used in loops.
contains() __contains__(self, item) Defines behavior for item in self.
enter / exit __enter__(self), __exit__(self, exc_type, exc_value, traceback) Defines behavior for context managers (with statement).