En esta página
Clases y objetos
Programación orientada a objetos en Python
La programación orientada a objetos (POO) organiza el código alrededor de objetos — estructuras que combinan datos (atributos) y comportamiento (métodos). En Python, todo es un objeto: enteros, strings, listas, funciones.
Una clase es el molde o plantilla para crear objetos. Un objeto (o instancia) es una copia concreta creada a partir de esa plantilla.
Definiendo una clase
class Coche:
"""Representa un coche con marca, modelo y año."""
# Atributo de clase — compartido por TODAS las instancias
ruedas = 4
tipo_vehiculo = "terrestre"
def __init__(self, marca: str, modelo: str, año: int) -> None:
"""Constructor — se llama al crear una nueva instancia."""
# Atributos de instancia — únicos por objeto
self.marca = marca
self.modelo = modelo
self.año = año
self._kilometraje = 0 # Convención: _ = "privado" (no enforced)
self.__vin = "VIN-SECRETO" # __ = name mangling
def describir(self) -> str:
"""Devuelve descripción del coche."""
return f"{self.año} {self.marca} {self.modelo}"
def conducir(self, km: float) -> None:
"""Simula conducir el coche."""
if km < 0:
raise ValueError("Los kilómetros no pueden ser negativos")
self._kilometraje += km
print(f"Conduciendo {km} km en {self.describir()}")
# Crear instancias
coche1 = Coche("Toyota", "Corolla", 2022)
coche2 = Coche("Tesla", "Model 3", 2024)
print(coche1.describir()) # 2022 Toyota Corolla
print(coche2.marca) # Tesla
# Atributo de clase accesible desde instancias Y la clase
print(coche1.ruedas) # 4
print(Coche.ruedas) # 4
coche1.conducir(100)El parámetro self
self hace referencia a la instancia actual. En Python, se pasa explícitamente como primer parámetro de todos los métodos de instancia. Al llamar coche.describir(), Python lo traduce internamente a Coche.describir(coche).
Atributos privados y name mangling
class CuentaBancaria:
def __init__(self, titular: str, saldo: float = 0) -> None:
self.titular = titular # Público
self._historial: list[str] = [] # Convencionalmente "privado"
self.__saldo = saldo # Name mangling: _CuentaBancaria__saldo
def depositar(self, monto: float) -> None:
if monto <= 0:
raise ValueError("El monto debe ser positivo")
self.__saldo += monto
self._historial.append(f"+{monto:.2f}")
def retirar(self, monto: float) -> None:
if monto <= 0:
raise ValueError("El monto debe ser positivo")
if monto > self.__saldo:
raise ValueError("Saldo insuficiente")
self.__saldo -= monto
self._historial.append(f"-{monto:.2f}")
def obtener_saldo(self) -> float:
return self.__saldo
cuenta = CuentaBancaria("Ana", 1000)
cuenta.depositar(500)
cuenta.retirar(200)
print(cuenta.obtener_saldo()) # 1300.0
print(cuenta._historial) # ['+500.00', '-200.00']
# Name mangling — el nombre cambia pero no es verdaderamente privado
# print(cuenta.__saldo) # AttributeError
print(cuenta._CuentaBancaria__saldo) # 1300.0 — accesible si quieres@property — Atributos calculados con sintaxis de acceso
El decorador @property permite definir métodos que se acceden como atributos:
class Temperatura:
"""Almacena temperatura y permite conversión entre escalas."""
def __init__(self, celsius: float) -> None:
self._celsius = celsius
@property
def celsius(self) -> float:
return self._celsius
@celsius.setter
def celsius(self, valor: float) -> None:
if valor < -273.15:
raise ValueError("Temperatura por debajo del cero absoluto")
self._celsius = valor
@celsius.deleter
def celsius(self) -> None:
del self._celsius
@property
def fahrenheit(self) -> float:
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, valor: float) -> None:
self.celsius = (valor - 32) * 5/9
@property
def kelvin(self) -> float:
return self._celsius + 273.15
temp = Temperatura(100)
print(temp.celsius) # 100
print(temp.fahrenheit) # 212.0
print(temp.kelvin) # 373.15
temp.fahrenheit = 32
print(temp.celsius) # 0.0@staticmethod — Métodos sin acceso a self o cls
Los métodos estáticos son funciones dentro de una clase que no necesitan ni self ni cls. Son útiles para agrupar funciones relacionadas:
class Matematicas:
@staticmethod
def es_primo(n: int) -> bool:
if n < 2:
return False
return all(n % i != 0 for i in range(2, int(n**0.5) + 1))
@staticmethod
def mcd(a: int, b: int) -> int:
"""Máximo común divisor (algoritmo de Euclides)."""
while b:
a, b = b, a % b
return a
@staticmethod
def mcm(a: int, b: int) -> int:
"""Mínimo común múltiplo."""
return abs(a * b) // Matematicas.mcd(a, b)
# Llamar sin crear instancia
print(Matematicas.es_primo(17)) # True
print(Matematicas.mcd(48, 18)) # 6
print(Matematicas.mcm(4, 6)) # 12@classmethod — Métodos alternativos de construcción
Los métodos de clase reciben cls (la clase, no la instancia) como primer argumento. Son útiles para crear constructores alternativos:
from datetime import date
class Persona:
def __init__(self, nombre: str, año_nacimiento: int) -> None:
self.nombre = nombre
self.año_nacimiento = año_nacimiento
@property
def edad(self) -> int:
return date.today().year - self.año_nacimiento
@classmethod
def desde_cadena(cls, datos: str) -> "Persona":
"""Constructor alternativo desde string 'nombre,año'."""
nombre, año = datos.split(",")
return cls(nombre.strip(), int(año.strip()))
@classmethod
def desde_nacimiento(cls, nombre: str, fecha_nac: date) -> "Persona":
"""Constructor alternativo desde fecha de nacimiento."""
return cls(nombre, fecha_nac.year)
def __repr__(self) -> str:
return f"Persona(nombre={self.nombre!r}, edad={self.edad})"
p1 = Persona("Ana", 1995)
p2 = Persona.desde_cadena("Bruno, 1990")
p3 = Persona.desde_nacimiento("Carmen", date(1988, 3, 15))
print(p1) # Persona(nombre='Ana', edad=31)
print(p2) # Persona(nombre='Bruno', edad=36)Métodos dunder (mágicos)
Los métodos dunder (double underscore) permiten que tus clases se integren con la sintaxis y las operaciones nativas de Python:
class Vector2D:
"""Vector en dos dimensiones con operaciones matemáticas."""
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
def __repr__(self) -> str:
"""Representación técnica (para desarrolladores)."""
return f"Vector2D({self.x}, {self.y})"
def __str__(self) -> str:
"""Representación legible (para usuarios)."""
return f"({self.x}, {self.y})"
def __add__(self, otro: "Vector2D") -> "Vector2D":
"""Habilita v1 + v2."""
return Vector2D(self.x + otro.x, self.y + otro.y)
def __sub__(self, otro: "Vector2D") -> "Vector2D":
"""Habilita v1 - v2."""
return Vector2D(self.x - otro.x, self.y - otro.y)
def __mul__(self, escalar: float) -> "Vector2D":
"""Habilita v * escalar."""
return Vector2D(self.x * escalar, self.y * escalar)
def __rmul__(self, escalar: float) -> "Vector2D":
"""Habilita escalar * v (operando derecho)."""
return self.__mul__(escalar)
def __eq__(self, otro: object) -> bool:
"""Habilita v1 == v2."""
if not isinstance(otro, Vector2D):
return NotImplemented
return self.x == otro.x and self.y == otro.y
def __abs__(self) -> float:
"""Habilita abs(v) — magnitud del vector."""
return (self.x ** 2 + self.y ** 2) ** 0.5
def __len__(self) -> int:
"""Habilita len(v) — número de componentes."""
return 2
def __bool__(self) -> bool:
"""Habilita bool(v) — False solo si es vector nulo."""
return self.x != 0 or self.y != 0
v1 = Vector2D(3, 4)
v2 = Vector2D(1, 2)
print(v1 + v2) # (4, 6)
print(v1 - v2) # (2, 2)
print(v1 * 2) # (6, 8)
print(3 * v2) # (3, 6)
print(abs(v1)) # 5.0 (hipotenusa del triángulo 3-4-5)
print(len(v1)) # 2
print(bool(v1)) # True
print(bool(Vector2D(0, 0))) # False
print(repr(v1)) # Vector2D(3, 4)dataclasses — Clases de datos sin repetición
El módulo dataclasses (Python 3.7+) genera automáticamente __init__, __repr__, __eq__ y más:
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class Articulo:
titulo: str
autor: str
contenido: str
etiquetas: list[str] = field(default_factory=list)
publicado: bool = False
creado_en: datetime = field(default_factory=datetime.now)
vistas: int = field(default=0, repr=False) # Excluir del repr
def publicar(self) -> None:
self.publicado = True
print(f"'{self.titulo}' publicado")
@property
def resumen(self) -> str:
palabras = self.contenido.split()
if len(palabras) <= 20:
return self.contenido
return " ".join(palabras[:20]) + "..."
# dataclass con frozen=True — inmutable (como namedtuple pero mejor)
@dataclass(frozen=True)
class CoordenadaGPS:
latitud: float
longitud: float
def __post_init__(self) -> None:
if not -90 <= self.latitud <= 90:
raise ValueError(f"Latitud inválida: {self.latitud}")
if not -180 <= self.longitud <= 180:
raise ValueError(f"Longitud inválida: {self.longitud}")
articulo = Articulo("Python 3.14", "David", "Contenido del artículo...")
print(articulo) # Articulo(titulo='Python 3.14', autor='David', ...)
articulo.publicar()
madrid = CoordenadaGPS(40.4168, -3.7038)
print(madrid) # CoordenadaGPS(latitud=40.4168, longitud=-3.7038)Resumen
Las clases en Python ofrecen una POO completa y expresiva. Los decoradores @property, @staticmethod y @classmethod permiten diseñar APIs limpias. Los métodos dunder integran tus objetos con la sintaxis nativa de Python. Para clases de datos simples, @dataclass elimina el código repetitivo. En la próxima lección ampliaremos estos conceptos con herencia, polimorfismo y clases abstractas.
Inicia sesión para guardar tu progreso