🧪 Taller de Repaso
vistas

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.

RC
Ing. Roy Carrasco
Facultad de Ingeniería de Sistemas · UAB

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.

📎 Prerrequisito y alcance

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.

⚠️ A veces tu código va a "fallar" a propósito

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í.

💻 Código en vivo

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).

✋ Se escribe a mano

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.

🧪 Escribe cualquier código Python

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).

Enunciado — 1.1: Libro protegido

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.

✍️ Ejercicio 1.1 — Libro protegido
Enunciado — 1.2: Mochila con límite privado

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).

✍️ Ejercicio 1.2 — Mochila con límite privado
Enunciado — 1.3: Termostato con método privado

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.

✍️ Ejercicio 1.3 — Termostato con método privado

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.

Enunciado — 2.1: Producto con precio protegido

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.

✍️ Ejercicio 2.1 — Producto con precio protegido
Enunciado — 2.2: Mascota que cumple años

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.

✍️ Ejercicio 2.2 — Mascota que cumple años
Enunciado — 2.3: Cuenta de ahorro con depósito

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.

✍️ Ejercicio 2.3 — Cuenta de ahorro con 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().

Enunciado — 3.1: Producto, versión @property

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).

✍️ Ejercicio 3.1 — Producto, versión @property
Enunciado — 3.2: Estudiante con nota validada

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.

✍️ Ejercicio 3.2 — Estudiante con nota validada
Enunciado — 3.3: Termómetro con conversión de solo lectura

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.

✍️ Ejercicio 3.3 — Termómetro con conversión de solo lectura

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.

Clase que tenés que escribir
  • __init__(self, titular, saldo_inicial) — guarda titular y asigna saldo_inicial a través de la property (self.saldo = saldo_inicial).
  • Una property saldo (getter) y su @saldo.setter, que lance raise ValueError("El saldo no puede ser negativo") si el valor es negativo.
  • 1. depositar(self, monto) — si monto no es positivo, raise ValueError("El monto a depositar debe ser positivo"); si lo es, sumalo al saldo asignando self.saldo = self.saldo + monto.
  • 2. retirar(self, monto) — si monto no 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 objeto CuentaBancaria como parámetro. Debe llamar a self.retirar(monto) y, si no falló, a otra_cuenta.depositar(monto) —reutilizando los métodos que ya escribiste, sin repetir su lógica.

Pruébala con este código:

Python
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)
🏆 Resuelve aquí el reto
Salida esperada
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

HerramientaPara qué la usaste hoy
self._atributoConvención: "no tocar desde afuera", pero sigue siendo accesible.
self.__atributoName 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.__xGetter 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.setterLa 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.setterSolo 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.