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