🐍 Guía de Aula Invertida
vistas

Encapsulación en Python

Hasta ahora, cualquier atributo de un objeto se lee y se modifica libremente desde afuera: objeto.atributo = lo-que-sea, sin que nada lo impida. La encapsulación es el principio de proteger el estado interno de un objeto, exponiendo solo lo necesario y controlando cómo se lo modifica.

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

Bienvenido

En Clases y objetos ya viste que un objeto guarda sus propios atributos y que un método puede leerlos y modificarlos. Lo que no viste todavía es que, en Python, nada impide por defecto que alguien de afuera de la clase asigne cualquier valor a esos atributos, tenga sentido o no. Esta guía es sobre cómo prevenir eso.

Objetivo del módulo

Al finalizar esta guía vas a saber usar las convenciones _ y __ para señalar y dificultar el acceso externo a un atributo, entender qué es el name mangling, escribir getters y setters clásicos, y usar @property —la forma pythonica de lograr lo mismo sin cambiar cómo se accede al atributo desde afuera— para validar un valor antes de aceptarlo.

💻 Código en vivo

Todos los bloques con el encabezado Python son de solo lectura (para que los escribas a mano en el editor de abajo). El panel editor-panel que sigue a cada uno es donde escribes y ejecutas tu propio código —usa Pyodide, un Python real compilado a WebAssembly. La primera ejecución tarda unos segundos porque descarga el intérprete.

Además del código, esta guía tiene 3 preguntas conceptuales cortas para chequear que entendiste bien el porqué de cada herramienta, no solo su sintaxis.

0 / 0
preguntas y ejercicios enviados

📖 Aprende

Por qué el acceso directo a un atributo es riesgoso, y las convenciones _/__ que usa Python para señalarlo.

💻 Practica

Ejemplos editables y ejecutables en cada sección, construyendo la misma clase paso a paso.

🎯 Domina

Getters/setters clásicos, @property, y validación de datos con raise.

El problema: sin encapsulación

Cualquier atributo creado con self.atributo = valor queda accesible y modificable desde afuera de la clase, sin ninguna restricción. Eso significa que un objeto puede terminar en un estado que no tiene ningún sentido.

Python
class CuentaBancaria:
    def __init__(self, titular, saldo):
        self.titular = titular
        self.saldo = saldo

cuenta = CuentaBancaria("Ana", 100)
cuenta.saldo = -500
print(cuenta.saldo)
✍️ Tu turno: escribe el código y ejecútalo

Salida:

Consola
-500
Python no impide nada por defecto

A diferencia de otros lenguajes con una palabra clave private que el compilador hace cumplir, Python confía en quien usa la clase. Un saldo bancario negativo por una simple asignación no tiene sentido en el mundo real —pero cuenta.saldo = -500 corre sin ningún error. El resto de esta guía muestra las herramientas que Python sí ofrece para prevenir esto: convenciones, y validación de verdad con @property.

Convención de un guion bajo (_protegido)

Anteponer un guion bajo al nombre de un atributo (self._atributo) es la señal que usan los programadores de Python para decir "esto es un detalle interno de la clase, no lo toques desde afuera aunque puedas". Sigue siendo perfectamente accesible.

Python
class CuentaBancaria:
    def __init__(self, titular, saldo):
        self.titular = titular
        self._saldo = saldo  # convención: "no tocar desde afuera"

cuenta = CuentaBancaria("Ana", 100)
print(cuenta._saldo)
cuenta._saldo = -500
print(cuenta._saldo)
✍️ Tu turno: escribe el código y ejecútalo

Salida:

Consola
100
-500
Convención, no candado

Un solo guion bajo no cambia absolutamente nada a nivel técnico: cuenta._saldo se lee y se escribe exactamente igual que cuenta.saldo. Es pura comunicación entre programadores —muchos editores e IDEs incluso marcan en gris o con una advertencia el acceso a un atributo _protegido desde afuera de su clase, pero Python mismo no hace nada para impedirlo.

Convención de doble guion bajo (__privado) y name mangling

Anteponer dos guiones bajos (self.__atributo) activa un mecanismo real del lenguaje llamado name mangling: Python renombra el atributo por dentro a _NombreDeLaClase__atributo, así que acceder con el nombre original desde afuera ya no funciona.

Python
class CuentaBancaria:
    def __init__(self, titular, saldo):
        self.titular = titular
        self.__saldo = saldo

cuenta = CuentaBancaria("Ana", 100)
print(cuenta.__saldo)
✍️ Tu turno: escribe el código y ejecútalo

Salida:

Consola
AttributeError: 'CuentaBancaria' object has no attribute '__saldo'

El atributo sigue existiendo: solo que su nombre real, guardado adentro del objeto, es _CuentaBancaria__saldo. Se puede acceder así, aunque nadie debería hacerlo en código real:

Python
print(cuenta._CuentaBancaria__saldo)
Consola
100
Lo mismo aplica a métodos

