Encapsulamiento y propiedades
¿Qué es?
El encapsulamiento consiste en proteger los datos internos de un objeto para que solo se modifiquen de formas válidas. En lugar de dejar que cualquiera escriba directamente en un atributo, el objeto controla el acceso. Una propiedad (property) es la herramienta de Python para lograrlo sin cambiar la forma en que se usa el objeto desde fuera.
¿Cómo funciona?
En Python no existen atributos verdaderamente privados, pero hay una convención: un nombre que empieza con un guion bajo (_valor) significa "interno, no lo toques desde fuera". Para validar lecturas y escrituras se usa el decorador @property (para leer) y @nombre.setter (para escribir). Desde fuera el atributo se sigue usando como objeto.valor, pero por debajo se ejecuta tu código de validación.
¿Para qué sirve?
Sirve para garantizar invariantes: reglas que siempre deben cumplirse. En el sistema de notas, una nota debe estar entre 0.0 y 5.0. Si el objeto valida eso en un solo lugar, es imposible que un dato inválido entre, sin importar quién lo intente.
Qué pieza del sistema construimos
Construimos la clase Calificacion, que envuelve un valor numérico y garantiza que siempre sea válido (entre 0.0 y 5.0). También endurecemos Estudiante para que rechace notas inválidas. Así el resto del sistema puede confiar en que las notas son correctas sin volver a revisarlas.
El problema sin encapsulamiento
class Calificacion:
def __init__(self, valor):
self.valor = valor
c = Calificacion(4.0)
c.valor = 99 # nadie lo impide; dato inválido dentro del sistema
Cualquiera puede asignar un valor absurdo. Necesitamos un punto único donde validar.
Marcando el atributo como interno
Primero renombramos a _valor (convención de "no público") y movemos la validación a un solo método.
class Calificacion:
def __init__(self, valor):
self._valor = self._validar(valor)
def _validar(self, valor):
if not isinstance(valor, (int, float)):
raise TypeError("La nota debe ser numérica")
if not 0.0 <= valor <= 5.0:
raise ValueError("La nota debe estar entre 0.0 y 5.0")
return float(valor)
El método _validar es interno (empieza con _) y centraliza la regla. Si llega algo inválido, lanza una excepción en vez de guardar basura.
Exponiendo con property
Ahora queremos que desde fuera se lea c.valor (sin guion bajo) y que al asignar también se valide.
class Calificacion:
def __init__(self, valor):
self.valor = valor # esto ya pasa por el setter
@property
def valor(self):
return self._valor
@valor.setter
def valor(self, nuevo):
if not isinstance(nuevo, (int, float)):
raise TypeError("La nota debe ser numérica")
if not 0.0 <= nuevo <= 5.0:
raise ValueError("La nota debe estar entre 0.0 y 5.0")
self._valor = float(nuevo)
Detalle importante: en __init__ escribimos self.valor = valor, no self._valor. Así la asignación inicial también pasa por el setter y se valida. El setter guarda el dato real en self._valor.
c = Calificacion(4.0)
print(c.valor) # 4.0
c.valor = 5.0 # válido, pasa por el setter
# c.valor = 99 # ValueError: la nota debe estar entre 0.0 y 5.0
Una property puede ser de solo lectura: define el @property y omite el setter. Es ideal para valores calculados, como un promedio que no tiene sentido asignar a mano.
Una propiedad calculada en Estudiante
Aprovechamos property para que promedio se vea como un atributo, no como un método con paréntesis, y para que Estudiante rechace notas inválidas usando Calificacion.
class Estudiante:
def __init__(self, nombre):
self.nombre = nombre
self._notas = []
def agregar_nota(self, valor):
self._notas.append(Calificacion(valor)) # valida al entrar
@property
def promedio(self):
if not self._notas:
return 0.0
return sum(c.valor for c in self._notas) / len(self._notas)
Ahora estudiante.promedio se lee sin paréntesis y es imposible meter una nota fuera de rango: Calificacion lo impide en la puerta de entrada.
Error común: dentro del setter de valor, escribir self.valor = nuevo en vez de self._valor = nuevo. Eso vuelve a llamar al setter una y otra vez: recursión infinita. Guarda siempre en el atributo interno _valor.
Tech English: encapsulation = encapsulamiento, property = propiedad, getter / setter = lector / asignador, invariant = invariante (regla que siempre se cumple).
Ejercicios
-
Estudiantecon edad validada. Convierteedaden una propiedad que solo acepte enteros entre 0 y 120. Diseña elgettery elsettery demuestra con un ejemplo que asignar-5lanzaValueError. -
Property de solo lectura. Diseña en
Estudianteuna propiedad calculadaaprobado(de solo lectura) que seaTruesi el promedio es mayor o igual a 3.0. Explica por qué no tiene sentido darle unsetter.