# Part 4: OOP in Python - Quick Review for Experienced Developers

This document provides a concise overview of Object-Oriented Programming (OOP) syntax and concepts in Python for developers familiar with OOP principles.

---

### 1. Classes and Objects

- **Basic Definition**: Use the `class` keyword. The first argument to instance methods is conventionally `self`.
- **`__init__`**: The constructor method.
- **`@dataclass`**: A decorator that auto-generates boilerplate code like `__init__()`, `__repr__()`, `__eq__()`, etc. Highly recommended for data-centric classes.

```python
from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

    def distance_from_origin(self) -> float:
        return (self.x**2 + self.y**2) ** 0.5

p = Point(3, 4)
print(p.distance_from_origin()) # Output: 5.0
```

---

### 2. Inheritance

- **Syntax**: `class ChildClass(ParentClass):`
- **`super()`**: Used to call methods from the parent class.
- **Method Overriding**: Simply define a method with the same name in the child class.

```python
@dataclass
class Point3D(Point): # Inherits from Point
    z: float

    # Overriding the method
    def distance_from_origin(self) -> float:
        # Calling parent's method using super()
        d_2d = super().distance_from_origin()
        return (d_2d**2 + self.z**2) ** 0.5

p3d = Point3D(3, 4, 12)
print(p3d.distance_from_origin()) # Output: 13.0
```

---

### 3. Encapsulation

- **Convention, not Enforcement**: Python doesn't have `private` or `protected` keywords like Java/C++.
- **`_` (Single Underscore) Prefix**: A convention indicating an attribute is for internal use (protected).
- **`__` (Double Underscore) Prefix**: "Name mangling". The interpreter changes the name to `_ClassName__attribute`, making it harder to access from outside but not truly private.

```python
class MyClass:
    def __init__(self):
        self._internal_var = 1 # Convention for internal use
        self.__mangled_var = 2 # Name is mangled

    def get_mangled(self):
        return self.__mangled_var

obj = MyClass()
print(obj._internal_var) # 1 (Accessible)
# print(obj.__mangled_var) # AttributeError
print(obj._MyClass__mangled_var) # 2 (Still accessible if you know the mangled name)
```

---

### 4. Abstraction

- **`abc` module**: Use the `ABC` (Abstract Base Class) and `@abstractmethod` decorator to define abstract classes and methods.
- Subclasses must implement all abstract methods to be instantiated.

```python
from abc import ABC, abstractmethod

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

class Square(Shape):
    def __init__(self, side):
        self.side = side

    def area(self) -> float: # Must implement this method
        return self.side ** 2

# s = Shape() # TypeError: Can't instantiate abstract class
sq = Square(5) # OK
```

---

### 5. Magic Methods (Dunder Methods)

Methods with double underscores (`__method__`) that allow you to emulate the behavior of built-in types.

| Method | Description | Called by |
| :--- | :--- | :--- |
| `__init__(self, ...)` | Object initializer | `MyClass(...)` |
| `__str__(self)` | String representation for end-users | `str(obj)`, `print(obj)` |
| `__repr__(self)` | Unambiguous representation for developers | `repr(obj)` |
| `__len__(self)` | Length of the object | `len(obj)` |
| `__eq__(self, other)`| Equality comparison | `obj1 == obj2` |
| `__add__(self, other)`| Addition operator | `obj1 + obj2` |

---
For more detailed explanations, refer to the main [part_4_object_oriented_programming.md](./part_4_object_oriented_programming.md) document. 