# Chapter 6: Object-Oriented Programming (OOP)

Object-Oriented Programming (OOP) is a programming paradigm that organizes code into objects. Objects encapsulate data (attributes) and behavior (methods), making it easier to manage and scale programs.

#### Key Concepts of OOP

1. **Classes and Objects**:
   * A **class** is a blueprint for creating objects.
   * An **object** is an instance of a class.
2. **Attributes and Methods**:
   * **Attributes** represent the data of an object.
   * **Methods** are functions defined inside a class that operate on objects.
3. **Encapsulation**:
   * Bundling data and methods together.
   * Restricting access to certain parts of an object using access modifiers.
4. **Inheritance**:
   * Mechanism for creating a new class (child) from an existing class (parent).
5. **Polymorphism**:
   * Ability for methods to perform different actions based on the object that calls them.
6. **Abstraction**:
   * Hiding complex implementation details and showing only the essential features.

#### Defining and Creating Classes

**Syntax:**

```python
class ClassName:
    def __init__(self, parameters):
        self.attribute = value  # Initializing attributes

    def method(self):
        # Method logic
```

**Example:**

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

    def greet(self):
        print(f"Hello, my name is {self.name} and I am {self.age} years old.")

# Creating objects
person1 = Person("Alice", 25)
person1.greet()  # Output: Hello, my name is Alice and I am 25 years old.
```

#### Attributes and Methods

**Instance Attributes:**

* Specific to an object.
* Defined in the `__init__` method using `self`.

**Class Attributes:**

* Shared by all instances of the class.
* Defined directly within the class.

```python
class Circle:
    pi = 3.14  # Class attribute

    def __init__(self, radius):
        self.radius = radius  # Instance attribute

    def area(self):
        return Circle.pi * self.radius ** 2

circle = Circle(5)
print(circle.area())  # Output: 78.5
```

#### Encapsulation

Encapsulation allows control over access to attributes and methods.

**Access Modifiers:**

* **Public**: Attributes and methods accessible anywhere (default).
* **Protected**: Indicated by a single underscore `_`. Should be accessed only within the class and its subclasses.
* **Private**: Indicated by a double underscore `__`. Accessible only within the class.

```python
class BankAccount:
    def __init__(self, balance):
        self.__balance = balance  # Private attribute

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

    def get_balance(self):
        return self.__balance

account = BankAccount(1000)
account.deposit(500)
print(account.get_balance())  # Output: 1500
```

#### Inheritance

Inheritance enables a class to acquire attributes and methods from another class.

**Syntax:**

```python
class ParentClass:
    # Parent class definition

class ChildClass(ParentClass):
    # Child class definition
```

**Example:**

```python
class Animal:
    def speak(self):
        print("Animal makes a sound.")

class Dog(Animal):
    def speak(self):
        print("Dog barks.")

animal = Animal()
animal.speak()  # Output: Animal makes a sound.

dog = Dog()
dog.speak()  # Output: Dog barks.
```

#### Polymorphism

Polymorphism allows different classes to define methods with the same name but different behavior.

**Example:**

```python
class Bird:
    def move(self):
        print("Bird flies.")

class Fish:
    def move(self):
        print("Fish swims.")

animals = [Bird(), Fish()]
for animal in animals:
    animal.move()
# Output:
# Bird flies.
# Fish swims.
```

#### Abstraction

Abstraction hides the implementation details and shows only the functionality.

**Example with Abstract Base Class:**

```python
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

rectangle = Rectangle(5, 10)
print(rectangle.area())  # Output: 50
```

#### Exercises

**Exercise 1:**

Create a class `Car` with attributes `make`, `model`, and `year`. Include a method to display the car’s details.

**Solution:**

```python
class Car:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year

    def display_details(self):
        print(f"Car: {self.year} {self.make} {self.model}")

car = Car("Toyota", "Corolla", 2022)
car.display_details()  # Output: Car: 2022 Toyota Corolla
```

**Exercise 2:**

Create a base class `Employee` and a subclass `Manager`. The `Manager` class should have an additional attribute `department` and a method to display all details.

**Solution:**

```python
class Employee:
    def __init__(self, name, id):
        self.name = name
        self.id = id

    def display(self):
        print(f"Employee Name: {self.name}, ID: {self.id}")

class Manager(Employee):
    def __init__(self, name, id, department):
        super().__init__(name, id)
        self.department = department

    def display(self):
        super().display()
        print(f"Department: {self.department}")

manager = Manager("Alice", 101, "HR")
manager.display()
# Output:
# Employee Name: Alice, ID: 101
# Department: HR
```

**Exercise 3:**

Create an abstract class `Appliance` with a method `turn_on`. Implement two subclasses `WashingMachine` and `Refrigerator` that provide specific implementations for `turn_on`.

**Solution:**

```python
from abc import ABC, abstractmethod

class Appliance(ABC):
    @abstractmethod
    def turn_on(self):
        pass

class WashingMachine(Appliance):
    def turn_on(self):
        print("Washing machine is now ON.")

class Refrigerator(Appliance):
    def turn_on(self):
        print("Refrigerator is now ON.")

wm = WashingMachine()
wm.turn_on()  # Output: Washing machine is now ON.

fridge = Refrigerator()
fridge.turn_on()  # Output: Refrigerator is now ON.
```

In the next chapter, we will explore modules and packages, understanding how to organize and reuse code effectively in Python.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://py.d19.in/chapter-6-object-oriented-programming-oop.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