La misma convención sirve para métodos: un método def __validar(self): definido dentro de la clase también sufre name mangling, y está pensado como un método auxiliar interno, pensado para ser llamado únicamente desde otros métodos de la misma clase (self.__validar()), no desde afuera.

No es un blindaje real

El name mangling no existe por seguridad: existe principalmente para evitar que un atributo __algo definido en una clase choque por accidente con otro __algo de una subclase que la hereda (algo que vas a ver con más detalle en la guía de Herencia). Como mecanismo para "ocultar" datos es fácil de sortear, como recién viste.

Selección múltiple

Si una clase guarda self.__saldo y, desde afuera de la clase, escribís cuenta.__saldo, ¿qué pasa?

Getters y setters clásicos

Ya que __saldo es difícil de tocar directamente desde afuera, hace falta una forma controlada de leerlo y modificarlo. El estilo clásico —el mismo que usan lenguajes como Java— es escribir un método para leer (getter) y otro para escribir (setter), cada uno con su propio nombre.

Python
class CuentaBancaria:
    def __init__(self, titular, saldo):
        self.titular = titular
        self.__saldo = saldo

    def obtener_saldo(self):
        return self.__saldo

    def establecer_saldo(self, nuevo_saldo):
        if nuevo_saldo < 0:
            print("Error: el saldo no puede ser negativo")
            return
        self.__saldo = nuevo_saldo

cuenta = CuentaBancaria("Ana", 100)
print(cuenta.obtener_saldo())
cuenta.establecer_saldo(-50)
cuenta.establecer_saldo(300)
print(cuenta.obtener_saldo())
✍️ Tu turno: escribe el código y ejecútalo

Salida:

Consola
100
Error: el saldo no puede ser negativo
300
Funciona, pero cambia cómo se usa el atributo

Este código ya protege el saldo de valores inválidos, pero pagó un precio: en vez de leer y escribir con cuenta.saldo, ahora hay que llamar métodos con paréntesis, cuenta.obtener_saldo(). Eso rompe la simetría con el resto de tus clases, donde los atributos se acceden directo. La siguiente sección resuelve exactamente esto.

La forma pythonica: @property

@property es un decorador que convierte un método en algo que se lee como si fuera un atributo, sin paréntesis. Por dentro sigue siendo un método —con toda su lógica—, pero por fuera se usa exactamente igual que un atributo normal.

Python
class CuentaBancaria:
    def __init__(self, titular, saldo):
        self.titular = titular
        self.__saldo = saldo

    @property
    def saldo(self):
        return self.__saldo

cuenta = CuentaBancaria("Ana", 100)
print(cuenta.saldo)
✍️ Tu turno: escribe el código y ejecútalo

Salida:

Consola
100
Por ahora, de solo lectura

Tal como está, @property solo definió la parte de lectura. Si en este momento intentaras cuenta.saldo = 500, Python lanzaría AttributeError: property 'saldo' of 'CuentaBancaria' object has no setter —todavía falta la parte que permite escribir, con validación incluida. Eso es lo que sigue.

Selección múltiple

¿Cuál es la ventaja principal de @property frente a un getter clásico como obtener_saldo()?

Validar en el setter

Con @saldo.setter se agrega la contraparte de escritura: un método con el mismo nombre que se ejecuta automáticamente cada vez que alguien hace cuenta.saldo = valor. Ahí adentro se puede validar antes de aceptar el cambio.

Python
class CuentaBancaria:
    def __init__(self, titular, saldo):
        self.titular = titular
        self.saldo = saldo  # ya pasa por el setter de abajo

    @property
    def saldo(self):
        return self.__saldo

    @saldo.setter
    def saldo(self, nuevo_saldo):
        if nuevo_saldo < 0:
            raise ValueError("El saldo no puede ser negativo")
        self.__saldo = nuevo_saldo

cuenta = CuentaBancaria("Ana", 100)
cuenta.saldo = 300
print(cuenta.saldo)
cuenta.saldo = -50
✍️ Tu turno: escribe el código y ejecútalo

Salida:

Consola
300
ValueError: El saldo no puede ser negativo
self.saldo = saldo, no self.__saldo, dentro de __init__

Fíjate que __init__ asigna con self.saldo = saldo (el nombre de la property), no self.__saldo = saldo directo. Así, la validación del setter corre también al crear el objeto —si alguien intentara CuentaBancaria("Ana", -100), fallaría ahí mismo, en vez de crear una cuenta ya inválida desde el arranque.

Probalo vos mismo

El setter de arriba solo acepta valores mayores o iguales a 0. Si alguien intentara asignar cuenta.saldo = , el setter lo acepta, porque 300 no es menor que 0.

raise, no print + return

Fíjate que el setter usa raise ValueError(...), no print(...) seguido de return como en los getters/setters clásicos de más arriba. La diferencia importa: raise detiene el programa en el acto y obliga a quien llamó al código a enterarse del problema, en vez de dejar pasar un dato inválido en silencio con solo un mensaje impreso.

Selección múltiple

¿Por qué conviene usar raise ValueError(...) en vez de imprimir un mensaje y hacer return dentro de un setter, cuando el valor recibido es inválido?

