📘 Lección 5.1 — Clases y Objetos en Python
- Parte 1 — Introducción a las clases y objetos
- Parte 2 — Constructor
__init__
y creación de objetos - Parte 3 — Atributos: instancia, clase y propiedades
- Parte 4 — Métodos de instancia y de clase
- Parte 5 — Métodos especiales (
__str__
,__repr__
) y buenas prácticas
Haz clic en cada parte para navegar. Este módulo introduce los fundamentos de la Programación Orientada a Objetos (POO) en Python, desde la teoría hasta la práctica con ejemplos claros y ejercicios guiados.
Parte 1 — Introducción a las clases y objetos
Objetivo: comprender qué son las clases y los objetos en Python, por qué el enfoque orientado a objetos organiza mejor el código, y cómo dar los primeros pasos creando instancias.
1) ¿Qué es una clase? ¿Qué es un objeto?
En POO, una clase es como un plano: define las características (atributos) y los comportamientos (métodos) que tendrán los objetos. Un objeto (o instancia) es una realización concreta de ese plano. Es decir, con el plano Coche
puedes crear múltiples coches reales con colores o kilometrajes distintos.
# Plano (clase)
class Coche:
pass
# Casas construidas (objetos)
mi_coche = Coche()
coche_de_amigo = Coche()
Aunque ambos objetos provengan de la misma clase, cada uno tendrá sus valores propios para los atributos que definamos más adelante (color, modelo, etc.).
2) ¿Por qué usar POO?
- Organización: agrupa datos y comportamientos relacionados en un único módulo lógico (la clase).
- Reutilización: define una vez, crea muchas instancias sin repetir código.
- Modularidad: puedes cambiar la implementación sin romper a quien usa la clase (si mantienes la interfaz).
- Modelado natural del mundo real: mapea entidades (Libro, Persona, Cuenta) con facilidad.
3) Primer contacto: clase mínima y objetos
Definir una clase en Python es directo: class NombreDeLaClase:
. Luego puedes instanciar llamando a la clase como si fuera una función.
class Libro:
# Aquí más adelante añadiremos atributos y métodos
pass
# Dos objetos (instancias) distintos
libro_python = Libro()
novela_fantasia = Libro()
La clase Libro
actúa como plantilla; cada objeto representará un libro concreto.
4) Añadiendo atributos y comportamiento (vista previa)
Los atributos son datos asociados al objeto (p. ej., titulo
, autor
), mientras que los métodos son funciones que describen su comportamiento (p. ej., abrir()
, leer()
). Profundizamos en esto en las Partes 2–5; aquí una vista conceptual.
class Libro:
def __init__(self, titulo, autor, paginas):
self.titulo = titulo
self.autor = autor
self.paginas = paginas
self.pagina_actual = 0
self.abierto = False
def abrir(self):
if self.abierto:
return f"{self.titulo} ya está abierto"
self.abierto = True
return f"{self.titulo} ha sido abierto"
def leer(self, n):
if not self.abierto:
return f"No puedes leer: {self.titulo} está cerrado"
self.pagina_actual = min(self.pagina_actual + n, self.paginas)
if self.pagina_actual == self.paginas:
return f"Has terminado {self.titulo}"
return f"Página {self.pagina_actual}/{self.paginas}"
def __str__(self):
estado = "abierto" if self.abierto else "cerrado"
return f"{self.titulo} por {self.autor} — {estado}"
Este patrón integra constructor __init__
, métodos de instancia y un método especial __str__
para imprimir amigable.
5) Tabla de conceptos clave
Concepto | Qué es | Ejemplo |
---|---|---|
Clase | Plantilla (plano) que define atributos y métodos. | class Coche: ... |
Objeto | Instancia concreta de una clase con valores propios. | mi_coche = Coche() |
Atributo | Dato asociado (estado) del objeto. | self.titulo = "El Quijote" |
Método | Función de la clase que define comportamiento. | def leer(self, n): ... |
Abstracción/Encapsulamiento | Centrarse en el qué; agrupar y controlar acceso. | Modelo Libro oculta detalles internos. |
6) Mini-práctica guiada
- Crea la clase vacía
Persona
y dos objetos:class Persona: pass ana = Persona() juan = Persona()
Compara
ana is juan
(debe serFalse
): son instancias distintas del mismo plano. - Añade un constructor y prueba atributos:
class Persona: def __init__(self, nombre, edad): self.nombre = nombre self.edad = edad ana = Persona("Ana García", 28) juan = Persona("Juan López", 35) print(ana.nombre, juan.edad)
Cada objeto guarda sus propios valores.
- Añade un método simple y llámalo:
class Persona: def __init__(self, nombre, edad): self.nombre = nombre self.edad = edad def presentarse(self): return f"Hola, soy {self.nombre} y tengo {self.edad} años." print(ana.presentarse())
Los métodos definen el comportamiento (acción) del objeto.
7) Preguntas rápidas de chequeo
- ¿Qué diferencia hay entre clase y objeto en tu propio wording?
- ¿Por qué la POO mejora organización y reutilización en tu proyecto? Da un
- ¿Qué significa “abstraer” al modelar una entidad?
Parte 2 — Constructor __init__
y creación de objetos
Objetivo: dominar el constructor __init__
, entender el uso de self
, inicializar atributos de forma segura (con valores por defecto y validaciones) y crear instancias correctamente.
1) ¿Qué es el constructor __init__
?
El constructor es un método especial que se ejecuta automáticamente al crear un objeto (instancia) de una clase. Su función es inicializar el estado del objeto (sus atributos).
class Libro:
def __init__(self, titulo, autor, paginas):
# "self" es la propia instancia recién creada
self.titulo = titulo
self.autor = autor
self.paginas = paginas
self.pagina_actual = 0 # atributo con valor por defecto
# Crear instancias (objetos)
l1 = Libro("El Quijote", "Cervantes", 863)
l2 = Libro("1984", "Orwell", 328)
print(l1.titulo, l2.autor)
__init__
prepara el objeto para ser usado: fija atributos mínimos y deja el estado consistente.2) El parámetro self
: la referencia al objeto
self
es el primer parámetro de los métodos de instancia; representa la propia instancia. Con self.atributo
guardas datos dentro del objeto.
class Persona:
def __init__(self, nombre):
self.nombre = nombre # atributo de instancia
def saludar(self):
return f"Hola, soy {self.nombre}"
p = Persona("Ana")
print(p.saludar()) # Hola, soy Ana
self
en la definición del método o acceder a variables locales en vez de self.atributo
.3) Atributos en el constructor: obligatorios y por defecto
Diseña __init__
pensando en lo mínimo imprescindible y lo opcional con valores por defecto.
class Usuario:
def __init__(self, nombre, email, activo=True, pais="ES"):
self.nombre = nombre # obligatorio
self.email = email # obligatorio
self.activo = activo # por defecto True
self.pais = pais # por defecto "ES"
u = Usuario("Javier", "javi@example.com")
print(u.activo, u.pais) # True ES
3.1 Validaciones mínimas
Valida entradas para evitar objetos inválidos (fail-fast).
class CuentaBancaria:
def __init__(self, titular, saldo_inicial=0.0):
if not titular or not isinstance(titular, str):
raise ValueError("Titular inválido")
if saldo_inicial < 0:
raise ValueError("El saldo no puede ser negativo")
self.titular = titular
self.saldo = float(saldo_inicial)
3.2 Conversión de tipos (normalización)
class Producto:
def __init__(self, nombre, precio):
self.nombre = str(nombre).strip()
self.precio = float(precio) # normaliza a float
4) Tabla de patrones de inicialización
Patrón | Uso | Ejemplo |
---|---|---|
Obligatorios + opcionales | Parámetros mínimos + defaults sensatos | __init__(a, b, opt=True) |
Validación | Evitar estados inválidos | if x < 0: raise ValueError |
Normalización | Convertir/limpiar entradas | self.precio = float(p) |
Atributos derivados | Calcular campos a partir de otros | self.slug = nombre.lower() |
5) Creación de objetos (instanciación) paso a paso
- Python reserva memoria para la nueva instancia.
- Se llama a
__init__(self, ...)
con los argumentos que pasaste. - Dentro de
__init__
se inicializan atributos enself
. - Se devuelve la instancia lista para usar.
class Ticket:
def __init__(self, id, prioridad="media"):
self.id = id
self.prioridad = prioridad
self.resuelto = False
t1 = Ticket(101) # prioridad por defecto
t2 = Ticket(102, prioridad="alta")
print(t1.prioridad, t2.prioridad) # media alta
6) Errores comunes y cómo evitarlos
- Olvidar
self
en la firma de__init__
o métodos de instancia. - No usar
self.
al guardar atributos (quedan como variables locales y se pierden). - Mutables como valor por defecto (p. ej.
lista=[]
): comparten estado entre instancias. UsaNone
y crea una nueva colección dentro.
# ❌ Peligroso: mutable como default
class Carrito:
def __init__(self, items=[]):
self.items = items
c1 = Carrito()
c2 = Carrito()
c1.items.append("laptop")
print(c2.items) # "laptop" aparece en ambos (sorpresa)
# ✅ Seguro: usar None y crear la lista adentro
class CarritoSeguro:
def __init__(self, items=None):
self.items = list(items) if items is not None else []
7) Buenas prácticas de diseño del constructor
- Mínimos imprescindibles: pide solo lo necesario; el resto con valores por defecto.
- Validación ligera: comprueba lo crítico y falla pronto con mensajes claros.
- Coherencia de nombres: usa nombres de atributos autoexplicativos.
- Documenta: añade docstring en la clase explicando parámetros y efectos.
- Tipos (opcional): puedes añadir type hints para mayor claridad.
class Pedido:
"""Representa un pedido simple.
Args:
ref (str): Referencia única.
unidades (int): Cantidad pedida (> 0).
precio_unitario (float): Precio por unidad (>= 0).
"""
def __init__(self, ref: str, unidades: int, precio_unitario: float):
if unidades <= 0:
raise ValueError("unidades debe ser > 0")
if precio_unitario < 0:
raise ValueError("precio_unitario debe ser >= 0")
self.ref = ref
self.unidades = int(unidades)
self.precio_unitario = float(precio_unitario)
def total(self) -> float:
return self.unidades * self.precio_unitario
8) Caso práctico integrado
Construimos una clase con defaults, validación y un método operativo.
class Alumno:
def __init__(self, nombre, curso, nota=0.0):
if not nombre or not curso:
raise ValueError("nombre y curso son obligatorios")
if not (0.0 <= nota <= 10.0):
raise ValueError("nota fuera de rango (0..10)")
self.nombre = nombre.strip().title()
self.curso = curso.upper()
self.nota = float(nota)
def calificacion(self):
if self.nota >= 9:
return "Sobresaliente"
if self.nota >= 7:
return "Notable"
if self.nota >= 5:
return "Aprobado"
return "Suspenso"
a1 = Alumno("ana pérez", "python", 8.5)
print(a1.nombre, a1.curso, a1.calificacion())
9) Checklist rápido (calidad del __init__
)
- ¿Los parámetros mínimos están claros y ordenados?
- ¿Defaults razonables para lo no esencial?
- ¿Validaciones clave y mensajes útiles?
- ¿Sin mutables como valores por defecto?
- ¿Atributos escritos siempre como
self.x
?
10) Mini-ejercicios
- ProductoSeguro: crea una clase con
nombre
(str),precio
(float >= 0) ytags
(lista opcional). Evita el pitfall de mutables por defecto.class ProductoSeguro: def __init__(self, nombre, precio, tags=None): if precio < 0: raise ValueError("precio >= 0") self.nombre = str(nombre) self.precio = float(precio) self.tags = list(tags) if tags is not None else []
- Sesion: almacena
usuario
yexpira_en
(minutos, > 0). Normaliza aint
y valida rango.class Sesion: def __init__(self, usuario, expira_en): expira_en = int(expira_en) if expira_en <= 0: raise ValueError("expira_en debe ser > 0") self.usuario = usuario self.expira_en = expira_en
- Persona con
nombre
yedad
: limpia espacios en nombre y limita edad a 0..120.class Persona: def __init__(self, nombre, edad): nombre = str(nombre).strip() edad = int(edad) if not nombre: raise ValueError("nombre requerido") if not (0 <= edad <= 120): raise ValueError("edad fuera de rango") self.nombre = nombre.title() self.edad = edad
Parte 3 — Atributos: instancia, clase y propiedades
Objetivo: comprender los diferentes tipos de atributos (de instancia, de clase y propiedades), cómo se definen, cómo se accede a ellos y buenas prácticas para mantener el código claro y seguro.
1) Atributos de instancia
Los atributos de instancia pertenecen a cada objeto de forma individual. Se definen normalmente dentro del constructor __init__
mediante self
.
class Coche:
def __init__(self, marca, color):
self.marca = marca # atributo de instancia
self.color = color # atributo de instancia
c1 = Coche("Toyota", "rojo")
c2 = Coche("Ford", "azul")
print(c1.marca, c2.marca) # Toyota Ford
Cada objeto (c1
, c2
) tiene sus propios valores de marca
y color
, independientes uno del otro.
obj.__dict__
), no en la clase.print(c1.__dict__)
# {'marca': 'Toyota', 'color': 'rojo'}
2) Atributos de clase
Los atributos de clase son compartidos por todas las instancias. Se definen directamente dentro del bloque de la clase (fuera de los métodos).
class Coche:
ruedas = 4 # atributo de clase compartido
def __init__(self, marca):
self.marca = marca
a = Coche("Seat")
b = Coche("Renault")
print(a.ruedas, b.ruedas) # 4 4
Coche.ruedas = 5 # cambia para todos
print(a.ruedas, b.ruedas) # 5 5
Los atributos de clase viven en el namespace
de la clase, y son comunes a todas las instancias (a menos que una instancia defina su propio atributo con el mismo nombre).
Tipo | Definición | Alcance | Ejemplo |
---|---|---|---|
Instancia | Dentro de __init__ | Cada objeto | self.nombre |
Clase | Fuera de métodos | Compartido por todos | Coche.ruedas |
3) Precedencia y sombreado
Si un objeto tiene un atributo con el mismo nombre que uno de clase, el de la instancia “sombrea” al de clase.
class Persona:
especie = "Humano"
p1 = Persona()
p2 = Persona()
p1.especie = "Cyborg" # crea atributo de instancia
print(p1.especie) # Cyborg
print(p2.especie) # Humano
print(Persona.especie) # Humano
Modificar Persona.especie
afecta solo al valor compartido, no a las instancias que lo hayan sobrescrito.
4) Atributos dinámicos
Python permite añadir o eliminar atributos a objetos existentes en tiempo de ejecución (dinámicamente). Aunque flexible, debe usarse con cuidado para mantener coherencia.
class Animal:
pass
a = Animal()
a.nombre = "Toby"
a.tipo = "Perro"
print(a.nombre, a.tipo)
del a.tipo # se puede eliminar
print(a.__dict__)
Este comportamiento flexible es potente, pero puede generar errores difíciles de detectar si se abusa.
__init__
para mayor claridad y previsibilidad.5) Propiedades: control de acceso con @property
Las propiedades permiten controlar el acceso a los atributos (lectura, escritura y borrado) mediante decoradores. Son útiles para validar, transformar o proteger datos internos sin cambiar la interfaz pública.
class Cuenta:
def __init__(self, titular, saldo):
self.titular = titular
self._saldo = saldo # atributo “protegido” (por convención)
@property
def saldo(self):
"""Obtiene el saldo actual"""
return self._saldo
@saldo.setter
def saldo(self, valor):
if valor < 0:
raise ValueError("El saldo no puede ser negativo")
self._saldo = valor
c = Cuenta("Ana", 1000)
c.saldo += 200
print(c.saldo)
# c.saldo = -50 # ValueError
Así, el atributo _saldo
sigue siendo interno, pero se accede como si fuera público (c.saldo
), con validaciones automáticas.
Decorador | Función | Ejemplo |
---|---|---|
@property | Definir lectura controlada | def saldo(self): |
@atributo.setter | Definir escritura controlada | def saldo(self, valor): |
@atributo.deleter | Definir borrado controlado | def saldo(self): del self._saldo |
6) Propiedades de solo lectura
Si defines solo el getter y omites el setter, el atributo será de solo lectura.
class Rectangulo:
def __init__(self, ancho, alto):
self.ancho = ancho
self.alto = alto
@property
def area(self):
return self.ancho * self.alto
r = Rectangulo(10, 5)
print(r.area) # 50
# r.area = 60 # Error: no tiene setter
7) Encapsulamiento: convención de nombres
Python no tiene atributos realmente privados, pero usa prefijos por convención:
Prefijo | Significado | Ejemplo |
---|---|---|
Sin prefijo | Público | nombre |
_guion | Protegido (interno, no forzar acceso) | _saldo |
__doble_guion | Privado (name mangling interno) | __clave → _Clase__clave |
8) Buenas prácticas con atributos y propiedades
- Define todos los atributos de instancia en
__init__
(evita definirlos fuera). - Usa
@property
para controlar lectura/escritura si se requiere validación. - Evita exponer atributos internos directamente (
_atributo
). - Prefiere claridad: no abuses de
__doble_guion
salvo para evitar colisiones de nombres.
9) Mini-ejercicios
- Crea una clase
Empleado
con atributos de claseempresa = "TechCorp"
y de instancianombre
ysalario
. Luego imprime el nombre y la empresa de dos empleados distintos.class Empleado: empresa = "TechCorp" def __init__(self, nombre, salario): self.nombre = nombre self.salario = salario e1 = Empleado("Ana", 2500) e2 = Empleado("Luis", 3000) print(e1.nombre, e1.empresa) print(e2.nombre, e2.empresa)
- Implementa una clase
Producto
con atributo_precio
y propiedadprecio
que impida asignar valores negativos.class Producto: def __init__(self, nombre, precio): self.nombre = nombre self._precio = precio @property def precio(self): return self._precio @precio.setter def precio(self, valor): if valor < 0: raise ValueError("Precio inválido") self._precio = valor
- Diseña una clase
Rectangulo
que tenga atributosancho
yalto
y una propiedadarea
(solo lectura).
Parte 4 — Métodos de instancia y de clase
Objetivo: entender las diferencias entre los métodos de instancia, de clase y estáticos, así como las situaciones prácticas donde cada uno es más apropiado.
1) Métodos de instancia
Son los más comunes. Se definen con def
y su primer parámetro es siempre self
. Pueden acceder y modificar los atributos del objeto.
class Persona:
def __init__(self, nombre, edad):
self.nombre = nombre
self.edad = edad
def saludar(self):
return f"Hola, soy {self.nombre} y tengo {self.edad} años."
p1 = Persona("Ana", 30)
print(p1.saludar())
Este método actúa sobre self
(la instancia concreta). Si modificamos un atributo dentro del método, cambia solo en ese objeto.
p1.edad += 1
print(p1.saludar()) # Hola, soy Ana y tengo 31 años
self
).2) Métodos de clase (@classmethod
)
Un método de clase recibe cls
como primer parámetro en lugar de self
. Se asocia a la clase en sí, no a un objeto particular.
class Empleado:
empresa = "TechCorp"
contador = 0
def __init__(self, nombre):
self.nombre = nombre
Empleado.contador += 1
@classmethod
def total_empleados(cls):
return f"Hay {cls.contador} empleados en {cls.empresa}."
e1 = Empleado("Carlos")
e2 = Empleado("Lucía")
print(Empleado.total_empleados())
Aquí, el método total_empleados()
accede a un atributo de clase (cls.contador
), compartido por todas las instancias. Se puede invocar tanto desde la clase como desde una instancia.
print(e1.total_empleados()) # También válido
Tipo | Primer parámetro | Acceso | Ejemplo de uso |
---|---|---|---|
Instancia | self | Atributos del objeto | p1.saludar() |
Clase | cls | Atributos de clase | Empleado.total_empleados() |
3) Uso práctico de los métodos de clase
Son ideales para crear constructores alternativos o comportamientos que afectan a la clase completa.
class Fecha:
def __init__(self, dia, mes, anio):
self.dia = dia
self.mes = mes
self.anio = anio
@classmethod
def desde_cadena(cls, cadena):
d, m, a = map(int, cadena.split("/"))
return cls(d, m, a) # crea una nueva instancia
f = Fecha.desde_cadena("18/10/2025")
print(f.dia, f.mes, f.anio)
El método desde_cadena()
actúa como un constructor alternativo, muy usado para inicializar objetos desde diferentes formatos o fuentes de datos (archivos, JSON, cadenas, etc.).
4) Métodos estáticos (@staticmethod
)
Un método estático pertenece a la clase, pero no recibe ni self
ni cls
. Es una simple función agrupada dentro de la clase por organización o coherencia semántica.
class Calculadora:
@staticmethod
def sumar(a, b):
return a + b
@staticmethod
def es_par(n):
return n % 2 == 0
print(Calculadora.sumar(3, 7))
print(Calculadora.es_par(10))
Se usan cuando una función tiene relación conceptual con la clase, pero no necesita acceder a los datos del objeto ni a los atributos de clase.
5) Diferencias clave entre los tres tipos
Tipo | Decorador | Primer argumento | Acceso permitido | Ejemplo |
---|---|---|---|---|
Instancia | — | self | Atributos del objeto | obj.metodo() |
Clase | @classmethod | cls | Atributos de clase | Clase.metodo() |
Estático | @staticmethod | — | Ninguno (solo argumentos pasados) | Clase.utilidad(x) |
6) Combinación práctica en una sola clase
Veamos una clase que mezcla los tres tipos para distintos fines.
class Temperatura:
factor_c_f = 9 / 5 # atributo de clase
def __init__(self, celsius):
self.celsius = celsius
def a_fahrenheit(self): # método de instancia
return self.celsius * self.factor_c_f + 32
@classmethod
def desde_fahrenheit(cls, f):
c = (f - 32) / cls.factor_c_f
return cls(c) # devuelve una nueva instancia
@staticmethod
def es_temperatura_valida(valor):
return -273.15 <= valor < 6000
t1 = Temperatura(25)
print(t1.a_fahrenheit())
t2 = Temperatura.desde_fahrenheit(98.6)
print(t2.celsius)
print(Temperatura.es_temperatura_valida(-500))
a_fahrenheit()
: usaself
→ método de instancia.desde_fahrenheit()
: usacls
→ método de clase (constructor alternativo).es_temperatura_valida()
: independiente → método estático.
- Si accedes o modificas datos del objeto → usa
self
. - Si trabajas con la clase en general → usa
@classmethod
. - Si es una utilidad genérica → usa
@staticmethod
.
7) Ejercicios rápidos
- Crea una clase
Circulo
con atributo de clasePI = 3.1416
y método de instanciaarea()
que calcule el área a partir del radio. - Añade un
@classmethod
llamadodesde_diametro()
que cree un círculo a partir de su diámetro. - Añade un
@staticmethod
llamadoes_valido()
que devuelvaTrue
si el radio es positivo.
- Crea una clase
class Circulo:
PI = 3.1416
def __init__(self, radio):
self.radio = radio
def area(self):
return Circulo.PI * (self.radio ** 2)
@classmethod
def desde_diametro(cls, diametro):
return cls(diametro / 2)
@staticmethod
def es_valido(radio):
return radio > 0
8) Tabla resumen
Tipo de método | Acceso a | Decorador | Caso típico |
---|---|---|---|
Instancia | Datos del objeto | (ninguno) | Modificar atributos del objeto |
Clase | Datos de clase | @classmethod | Crear constructores alternativos |
Estático | Ninguno | @staticmethod | Funciones auxiliares relacionadas |
Parte 5 — Métodos especiales (__str__
, __repr__
) y buenas prácticas
Objetivo: aprender a representar objetos de forma legible con los métodos especiales __str__
y __repr__
, y aplicar las mejores prácticas de diseño orientado a objetos en Python.
1) ¿Qué son los métodos especiales?
Los métodos especiales (también llamados métodos mágicos o dunder methods, por su doble guion bajo) son los que comienzan y terminan con __
, como __init__
, __len__
o __str__
. Python los usa internamente para definir comportamientos de los objetos. Puedes sobreescribirlos para personalizar cómo se comportan tus clases.
class Libro:
def __init__(self, titulo, autor):
self.titulo = titulo
self.autor = autor
def __str__(self):
return f"'{self.titulo}' de {self.autor}"
def __repr__(self):
return f"Libro(titulo={self.titulo!r}, autor={self.autor!r})"
l = Libro("1984", "George Orwell")
print(str(l)) # '1984' de George Orwell
print(repr(l)) # Libro(titulo='1984', autor='George Orwell')
2) __str__
— Representación legible para el usuario
Este método define cómo se muestra el objeto cuando se imprime con print()
o se convierte con str()
. Su objetivo es ser amigable y entendible para el usuario final.
class Persona:
def __init__(self, nombre, edad):
self.nombre = nombre
self.edad = edad
def __str__(self):
return f"Persona: {self.nombre}, {self.edad} años"
p = Persona("Ana", 30)
print(p) # Persona: Ana, 30 años
__str__
debe ser simple, claro y orientado a presentación, no a depuración.3) __repr__
— Representación técnica o de depuración
El método __repr__
define cómo se muestra el objeto al usarlo en consola o dentro de listas, diccionarios o depuradores. Su objetivo es ser preciso y sin ambigüedad: idealmente, el texto debería poder recrear el objeto.
class Persona:
def __init__(self, nombre, edad):
self.nombre = nombre
self.edad = edad
def __repr__(self):
return f"Persona({self.nombre!r}, {self.edad!r})"
p = Persona("Carlos", 25)
print(repr(p)) # Persona('Carlos', 25)
Fíjate en el uso de !r
en las f-strings: fuerza el formato de repr()
sobre los valores.
4) Diferencias entre __str__
y __repr__
Método | Propósito | Orientado a | Ejemplo típico |
---|---|---|---|
__str__ | Representación legible para humanos | Usuario final | «Persona: Ana, 30 años» |
__repr__ | Representación técnica sin ambigüedad | Desarrollador / Depuración | «Persona(‘Ana’, 30)» |
5) Cómo interactúan ambos métodos
- Si solo defines
__repr__
, Python lo usará también como__str__
por defecto. - Si defines ambos,
print()
usará__str__
y la consola usará__repr__
.
class Producto:
def __init__(self, nombre, precio):
self.nombre = nombre
self.precio = precio
def __repr__(self):
return f"Producto({self.nombre!r}, {self.precio!r})"
p = Producto("Ratón", 15.99)
print(p) # Producto('Ratón', 15.99)
print([p]) # [Producto('Ratón', 15.99)]
6) Otros métodos especiales útiles
Algunos métodos mágicos relacionados que vale la pena conocer:
Método | Función | Ejemplo de uso |
---|---|---|
__len__ | Define el resultado de len(obj) | len(mi_lista) |
__eq__ | Compara objetos con == | a == b |
__lt__ | Ordena con < , > | p1 < p2 |
__add__ | Define la suma con + | a + b |
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Vector(4, 6)
7) Buenas prácticas con __str__
y __repr__
- Siempre devuelve una cadena (
str
), nuncaprint()
dentro del método. - Haz que
__repr__
sea lo más completo posible, idealmente reconstruible:eval(repr(obj))
debería funcionar si es viable. - Haz que
__str__
sea legible y descriptivo. - No mezcles lógica pesada o llamadas a red en estos métodos: deben ser rápidos.
- Cuando sea posible, usa
!r
en f-strings dentro de__repr__
para garantizar formato técnico.
8) Caso práctico: sistema de inventario
Una clase Producto
con ambos métodos bien definidos:
class Producto:
def __init__(self, codigo, nombre, precio):
self.codigo = codigo
self.nombre = nombre
self.precio = precio
def __str__(self):
return f"{self.nombre} — {self.precio:.2f} €"
def __repr__(self):
return f"Producto({self.codigo!r}, {self.nombre!r}, {self.precio!r})"
productos = [
Producto(101, "Teclado", 29.99),
Producto(102, "Ratón", 15.99)
]
print(productos[0]) # Teclado — 29.99 €
print(productos) # [Producto(101, 'Teclado', 29.99), Producto(102, 'Ratón', 15.99)]
Este patrón se usa en la mayoría de clases del mundo real (modelos, entidades, DTOs, etc.).
9) Ejercicios propuestos
- Implementa una clase
Alumno
connombre
ynota
.__str__
: devuelve"Alumno: Ana (Nota: 8.5)"
__repr__
: devuelve"Alumno('Ana', 8.5)"
- Crea una clase
Rectangulo
con ancho y alto.__repr__
debe devolver un texto que permita reconstruirlo.__str__
debe mostrar su área de forma legible.
- Diseña una clase
Pedido
con atributoscliente
yimporte
.__str__
: “Pedido de {cliente} por {importe}€”__repr__
: “Pedido({cliente!r}, {importe!r})”
10) Resumen final del módulo 5.1
- Una clase define el modelo; los objetos son sus instancias.
- __init__ inicializa; self representa a la instancia actual.
- Hay tres tipos de atributos: instancia, clase y propiedad.
- Hay tres tipos de métodos: instancia, clase (
@classmethod
) y estático (@staticmethod
). __str__
y__repr__
mejoran la legibilidad, la depuración y la calidad del código.