TechLead
Lección 8 de 25
5 min de lectura
Python

Python orientado a objetos

Domina clases, herencia, polimorfismo, metodos especiales y patrones de diseno OOP en Python

Clases y objetos

Python es un lenguaje multiparadigma, pero sus caracteristicas orientadas a objetos son poderosas y pythonicas. Todo en Python es un objeto — enteros, cadenas, funciones e incluso las clases mismas. Entender OOP en Python significa entender la palabra clave class, self, los metodos especiales (dunder) y el enfoque de Python hacia la encapsulacion.

class Dog:
    """A simple Dog class demonstrating OOP basics."""

    # Class attribute (shared by all instances)
    species = "Canis familiaris"

    def __init__(self, name: str, age: int, breed: str):
        """Initialize a Dog instance."""
        # Instance attributes (unique to each instance)
        self.name = name
        self.age = age
        self.breed = breed

    def bark(self) -> str:
        return f"{self.name} says Woof!"

    def description(self) -> str:
        return f"{self.name} is a {self.age}-year-old {self.breed}"

    def birthday(self) -> None:
        self.age += 1
        print(f"Happy birthday, {self.name}! Now {self.age} years old.")

# Creating instances
buddy = Dog("Buddy", 3, "Golden Retriever")
max_dog = Dog("Max", 5, "German Shepherd")

print(buddy.bark())          # "Buddy says Woof!"
print(max_dog.description()) # "Max is a 5-year-old German Shepherd"
print(Dog.species)           # "Canis familiaris"
buddy.birthday()             # "Happy birthday, Buddy! Now 4 years old."

Metodos especiales (dunder)

Python usa metodos de doble guion bajo (dunder) para implementar la sobrecarga de operadores y definir como se comportan los objetos con funciones y sintaxis integradas. Implementar estos metodos hace que tus clases se sientan como tipos nativos de Python.

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

    def __repr__(self) -> str:
        """Developer-friendly string representation."""
        return f"Vector({self.x}, {self.y})"

    def __str__(self) -> str:
        """User-friendly string representation."""
        return f"({self.x}, {self.y})"

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __sub__(self, other):
        return Vector(self.x - other.x, self.y - other.y)

    def __mul__(self, scalar: float):
        return Vector(self.x * scalar, self.y * scalar)

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

    def __eq__(self, other) -> bool:
        return self.x == other.x and self.y == other.y

    def __len__(self) -> int:
        return 2

    def __getitem__(self, index):
        if index == 0: return self.x
        if index == 1: return self.y
        raise IndexError("Vector index out of range")

v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(v1 + v2)      # (4, 6)
print(v1 * 3)       # (9, 12)
print(abs(v1))       # 5.0
print(v1 == Vector(3, 4))  # True
print(v1[0])         # 3

Herencia y polimorfismo

Python soporta herencia simple y multiple. Las subclases heredan todos los metodos y atributos de su clase padre y pueden sobreescribirlos o extenderlos. Python usa el Orden de Resolucion de Metodos (MRO) para determinar que metodo llamar en escenarios de herencia multiple.

from abc import ABC, abstractmethod

class Shape(ABC):
    """Abstract base class for shapes."""

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

    @abstractmethod
    def perimeter(self) -> float:
        pass

    def describe(self) -> str:
        return f"{self.__class__.__name__}: area={self.area():.2f}, perimeter={self.perimeter():.2f}"

class Circle(Shape):
    def __init__(self, radius: float):
        self.radius = radius

    def area(self) -> float:
        return 3.14159 * self.radius ** 2

    def perimeter(self) -> float:
        return 2 * 3.14159 * self.radius

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

    def area(self) -> float:
        return self.width * self.height

    def perimeter(self) -> float:
        return 2 * (self.width + self.height)

# Polymorphism - same interface, different behavior
shapes: list[Shape] = [Circle(5), Rectangle(4, 6)]
for shape in shapes:
    print(shape.describe())
# Circle: area=78.54, perimeter=31.42
# Rectangle: area=24.00, perimeter=20.00

# super() for calling parent methods
class Square(Rectangle):
    def __init__(self, side: float):
        super().__init__(side, side)

s = Square(5)
print(s.area())  # 25.0

Propiedades y encapsulacion

Python no tiene atributos verdaderamente privados. Por convencion, un prefijo de guion bajo simple significa "protegido" y un prefijo de doble guion bajo activa el name mangling. El decorador @property te permite definir getters, setters y deleters para acceso controlado a atributos.

class BankAccount:
    def __init__(self, owner: str, balance: float = 0):
        self.owner = owner
        self._balance = balance  # "protected" by convention
        self._transactions: list[float] = []

    @property
    def balance(self) -> float:
        """Read-only balance property."""
        return self._balance

    def deposit(self, amount: float) -> None:
        if amount <= 0:
            raise ValueError("Deposit must be positive")
        self._balance += amount
        self._transactions.append(amount)

    def withdraw(self, amount: float) -> None:
        if amount <= 0:
            raise ValueError("Withdrawal must be positive")
        if amount > self._balance:
            raise ValueError("Insufficient funds")
        self._balance -= amount
        self._transactions.append(-amount)

    @property
    def transaction_history(self) -> list[float]:
        return self._transactions.copy()  # Return a copy to prevent mutation

    def __repr__(self) -> str:
        return f"BankAccount(owner='{self.owner}', balance={self._balance:.2f})"

account = BankAccount("Alice", 1000)
account.deposit(500)
account.withdraw(200)
print(account.balance)              # 1300.0
print(account.transaction_history)  # [500, -200]
# account.balance = 999999  # AttributeError - read-only property

Dataclasses

El modulo dataclasses (Python 3.7+) genera automaticamente __init__, __repr__, __eq__ y otros metodos, reduciendo el codigo repetitivo para clases que almacenan datos.

from dataclasses import dataclass, field

@dataclass
class User:
    name: str
    email: str
    age: int
    roles: list[str] = field(default_factory=list)
    active: bool = True

user = User("Alice", "alice@example.com", 30)
print(user)  # User(name='Alice', email='alice@example.com', age=30, roles=[], active=True)

user2 = User("Alice", "alice@example.com", 30)
print(user == user2)  # True (auto-generated __eq__)

@dataclass(frozen=True)  # Immutable dataclass
class Point:
    x: float
    y: float

p = Point(3.0, 4.0)
# p.x = 5.0  # FrozenInstanceError

Puntos clave

  • Usa metodos dunder: Haz que tus clases se comporten como tipos integrados
  • ABC para interfaces: Usa clases base abstractas para definir contratos
  • Propiedades sobre getters/setters: Usa @property para acceso controlado
  • Dataclasses reducen codigo repetitivo: Usalas para clases que almacenan datos

Continuar Aprendiendo