El polimorfismo cerró con una advertencia: nada impide escribir odisea.precio = -5 o fausto.stock = -3, y a partir de ahí todos los cálculos de Papyrus mienten. En 05-01 escribiste vender() precisamente para que el stock nunca quedara negativo... pero cualquiera puede saltarse el método y tocar el atributo directamente. La encapsulación es el principio que corrige esto: cada objeto protege sus datos internos y solo permite modificarlos por vías controladas, de modo que sus invariantes — las reglas que siempre deben cumplirse, como "el stock nunca es negativo" — no puedan romperse ni por accidente. Python lo resuelve a su manera: sin candados de verdad, con convenciones claras y una herramienta elegante llamada property.
Contenido
- Qué es encapsular y por qué importa
- La convención del guion bajo:
_protegido - Doble guion bajo y name mangling:
__privado - El problema de los getters/setters al estilo Java
@property: atributos calculados y validados- El setter: validar sin cambiar la interfaz
Productoblindado: la versión final- Errores comunes y consejos
- Ejercicios con soluciones
Qué es encapsular y por qué importa
Encapsular es separar el qué (la interfaz pública: precio_final(), vender()) del cómo (los detalles internos: en qué atributo se guarda el stock, cómo se valida). Los beneficios son concretos:
- Invariantes del negocio garantizadas: en Papyrus, el stock nunca es negativo, el precio nunca es negativo, un código de socio siempre tiene el formato
NOMBRE-NNN. Si el objeto vigila sus propios datos, ninguna parte del programa puede corromperlos. - Libertad para cambiar por dentro: si mañana el stock pasa de un entero a un desglose por almacén, el resto del código no se entera mientras
hay_stock()siga respondiendo igual. - Menos superficie de error: quien usa la clase solo ve lo que debe usar.
Ahora bien, una diferencia cultural importante: Python no tiene atributos privados de verdad (como private en Java o C#). Su filosofía se resume en la frase "somos todos adultos que consienten": el lenguaje señala qué es interno, y confía en que lo respetes.
La convención del guion bajo: _protegido
Un guion bajo inicial marca un atributo o método como interno: "no lo toques desde fuera; puede cambiar sin previo aviso". Es solo una convención — Python no lo impide — pero toda la comunidad la respeta.
class Socio:
"""Un socio del club de Papyrus."""
def __init__(self, nombre, codigo):
self.nombre = nombre
self._codigo = codigo # interno: no manipular desde fuera
self._compras = [] # interno: historial de compras
def registrar_compra(self, importe):
self._compras.append(importe) # única vía legítima de modificarlo
def total_gastado(self):
return round(sum(self._compras), 2)
luis = Socio("Luis", "LUIS-001")
luis.registrar_compra(12.35)
luis._compras.append(-999) # Python NO lo impide... pero está mal vistoLa última línea funciona, y ahí está la clave: _compras es una señal de tráfico, no un muro. Los buenos linters y revisores de código la marcarán, y ese contrato social es sorprendentemente eficaz.
Doble guion bajo y name mangling: __privado
Dos guiones bajos iniciales (sin dos finales) activan el name mangling (retorcimiento de nombres): Python renombra el atributo internamente a _NombreClase__atributo.
class Socio:
def __init__(self, nombre, codigo):
self.nombre = nombre
self.__codigo = codigo
luis = Socio("Luis", "LUIS-001")
print(luis.__codigo) # AttributeError: 'Socio' object has no attribute '__codigo'
print(luis._Socio__codigo) # LUIS-001 → ¡sigue siendo accesible!Puntos que debes tener claros:
- No es seguridad real: cualquiera que conozca el truco accede con
_Socio__codigo. Es ocultación superficial, no cifrado ni protección. - Su propósito genuino es evitar colisiones de nombres en herencia: si
Productousa__contadory una subclase define otro__contador, el mangling los mantiene separados (_Producto__contadorvs_Libro__contador). - En el día a día, la comunidad Python prefiere el guion bajo simple. Reserva
__privadopara cuando escribas clases pensadas para ser heredadas por desconocidos.
| Notación | Nombre real | Significado | ¿Bloquea el acceso? |
|---|---|---|---|
precio |
precio |
Público: úsalo con libertad | No |
_precio |
_precio |
Interno por convención | No (contrato social) |
__precio |
_Clase__precio |
Interno + antichoque en herencia | No (solo lo disfraza) |
__precio__ |
__precio__ |
Reservado para métodos mágicos (05-05) | No los inventes tú |
El problema de los getters/setters al estilo Java
En lenguajes con privacidad estricta, la receta clásica es: atributo privado + método get + método set. Trasladada literalmente a Python queda así:
class LibroJavero:
def __init__(self, titulo, precio):
self._titulo = titulo
self._precio = precio
def get_precio(self):
return self._precio
def set_precio(self, valor):
if valor < 0:
raise ValueError("El precio no puede ser negativo")
self._precio = valor
libro = LibroJavero("Hamlet", 9.95)
libro.set_precio(10.50) # verboso
print(libro.get_precio()) # y poco natural en PythonValida, sí, pero al precio de una interfaz fea y de romper todo el código existente que hacía libro.precio. Python tiene algo mejor.
@property: atributos calculados y validados
Una property es un método disfrazado de atributo: se lee como libro.precio (sin paréntesis), pero por debajo ejecuta código. Es un decorador de la biblioteca estándar (los decoradores en general se estudian en el módulo 8; aquí basta con saber usarlo).
class Libro:
def __init__(self, titulo, precio, stock=0):
self.titulo = titulo
self._precio = precio # el dato real vive en el atributo interno
self._stock = stock
@property
def precio(self):
"""Precio base del libro, en euros."""
return self._precio
odisea = Libro("La Odisea", 12.50, 4)
print(odisea.precio) # 12.50 — se LEE como atributo, sin ()
odisea.precio = 13.00 # AttributeError: property 'precio' has no setterSin setter, la property crea un atributo de solo lectura: ya nadie puede asignar un precio por las bravas. Las properties también sirven para atributos calculados que no se almacenan:
@property
def precio_con_iva(self):
return round(self._precio * 1.04, 2)
print(odisea.precio_con_iva) # 13.00 — calculado al vuelo, siempre coherenteEl setter: validar sin cambiar la interfaz
Para permitir la asignación con control, se añade el setter con @nombre.setter:
@precio.setter
def precio(self, valor):
if valor < 0:
raise ValueError(f"Precio negativo no permitido: {valor}")
self._precio = valorAhora la validación es automática y la sintaxis sigue siendo natural:
odisea.precio = 13.50 # OK: pasa por el setter y valida
odisea.precio = -5 # ValueError: Precio negativo no permitido: -5raise ValueError(...) detiene el programa con un mensaje claro; ya lo usaste conceptualmente al validar entradas, y el módulo 7 enseñará a capturar estos errores con try/except. Lo importante hoy: el objeto rechaza el dato inválido en el momento exacto en que intenta entrar.
| Estilo Java en Python | Estilo pythónico (@property) |
|
|---|---|---|
| Leer | libro.get_precio() |
libro.precio |
| Escribir | libro.set_precio(13.5) |
libro.precio = 13.5 |
| Validación | Sí, en set_precio |
Sí, en el setter |
| ¿Rompe el código que usaba el atributo? | Sí, hay que reescribirlo | No: misma sintaxis de siempre |
| Solo lectura | Omitir set_precio (pero _precio sigue a la vista) |
Property sin setter |
| Veredicto | Evitar | Preferir |
La última fila de "¿rompe el código?" esconde la gran ventaja estratégica: puedes empezar con un atributo público normal (self.precio) y convertirlo en property años después, cuando haga falta validar, sin tocar ni una línea del código cliente. Por eso en Python no se escriben getters/setters preventivos "por si acaso", como exige la ortodoxia Java.
Producto blindado: la versión final
Aplicamos todo a la clase base de la jerarquía (05-02). Invariantes a proteger: precio ≥ 0, stock entero ≥ 0.
class Producto:
"""Artículo de Papyrus con invariantes protegidas."""
IVA_LIBROS = 0.04
DESCUENTO_SOCIO = 0.05
def __init__(self, titulo, precio, stock=0):
self.titulo = titulo
self.precio = precio # ¡pasa por el setter también desde __init__!
self.stock = stock # ídem: valida desde el primer momento
@property
def precio(self):
return self._precio
@precio.setter
def precio(self, valor):
if valor < 0:
raise ValueError(f"Precio negativo no permitido: {valor}")
self._precio = valor
@property
def stock(self):
return self._stock
@stock.setter
def stock(self, valor):
if not isinstance(valor, int):
raise ValueError(f"El stock debe ser un entero, no {type(valor).__name__}")
if valor < 0:
raise ValueError(f"Stock negativo no permitido: {valor}")
self._stock = valor
def vender(self, unidades=1):
"""Única vía recomendada para descontar stock."""
if unidades > self._stock:
return False
self._stock -= unidades
return True
def precio_final(self, socio=False):
descuento = Producto.DESCUENTO_SOCIO if socio else 0
return round(self._precio * (1 - descuento) * (1 + Producto.IVA_LIBROS), 2)Dos sutilezas de oro en este código:
__init__asigna aself.precio, no aself._precio: así la validación se aplica también al construir.Producto("Fantasma", -3, 1)falla inmediatamente, que es donde debe fallar.- Las subclases heredan el blindaje gratis:
Libro,RevistayLibroDigitalno cambian ni una línea y sus precios y stocks quedan protegidos, porque sussuper().__init__(...)desembocan en estos setters.
fausto = Libro("Fausto", "Goethe", 21.00, 0)
fausto.stock = -3 # ValueError: Stock negativo no permitido: -3
fausto.stock = 2.5 # ValueError: El stock debe ser un entero, no float
fausto.stock = 5 # OK: llegaron ejemplares — los de la cola de reservas, atentos
fausto.vender(2) # True
print(fausto.stock) # 3Errores Comunes y Consejos
- Recursión infinita en la property: escribir
return self.preciodentro del getter deprecio(oself.precio = valoren su setter) llama a la property una y otra vez hastaRecursionError. Dentro del getter/setter, usa siempre el atributo internoself._precio. - Validar en el setter pero asignar
self._precioen__init__: el objeto nace sin validar. Asigna en__init__al nombre público (self.precio = precio) para que el setter actúe desde el primer segundo. - Creer que
__privadoprotege datos sensibles: el name mangling se salta conobj._Clase__atributo; no es un mecanismo de seguridad, solo de higiene de nombres en herencia. - Escribir getters/setters
get_x()/set_x()preventivos para todo: es ruido heredado de Java. Atributo público primero; property cuando (y solo cuando) necesites validar o calcular. - Olvidar el
@propertyencima del getter y dejar solo@precio.setter:NameError, porque el setter se define a partir de la property existente. El orden es: primero@property, después@nombre.setter. - Consejo: decide qué es interfaz pública (lo que documentas:
titulo,precio,stock,vender(),precio_final()) y marca el resto con_. Una clase con pocas puertas de entrada es una clase fácil de razonar y de mantener.
Ejercicios
Ejercicio 1: título de solo lectura
En Papyrus, el título de un producto no debe cambiar una vez creado (es su identidad en el catálogo). Convierte titulo en una property sin setter que además valide en __init__ (a través de un método interno o del propio guardado) que no sea una cadena vacía tras strip(). Comprueba que odisea.titulo = "Otro" falla y que Producto(" ", 5.0) lanza ValueError.
Ejercicio 2: el código de socio blindado
Crea la clase Socio con nombre público y una property codigo con getter y setter. El setter debe validar el formato NOMBRE-NNN de Papyrus: la parte izquierda alfabética y en mayúsculas, un guion, y tres dígitos (repasa split(), isalpha(), isdigit(), isupper() de 04-05). "LUIS-001" y "MARTA-002" deben pasar; "luis-001", "LUIS001" y "LUIS-1" deben lanzar ValueError.
Ejercicio 3: reservas encapsuladas
Añade a Producto un atributo interno _reservas (lista) y los métodos reservar(cliente) y atender_reserva(). reservar solo se permite si NO hay stock (si hay, que devuelva el aviso "Hay stock: compra directa"); atender_reserva saca al primer cliente de la lista (orden de llegada) o devuelve None si no hay reservas. Simula: Fausto con stock 0, reservan Júlia y Omar, llega stock, se atiende a Júlia.
Soluciones
Solución 1:
class Producto:
def __init__(self, titulo, precio, stock=0):
titulo = titulo.strip()
if not titulo: # truthiness (02-01): "" es falso
raise ValueError("El título no puede estar vacío")
self._titulo = titulo
self.precio = precio
self.stock = stock
@property
def titulo(self):
return self._titulo
# ... resto de properties como en la lección ...
odisea = Producto("La Odisea", 12.50, 4)
odisea.titulo = "Otro" # AttributeError: property 'titulo' has no setter
Producto(" ", 5.0) # ValueError: El título no puede estar vacíoSin setter, la property es de solo lectura: la identidad del producto queda sellada al nacer.
Solución 2:
class Socio:
def __init__(self, nombre, codigo):
self.nombre = nombre
self.codigo = codigo # pasa por el setter
@property
def codigo(self):
return self._codigo
@codigo.setter
def codigo(self, valor):
partes = valor.split("-")
valido = (
len(partes) == 2
and partes[0].isalpha() and partes[0].isupper()
and len(partes[1]) == 3 and partes[1].isdigit()
)
if not valido:
raise ValueError(f"Código de socio inválido: {valor!r} (formato NOMBRE-NNN)")
self._codigo = valor
Socio("Luis", "LUIS-001") # OK
Socio("Luis", "luis-001") # ValueError (minúsculas)
Socio("Luis", "LUIS-1") # ValueError (faltan dígitos)Es la misma regla que es_socio_valido() de papyrus_utils.py, pero ahora vive donde debe: en el objeto que guarda el dato, imposible de olvidar.
Solución 3:
class Producto:
def __init__(self, titulo, precio, stock=0):
self.titulo = titulo
self.precio = precio
self.stock = stock
self._reservas = [] # interno: solo se toca vía métodos
def reservar(self, cliente):
if self.stock > 0:
return "Hay stock: compra directa"
self._reservas.append(cliente)
return f"{cliente} apuntado ({len(self._reservas)} en cola)"
def atender_reserva(self):
if self._reservas:
return self._reservas.pop(0) # primero en llegar, primero servido
return None
fausto = Producto("Fausto", 21.00, 0)
print(fausto.reservar("Júlia")) # Júlia apuntado (1 en cola)
print(fausto.reservar("Omar")) # Omar apuntado (2 en cola)
fausto.stock = 3 # llegan ejemplares (validado por el setter)
print(fausto.atender_reserva()) # JúliaLa lista _reservas nunca se manipula desde fuera: el orden de llegada está garantizado por construcción. (Para colas largas, recuerda que deque de 04-06 hace popleft() en tiempo constante; aquí pop(0) basta.)
Conclusión
La encapsulación ha cerrado la grieta que el polimorfismo dejó a la vista: _interno señala lo que no debe tocarse, __privado añade name mangling (higiene de nombres, nunca seguridad real), y @property con su @setter consigue lo mejor de dos mundos — la sintaxis natural de un atributo con la validación de un método. La clase Producto de Papyrus ya no admite precios negativos ni stocks imposibles, y todas sus subclases heredan el blindaje sin cambiar una línea. Con datos unidos a su comportamiento (05-01), organizados en jerarquía (05-02), polimórficos (05-03) y protegidos (05-04), solo falta que tus objetos hablen el idioma nativo de Python: que print(odisea) muestre algo digno, que sorted(catalogo) ordene libros sin key=, que libro in carrito simplemente funcione. Todo eso lo conceden los métodos mágicos, y con ellos saldaremos por fin el gancho pendiente desde 05-01: el print "feo". Es la próxima lección.
Curso de Programación en Python
Módulo 1: Introducción a Python
- Introducción a Python
- Configuración del Entorno de Desarrollo
- Sintaxis de Python y Tipos de Datos Básicos
- Variables y Constantes
- Entrada y Salida Básica
- Entornos Virtuales y Gestión de Paquetes
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for y while
- Herramientas de Control de Flujo
- Comprensiones de Listas
Módulo 3: Funciones y Módulos
- Definición de Funciones
- Argumentos de Función
- Funciones Lambda
- Módulos y Paquetes
- Visión General de la Biblioteca Estándar
Módulo 4: Estructuras de Datos
Módulo 5: Programación Orientada a Objetos
Módulo 6: Manejo de Archivos
- Lectura y Escritura de Archivos
- Trabajo con Archivos CSV
- Manejo de Datos JSON
- Operaciones con Archivos y Directorios
Módulo 7: Manejo de Errores y Excepciones
- Introducción a las Excepciones
- Manejo de Excepciones
- Lanzamiento de Excepciones
- Excepciones Personalizadas
- Buenas Prácticas y Registro de Errores con logging
Módulo 8: Temas Avanzados
- Anotaciones de Tipos (type hints)
- Decoradores
- Generadores
- Administradores de Contexto
- Concurrencia: Hilos y Procesos
- Asyncio para Programación Asíncrona
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con unittest
- Pruebas con pytest
- Desarrollo Guiado por Pruebas
- Técnicas de Depuración
- Uso de pdb para Depuración
Módulo 10: Desarrollo Web con Python
- Introducción al Desarrollo Web
- Fundamentos del Framework Flask
- Construcción de APIs REST con Flask
- Introducción a Django
- Construcción de Aplicaciones Web con Django
Módulo 11: Ciencia de Datos con Python
- Introducción a la Ciencia de Datos
- NumPy para Computación Numérica
- Pandas para Manipulación de Datos
- Matplotlib para Visualización de Datos
- Introducción al Aprendizaje Automático con scikit-learn
