Encapsulación: Taller Integrador
Ya viste la teoría de _, __, getters/setters y @property como tema separado. Este taller no enseña nada nuevo: te da 9 problemas cortos más un reto final para que practiques esas herramientas escribiendo clases de cero, hasta que dejen de sentirse como una receta y se sientan como una costumbre.
Bienvenido
Este taller es la práctica integradora de Encapsulación junto con todo lo anterior: Clases y Objetos, Funciones y el resto de guías de POO. No repite la teoría —si algo no te suena (name mangling, @property, raise), es el momento de volver a repasarlo antes de seguir.
Este taller asume que ya completaste Encapsulación. Ningún ejercicio necesita herencia ni polimorfismo —nada de eso se explicó todavía—, y tampoco necesitás try/except (manejo de excepciones): esta materia todavía no lo vio.
Varios ejercicios te piden provocar un error a propósito (acceder a un atributo privado desde afuera, o asignarle un valor inválido a una property). Ver un AttributeError o un ValueError en rojo en esos casos es el resultado esperado, no una señal de que tu código está mal. Como todavía no viste try/except, no hace falta atraparlo: comprobá que aparece, y seguí.
Cada ejercicio trae un editor vacío: el enunciado te dice qué clase tenés que escribir, y vos escribís el código Python de cero. Presiona ▶ Ejecutar para correrlo de verdad en tu navegador con Pyodide (Python real compilado a WebAssembly). La primera ejecución tarda unos segundos porque descarga el intérprete. Si estás resolviendo el taller desde un lanzamiento de ATENZA, además aparece el botón 📨 Enviar para revisión una vez que el código corra sin errores, para mandar ese ejercicio al docente (no se autocorrige).
En este taller, copiar y pegar código en los editores está deshabilitado (incluido arrastrar y soltar). La idea es que practiques escribiendo la sintaxis vos mismo, no que la traigas de otro lado.
🔒 Convenciones
Practicás _protegido y __privado hasta ver de primera mano qué impide cada uno y qué no.
🛠️ Getters/setters → @property
Escribís el mismo control de datos primero a mano y después con la forma pythonica.
🎯 Reto final
Una transferencia entre dos cuentas bancarias, combinando encapsulación con "pasar un objeto como parámetro".
Zona de pruebas libres
Este bloque queda disponible durante todo el taller. Usalo en cualquier momento para probar una idea suelta, revisar cómo se comporta una clase, o experimentar algo que se te ocurra a mitad de un ejercicio —sin desordenar los bloques numerados.
1. Convención _ y name mangling (__)
Tres problemas para comprobar en carne propia la diferencia real entre un guion bajo (convención, sigue accesible) y dos guiones bajos (name mangling, más difícil de tocar por accidente).
Escribe una clase Libro con __init__(self, titulo) que guarde el atributo como self._titulo (con un solo guion bajo). Agrégale un método mostrar(self) que imprima "Estoy leyendo: " + self._titulo. Crea un objeto, llamá mostrar(), después reasignale _titulo a otro valor desde afuera de la clase, y volvé a llamar mostrar() para comprobar que la convención no impidió nada.
Escribe una clase Mochila con __init__(self, peso_max) que guarde el límite como self.__peso_max (name mangling). Agrégale un método cabe(self, peso) que devuelva True si peso es menor o igual a self.__peso_max, o False en caso contrario. Crea un objeto con peso_max=10 y probá cabe() con dos valores distintos, imprimiendo el resultado de cada uno. Por último, en una línea aparte, intentá print(mochila.__peso_max) para ver el AttributeError (ver el aviso de arriba: es el resultado esperado).
Escribe una clase Termostato con __init__(self, temperatura) que guarde self.__temperatura. Agrégale un método privado __dentro_de_rango(self) que devuelva True si self.__temperatura está entre 15 y 30 (ambos incluidos). Después, un método público ajustar(self, nueva_temperatura) que: guarde el valor viejo en una variable, asigne self.__temperatura = nueva_temperatura, y llame a self.__dentro_de_rango() para comprobar el resultado —si da False, debe volver a dejar self.__temperatura en el valor viejo e imprimir "Fuera de rango permitido". Prueba ajustar() con un valor válido y con uno inválido.
2. Getters y setters clásicos
Tres problemas con el estilo clásico: un método para leer, otro para escribir con validación —sin @property todavía.
Escribe una clase Producto con __init__(self, nombre, precio) que guarde el precio como self.__precio. Agrégale obtener_precio(self) (devuelve el precio) y establecer_precio(self, nuevo_precio): si nuevo_precio es menor o igual a 0, debe imprimir "Error: el precio debe ser mayor a 0" y no cambiar nada; si no, actualiza self.__precio. Prueba establecer_precio() con un valor inválido y con uno válido, imprimiendo obtener_precio() después de cada intento.
Escribe una clase Mascota con __init__(self, nombre, edad) que guarde la edad como self.__edad. Agrégale obtener_edad(self), establecer_edad(self, nueva_edad) (rechaza con un print() cualquier edad negativa), y un método cumplir_anios(self) que llame a self.establecer_edad(self.obtener_edad() + 1) —reutilizando el propio setter en vez de tocar self.__edad directo. Crea un objeto y llamá cumplir_anios() dos veces seguidas, imprimiendo obtener_edad() después de cada una.
Escribe una clase CuentaAhorro con __init__(self, saldo) que guarde self.__saldo. Agrégale obtener_saldo(self), establecer_saldo(self, nuevo_saldo) (rechaza con un print() cualquier saldo negativo), y un método depositar(self, monto) que llame a self.establecer_saldo(self.obtener_saldo() + monto). Crea una cuenta con saldo inicial 100, depositá 50 y 30 por separado, imprimiendo el saldo después de cada depósito.
3. @property y validación con raise
Tres problemas para reescribir el mismo control de datos, ahora con la forma pythonica: se accede como un atributo normal, y un valor inválido detiene el programa con raise ValueError(...) en vez de solo avisar con un print().
Reescribe la clase Producto del ejercicio 2.1 usando @property: una property precio (getter que devuelve self.__precio) y su @precio.setter, que lance raise ValueError("El precio debe ser mayor a 0") si el valor no es positivo. En __init__, asigná el precio con self.precio = precio (a través del setter, no self.__precio directo). Prueba asignando producto.precio = 50 y después producto.precio = -10 (esto último debe detener el programa con el error).
Escribe una clase Estudiante con __init__(self, nombre, nota), donde nota se asigna a través de una property nota. Su @nota.setter debe lanzar raise ValueError("La nota debe estar entre 0 y 100") si el valor está fuera de ese rango. Agrégale un método aprobo(self) que devuelva True si self.nota es mayor o igual a 51. Crea un estudiante con una nota válida, imprimí si aprobó, y después probá asignarle estudiante.nota = 150 para ver el error.
Escribe una clase Termometro con __init__(self, celsius), donde celsius se asigna a través de una property celsius. Su @celsius.setter debe lanzar raise ValueError("La temperatura no puede ser menor al cero absoluto") si el valor es menor a -273.15. Agrégale además una property de solo lectura fahrenheit (sin setter) que calcule self.celsius * 9 / 5 + 32. Crea un objeto con una temperatura válida e imprimí ambas properties.
Reto final: Transferencia entre cuentas
Cierra el taller combinando encapsulación con algo que ya sabés de antes: pasar un objeto como parámetro a un método de otro objeto.
__init__(self, titular, saldo_inicial)— guardatitulary asignasaldo_iniciala través de la property (self.saldo = saldo_inicial).- Una property
saldo(getter) y su@saldo.setter, que lanceraise ValueError("El saldo no puede ser negativo")si el valor es negativo. - 1.
depositar(self, monto)— simontono es positivo,raise ValueError("El monto a depositar debe ser positivo"); si lo es, sumalo al saldo asignandoself.saldo = self.saldo + monto. - 2.
retirar(self, monto)— simontono es positivo o es mayor al saldo disponible,raise ValueError("Fondos insuficientes"); si no, restalo de la misma forma. - 3.
transferir(self, otra_cuenta, monto)— recibe otro objetoCuentaBancariacomo parámetro. Debe llamar aself.retirar(monto)y, si no falló, aotra_cuenta.depositar(monto)—reutilizando los métodos que ya escribiste, sin repetir su lógica.
Pruébala con este código:
cuenta_ana = CuentaBancaria("Ana", 500)
cuenta_beto = CuentaBancaria("Beto", 100)
cuenta_ana.transferir(cuenta_beto, 200)
print(cuenta_ana.saldo)
print(cuenta_beto.saldo)
cuenta_ana.transferir(cuenta_beto, 10000)
300 300 ValueError: Fondos insuficientes
La última línea (cuenta_ana.transferir(cuenta_beto, 10000)) debe detener el programa con ese error —Ana no tiene 10.000 para transferir.
Cheat Sheet del taller
| Herramienta | Para qué la usaste hoy |
|---|---|
self._atributo | Convención: "no tocar desde afuera", pero sigue siendo accesible. |
self.__atributo | Name mangling: Python lo renombra por dentro, más difícil de acceder por accidente. |
def __metodo(self): ... | Método auxiliar interno, pensado para llamarse solo desde otros métodos de la misma clase. |
def obtener_x(self): return self.__x | Getter clásico: leer un atributo privado de forma controlada. |
def establecer_x(self, valor): ... | Setter clásico: validar antes de escribir, con print() si el valor es inválido. |
@property / @x.setter | La misma idea que un getter/setter, pero se accede como objeto.x, sin paréntesis. |
raise ValueError("mensaje") | Rechazar un valor inválido deteniendo el programa, en vez de solo avisar. |
Property sin @x.setter | Solo lectura —útil para un valor calculado, como fahrenheit a partir de celsius. |
metodo(self, otro_objeto) | Pasar un objeto como parámetro a un método de otro objeto (la transferencia del reto final). |
¿Sabías que...?
🔁 Cada cuenta sigue siendo independiente
En el reto final, cuenta_ana y cuenta_beto son dos objetos separados: cuando transferir() modifica el saldo de una a través de self y el de la otra a través del parámetro otra_cuenta, cada una guarda su propio __saldo sin pisar al de la otra.
🧮 Una property puede no guardar nada
fahrenheit del ejercicio 3.3 no tiene ningún self.__fahrenheit guardado —se recalcula cada vez que la leés, a partir de celsius. Una property es un método disfrazado de atributo, así que puede calcular en vez de solo devolver algo guardado.
🏗️ Herencia, la siguiente gran idea
Producto, Estudiante y CuentaBancaria de este taller repiten el mismo patrón de property + setter validado una y otra vez. La herencia va a permitir definir ese patrón una sola vez y que otras clases lo hereden.