Encapsulamiento
En resumen: El encapsulamiento significa que los datos de un objeto se cambian solo a través de sus métodos: en Python, con la convención _nombre y propiedades con @property.
Problema
Menos 500 €
En la lección anterior pusimos la comprobación del retiro en un método. Pero el campo está abierto, y cualquiera puede escribir en él directamente:
account = Account("María", 100)account.withdraw(500)account.balance = -500print(account.balance)-500El método se negó con razón a retirar 500 €. La línea siguiente simplemente fijó el saldo a mano. La comprobación existe, pero se puede saltar. Necesitamos una forma de decir «solo la cuenta toca el saldo».
La idea
_balance: interno de la clase
Pon un guion bajo delante del nombre del campo: _balance.
class Account: def __init__(self, owner, balance): self._owner = owner self._balance = balance
# ...Python no tiene private. El guion bajo es una convención entre programadores: «esto es interno de la clase, no lo toques desde fuera». Python no detiene a nadie. La línea
account._balance = -500se ejecuta, y el saldo pasa a ser -500. Pero quien la lee ve que se rompe la regla (PyCharm, por ejemplo, la subraya). El código de fuera de la clase usa solo los métodos de la clase.
La clase oculta sus datos y los muestra solo a través de sus propios métodos. Esto se llama encapsulamiento. Java y C# tienen palabras clave para ello, los modificadores de acceso (private, public). En Python, ese papel lo cumple el guion bajo.
Más precisamente, para curiosos
- Con dos guiones bajos (
self.__balance), Python renombra el campo a_Account__balance. Desde fuera,account.__balancedesaparece, peroaccount._Account__balancetodavía llega a él. Esto protege de un choque de nombres accidental en las subclases, no de tocar el campo a propósito. Normalmente basta con un guion bajo. - La convención también sirve para los métodos:
_check()es un método pensado solo para la propia clase.
La idea
Leer sin tocar
Desde fuera, ahora no deberíamos ni siquiera leer el saldo. Así que la clase ofrece un método que solo lo devuelve:
class Account: def __init__(self, owner, balance): self._owner = owner self._balance = balance
@property def owner(self): return self._owner
@property def balance(self): return self._balance
def deposit(self, amount): if amount > 0: self._balance += amount
def withdraw(self, amount): if amount > 0 and amount <= self._balance: self._balance -= amount return True return False
account = Account("María", 100)account.withdraw(30)account.deposit(50)print(f"{account.owner}: {account.balance} €")María: 120 €balance es una propiedad. @property encima del método significa que desde fuera la lees como un campo: account.balance, sin paréntesis. Por dentro devuelve el valor de _balance. Este es el getter de Python. El saldo cambia solo a través de deposit y withdraw, y ellos hacen las comprobaciones.
En una propiedad sin setter no se puede escribir. La línea account.balance = -500 se detiene con:
AttributeError: property 'balance' of 'Account' object has no setterAhora withdraw devuelve True o False: si el retiro salió bien. Así quien retira sabe si funcionó.
account = Account("María", 100)account.withdraw(500)account.deposit(50)print(account.balance)Paso a paso, en palabras
- _balance es interno: por convención, el código de fuera no toca el campo. Pasa por los métodos.
- El retiro pasa por la comprobación del método: 500 es más que 100. Rechazado, y el saldo se queda como está.
- El depósito también se comprueba: 50 es un importe positivo. El método lo suma.
- La propiedad balance entrega una copia del saldo. El campo _balance en sí se queda dentro.
Pruébalo
Rechazado
¿Qué imprimirá el programa?
account = Account("María", 100)print(account.withdraw(150))account.deposit(-20)print(account.balance)150 € es más que el saldo, así que withdraw devuelve False y no toca el saldo. El depósito de -20 € también se rechaza, porque el importe tiene que ser positivo. El saldo se queda en 100.
La idea
Setter: un cambio con comprobación
A veces el código de fuera necesita cambiar un campo, pero no a cualquier valor. La edad de un animal puede cambiar, pero no puede ser negativa:
class Animal: def __init__(self): self._age = 0
@property def age(self): return self._age
@age.setter def age(self, value): if value >= 0: self._age = value
cat = Animal()cat.age = 4cat.age = -3print(cat.age)4@age.setter crea un setter para la propiedad age: un método con el mismo nombre que se llama con cat.age = -3. value es el valor a la derecha de =, aquí -3. La comprobación falla y la edad se queda en 4. Desde fuera, la escritura parece la de un campo normal, pero pasa por la comprobación.
La idea
¿Qué campo lleva setter?
No todos los campos necesitan un setter, y algunos ni siquiera un getter. Piensa en lo que el código de fuera debería poder hacer con el producto:
- el nombre y el precio se fijan al crear el producto y después solo se leen: solo getter;
- la cantidad en el carrito cambia, pero es como mínimo 1: un getter y un setter con comprobación.
class Product: def __init__(self, name, price): self._name = name self._price = price self._quantity = 1
@property def name(self): return self._name
@property def price(self): return self._price
@property def quantity(self): return self._quantity
@quantity.setter def quantity(self, value): if value >= 1: self._quantity = value
coffee = Product("Café", 7.50)coffee.quantity = 3coffee.quantity = 0print(f"{coffee.name} x{coffee.quantity}")Café x3La regla es: empieza con _ para todo y abre solo lo que necesitas. Un setter que guarda cualquier cosa sin comprobar trae de vuelta el problema de la primera tarjeta.
Pruébalo
¿Qué falta?
class Account:
def __init__(self, balance):
self. = balance_balance: el guion bajo dice «interno de la clase, no lo toques desde fuera».Pruébalo
Encuentra el error
El campo es _balance, y aun así el saldo pasa a ser -500:
account = Account()account.balance = 100account.withdraw(30)account.balance = -500print(account.balance)-500¿Qué línea de la clase rompe la protección?
Elige la línea con el error.
El setter bajo @balance.setter guarda cualquier valor sin comprobación. El campo es interno, pero el setter lo vuelve a abrir. Quítalo y fija el saldo inicial en __init__. Después solo cambia a través de deposit y withdraw.
Ejercicio
Datos ocultos
Resuelve las tareas en tu editor. Todos los campos empiezan con un guion bajo.
1. Persona. Person con un nombre y una edad, un constructor, propiedades de solo lectura name y age y introduce().
María tiene 34 añosHola, soy María y tengo 34 años2. Una cuenta sin números rojos. Account con un titular y un saldo. Un depósito acepta solo un importe positivo. Un retiro devuelve si salió bien y nunca deja que el saldo baje de 0. El código de fuera puede leer el saldo, pero no escribirlo.
account = Account("Pablo", 50)account.deposit(25)account.deposit(-10)print(account.withdraw(100))print(account.withdraw(40))print(f"Saldo: {account.balance} €")FalseTrueSaldo: 35 €3. Animal. Animal con una especie y una edad. La especie se fija en el constructor y después solo se lee. La edad tiene un setter (@age.setter) que rechaza un número negativo e imprime un mensaje. El constructor también pasa por la misma comprobación: en __init__ escribe self.age = age, sin el guion bajo.
cat = Animal("gato", 3)cat.age = 4cat.age = -1print(f"{cat.species}, {cat.age} años")Edad no válida: -1gato, 4 añosMás tareas
- Nota. Una clase
Gradecon el nombre de una asignatura y una nota de 2 a 6. El setter rechaza todo lo que está fuera de esos límites. Pruébalo con 5, 7 y 1. - Producto con descuento. Añade al
Productde la lección un método que aplica un descuento en porcentaje y acepta solo del 1 al 90 %. El precio no tiene setter.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Parámetros y return · Clases y objetos · Constructores y self · Métodos de instancia
- Continúa en: Lo que comparten todos los objetos: atributos de clase · Herencia · Polimorfismo · Clases abstractas
- Ejercicios sobre el tema: Clases y objetos