Errores comunes

❌ Confundir _ con __

Un guion bajo (_atributo) es solo una convención visual: sigue siendo tan accesible como cualquier otro atributo. Dos guiones bajos (__atributo) sí activan el name mangling —más difícil de acceder por accidente, pero tampoco imposible.

❌ print() + return en vez de raise

Rechazar un valor inválido con solo un mensaje impreso deja el programa corriendo como si nada hubiera pasado. Quien llamó al setter nunca se entera de que su dato fue ignorado, a menos que revise la salida a mano.

❌ Definir @property sin su @nombre.setter

Una property sin setter es de solo lectura. Intentar objeto.atributo = valor sobre ella lanza AttributeError: property ... has no setter, incluso si el getter existe y funciona bien.

❌ Pensar que __atributo es invisible de verdad

El name mangling no borra el atributo: solo le cambia el nombre a _Clase__atributo. Alguien que conozca el truco igual puede leerlo o modificarlo desde afuera —no es un mecanismo de seguridad.

Ejercicios Propuestos

Pon en práctica lo aprendido resolviendo los siguientes ejercicios directamente en el bloque editable.

  1. Escribe una clase Persona con un atributo protegido _nombre (asignado en __init__) y un método mostrar(self) que lo imprima. Después, desde afuera, reasígnale _nombre a otro valor y volvé a llamar mostrar() para comprobar que la convención no impidió nada.
  2. Escribe una clase Mascota con un atributo privado __edad, más los métodos clásicos obtener_edad(self) y establecer_edad(self, nueva_edad) —este último debe rechazar con un print() cualquier edad negativa.
  3. Reescribe la clase Mascota del ejercicio anterior usando @property y @edad.setter en vez de los dos métodos, lanzando raise ValueError(...) si la edad es negativa.
  4. Escribe una clase Estudiante con una property nota (de 0 a 100) que valide el rango completo con @nota.setter (raise ValueError fuera de rango), y un método aprobo(self) que devuelva True si la nota es mayor o igual a 51.
  5. Escribe una clase Termometro con un atributo privado __celsius y una property de solo lectura fahrenheit que calcule celsius * 9 / 5 + 32 —sin ningún setter: una property no siempre protege un valor guardado, a veces solo calcula uno nuevo.
✍️ Resuelve aquí los ejercicios

Reto Final

Completa una clase CuentaBancaria más realista que la de los ejemplos, combinando todo lo visto:

  • __init__(self, titular, saldo_inicial) debe guardar titular y asignar saldo_inicial a través de la property (self.saldo = saldo_inicial), para que la validación corra desde el arranque.
  • Una property saldo (getter) y su @saldo.setter, que rechace con raise ValueError(...) cualquier valor negativo.
  • Un método depositar(self, monto): si monto no es positivo, raise ValueError(...); si lo es, súmalo al saldo asignando self.saldo = self.saldo + monto (para que también pase por el setter).
  • Un método retirar(self, monto): si monto no es positivo o es mayor al saldo disponible, raise ValueError("Fondos insuficientes"); si no, réstalo del saldo de la misma forma.

Pruébala con este código:

Python
cuenta = CuentaBancaria("Beto", 200)
cuenta.depositar(100)
print(cuenta.saldo)
cuenta.retirar(50)
print(cuenta.saldo)
cuenta.retirar(1000)
✍️ Resuelve el reto acá
Salida esperada
300
250
ValueError: Fondos insuficientes

La última línea del ejemplo (cuenta.retirar(1000)) debe detener el programa con ese error —no imprimir nada y seguir de largo.

Cheat Sheet

UsoEjemplo
Atributo "protegido" (convención)self._atributo = valor
Atributo "privado" (name mangling)self.__atributo = valor
Acceder al nombre real desde afueraobjeto._Clase__atributo (no recomendado)
Getter clásicodef obtener_x(self): return self.__x
Setter clásicodef establecer_x(self, valor): ...
Property (getter)@property
def x(self): return self.__x
Setter de una property@x.setter
def x(self, valor): ...
Rechazar un valor inválidoraise ValueError("mensaje")
Property de solo lectura@property sin @x.setter

¿Sabías que...?

🔓 "private" no existe en Python

A diferencia de Java, C++ o C#, que tienen una palabra clave private que el compilador hace cumplir de verdad, Python no tiene nada equivalente. La filosofía de la comunidad se resume en una frase famosa: "we're all consenting adults here" (acá todos somos adultos que dieron su consentimiento) —se confía en que el programador respete las convenciones.

🧩 El name mangling piensa en la herencia

Su propósito principal no es ocultar datos: es evitar que un atributo __algo de una clase choque por accidente con otro __algo del mismo nombre definido en una subclase que la hereda —un problema que vas a entender mejor en la guía de Herencia.

🗑️ property también tiene un deleter

Además de @property (leer) y @x.setter (escribir), existe @x.deleter, que se ejecuta cuando alguien hace del objeto.x. Es poco común usarlo, pero existe para completar el trío.