Ir al contenido
wannacode

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 = -500
print(account.balance)
Salida
-500

El 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 = -500

se 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.__balance desaparece, pero account._Account__balance todaví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:

bank.py
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} €")
Salida
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:

Error
AttributeError: property 'balance' of 'Account' object has no setter

Ahora 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
  1. _balance es interno: por convención, el código de fuera no toca el campo. Pasa por los métodos.
  2. El retiro pasa por la comprobación del método: 500 es más que 100. Rechazado, y el saldo se queda como está.
  3. El depósito también se comprueba: 50 es un importe positivo. El método lo suma.
  4. 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)
Respuesta

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 = 4
cat.age = -3
print(cat.age)
Salida
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 = 3
coffee.quantity = 0
print(f"{coffee.name} x{coffee.quantity}")
Salida
Café x3

La 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?

El saldo es interno de la clase. ¿Cómo se llama el campo, por convención?
class Account:
    def __init__(self, balance):
        self. = balance

Pruébalo

Encuentra el error

El campo es _balance, y aun así el saldo pasa a ser -500:

account = Account()
account.balance = 100
account.withdraw(30)
account.balance = -500
print(account.balance)
Salida
-500

¿Qué línea de la clase rompe la protección?

Elige la línea con el error.

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

Salida
María tiene 34 años
Hola, soy María y tengo 34 años

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

El programa
account = Account("Pablo", 50)
account.deposit(25)
account.deposit(-10)
print(account.withdraw(100))
print(account.withdraw(40))
print(f"Saldo: {account.balance} €")
Salida
False
True
Saldo: 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.

El programa
cat = Animal("gato", 3)
cat.age = 4
cat.age = -1
print(f"{cat.species}, {cat.age} años")
Salida
Edad no válida: -1
gato, 4 años

Más tareas

  • Nota. Una clase Grade con 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 Product de 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

1. El campo es _balance. ¿Puede cambiarlo el código de fuera de la clase?
2. ¿Por qué la cuenta no tiene un setter para el saldo?
3. En @age.setter def age(self, value):, ¿qué es value?
4. ¿Qué significa encapsulamiento?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·