Python
Tutorial Python: Encapsulación
Aprende encapsulación en Python con atributos privados, getters, setters, propiedades y métodos privados para código seguro y mantenible.
Aprende Python y certifícateAtributos privados
En Python, la encapsulación permite controlar el acceso a los datos internos de una clase. Imagina una clase como una caja fuerte que contiene información valiosa: necesitamos mecanismos para proteger esa información y controlar cómo se accede a ella.
A diferencia de otros lenguajes como Java o C++, Python sigue una filosofía de "somos todos adultos aquí", lo que significa que no impone restricciones estrictas de acceso. Sin embargo, proporciona convenciones para indicar que ciertos atributos deben tratarse como privados.
Convención de nombres para atributos privados
En Python, la convención para marcar un atributo como privado es anteponer un guion bajo (_
) al nombre del atributo:
class CuentaBancaria:
def __init__(self, titular, saldo_inicial):
self._titular = titular
self._saldo = saldo_inicial
def depositar(self, cantidad):
if cantidad > 0:
self._saldo += cantidad
return True
return False
En este ejemplo, _titular
y _saldo
son atributos que se consideran privados por convención. Esto significa que:
- Son detalles de implementación interna
- No deberían ser accedidos directamente desde fuera de la clase
- Podrían cambiar en futuras versiones de la clase
Sin embargo, esta convención es solo un acuerdo entre programadores. Técnicamente, aún es posible acceder a estos atributos desde fuera de la clase:
cuenta = CuentaBancaria("Ana García", 1000)
# Esto funciona, pero no es recomendable
print(cuenta._saldo) # Imprime: 1000
Atributos "realmente" privados con doble guion bajo
Para casos donde necesitamos un nivel mayor de protección, Python ofrece un mecanismo llamado name mangling (desfiguración de nombres) usando doble guion bajo (__
):
class CuentaBancaria:
def __init__(self, titular, saldo_inicial, pin):
self._titular = titular
self._saldo = saldo_inicial
self.__pin = pin # Atributo "realmente" privado
def validar_pin(self, pin_ingresado):
return self.__pin == pin_ingresado
Cuando usamos __pin
, Python renombra internamente este atributo a _CuentaBancaria__pin
. Esto hace más difícil (aunque no imposible) acceder al atributo desde fuera de la clase:
cuenta = CuentaBancaria("Ana García", 1000, "1234")
# Esto generará un AttributeError
try:
print(cuenta.__pin)
except AttributeError as e:
print(f"Error: {e}")
# Esto funciona, pero requiere conocer el mecanismo interno
print(cuenta._CuentaBancaria__pin) # Imprime: 1234
Cuándo usar atributos privados
Los atributos privados son útiles en varias situaciones:
- Proteger la integridad de los datos: Cuando un atributo debe mantener ciertas restricciones (como un saldo que nunca debe ser negativo)
- Ocultar detalles de implementación: Para poder cambiar la implementación interna sin afectar el código que usa la clase
- Evitar conflictos de nombres: Especialmente en herencia, para evitar que las subclases sobrescriban accidentalmente atributos importantes
Ejemplo práctico: Validación de datos
Un caso de uso común para atributos privados es cuando necesitamos validar los datos antes de asignarlos:
class Producto:
def __init__(self, nombre, precio):
self._nombre = nombre
# Validamos el precio antes de asignarlo
if precio < 0:
raise ValueError("El precio no puede ser negativo")
self._precio = precio
# Los métodos para acceder y modificar vendrán en la siguiente sección
Atributos privados vs. protegidos
En la terminología de la programación orientada a objetos:
- Atributos privados (
__nombre
): Solo accesibles dentro de la propia clase - Atributos protegidos (
_nombre
): Accesibles dentro de la clase y sus subclases
class Vehiculo:
def __init__(self, marca, modelo):
self._marca = marca # Protegido (convención)
self.__modelo = modelo # Privado (name mangling)
class Coche(Vehiculo):
def __init__(self, marca, modelo, puertas):
super().__init__(marca, modelo)
self._puertas = puertas
def info(self):
# Podemos acceder a _marca (protegido)
print(f"Marca: {self._marca}")
# Esto generará un AttributeError
try:
print(f"Modelo: {self.__modelo}")
except AttributeError:
print("No se puede acceder a __modelo desde la subclase")
Buenas prácticas con atributos privados
- Usa un solo guion bajo (
_nombre
) para la mayoría de los casos - Reserva el doble guion bajo (
__nombre
) para evitar conflictos en la herencia o cuando realmente necesitas protección adicional - Documenta claramente qué atributos son parte de la interfaz pública y cuáles son detalles de implementación
- Proporciona métodos para acceder y modificar atributos privados cuando sea necesario (veremos esto en la siguiente sección sobre getters y setters)
Los atributos privados son el primer paso para implementar una encapsulación efectiva en Python. Aunque el lenguaje no impone restricciones estrictas, seguir estas convenciones mejora significativamente la calidad y mantenibilidad del código.
Getters y setters
Una vez que hemos definido atributos privados en nuestras clases, necesitamos una forma controlada de acceder y modificar estos datos. Aquí es donde entran en juego los getters y setters, que son métodos especiales diseñados para leer y escribir atributos privados de manera segura.
¿Por qué usar getters y setters?
Los getters y setters nos permiten:
- Validar datos antes de asignarlos
- Calcular valores derivados bajo demanda
- Controlar el acceso a los atributos internos
- Modificar la implementación interna sin afectar el código cliente
En Python, los getters y setters se implementan como métodos normales, siguiendo una convención de nombres clara:
class Persona:
def __init__(self, nombre, edad):
self._nombre = nombre
self._edad = edad
# Getter para nombre
def get_nombre(self):
return self._nombre
# Setter para nombre
def set_nombre(self, nuevo_nombre):
if isinstance(nuevo_nombre, str) and len(nuevo_nombre) > 0:
self._nombre = nuevo_nombre
else:
raise ValueError("El nombre debe ser una cadena no vacía")
# Getter para edad
def get_edad(self):
return self._edad
# Setter para edad
def set_edad(self, nueva_edad):
if isinstance(nueva_edad, int) and 0 <= nueva_edad <= 120:
self._edad = nueva_edad
else:
raise ValueError("La edad debe ser un entero entre 0 y 120")
Uso de getters y setters
Veamos cómo se utilizan estos métodos:
# Crear una instancia
ana = Persona("Ana López", 29)
# Usar getters para acceder a los datos
print(ana.get_nombre()) # Ana López
print(ana.get_edad()) # 29
# Usar setters para modificar los datos
ana.set_nombre("Ana María López")
ana.set_edad(30)
# Verificar los cambios
print(ana.get_nombre()) # Ana María López
print(ana.get_edad()) # 30
# Intentar asignar un valor inválido
try:
ana.set_edad(-5)
except ValueError as e:
print(f"Error: {e}") # Error: La edad debe ser un entero entre 0 y 120
Ventajas de los getters y setters
Los getters y setters ofrecen varias ventajas importantes:
- Validación de datos: Podemos verificar que los valores asignados cumplan con ciertas condiciones.
- Encapsulación: Ocultamos los detalles de implementación y exponemos solo lo necesario.
- Flexibilidad: Podemos cambiar la implementación interna sin afectar el código que usa la clase.
- Depuración: Facilitan la detección de problemas al centralizar el acceso a los datos.
Ejemplo práctico: Clase Producto
Veamos un ejemplo más completo con una clase Producto
que utiliza getters y setters para controlar el acceso a sus atributos:
class Producto:
def __init__(self, nombre, precio, stock=0):
self._nombre = nombre
self._precio = precio
self._stock = stock
self._descuento = 0
# Getters
def get_nombre(self):
return self._nombre
def get_precio(self):
# Aplicamos el descuento al devolver el precio
return self._precio * (1 - self._descuento)
def get_precio_base(self):
# Devolvemos el precio sin descuento
return self._precio
def get_stock(self):
return self._stock
def get_descuento(self):
return self._descuento
# Setters
def set_nombre(self, nuevo_nombre):
if not isinstance(nuevo_nombre, str) or len(nuevo_nombre) == 0:
raise ValueError("El nombre debe ser una cadena no vacía")
self._nombre = nuevo_nombre
def set_precio(self, nuevo_precio):
if not isinstance(nuevo_precio, (int, float)) or nuevo_precio < 0:
raise ValueError("El precio debe ser un número positivo")
self._precio = nuevo_precio
def set_stock(self, nuevo_stock):
if not isinstance(nuevo_stock, int) or nuevo_stock < 0:
raise ValueError("El stock debe ser un entero positivo")
self._stock = nuevo_stock
def set_descuento(self, nuevo_descuento):
if not isinstance(nuevo_descuento, float) or not 0 <= nuevo_descuento <= 1:
raise ValueError("El descuento debe ser un número entre 0 y 1")
self._descuento = nuevo_descuento
Ahora podemos usar esta clase de manera segura:
# Crear un producto
laptop = Producto("Laptop XPS", 1200.0, 10)
# Obtener información
print(f"Producto: {laptop.get_nombre()}")
print(f"Precio base: ${laptop.get_precio_base()}")
print(f"Stock disponible: {laptop.get_stock()} unidades")
# Aplicar un descuento del 15%
laptop.set_descuento(0.15)
print(f"Precio con descuento: ${laptop.get_precio()}")
# Actualizar el stock después de una venta
laptop.set_stock(laptop.get_stock() - 1)
print(f"Stock actualizado: {laptop.get_stock()} unidades")
# Intentar establecer un precio negativo
try:
laptop.set_precio(-100)
except ValueError as e:
print(f"Error: {e}") # Error: El precio debe ser un número positivo
Getters y setters en herencia
Los getters y setters también son útiles en el contexto de la herencia, permitiendo a las subclases acceder y modificar atributos protegidos de manera controlada:
class Electrónico(Producto):
def __init__(self, nombre, precio, stock, garantía_meses):
super().__init__(nombre, precio, stock)
self._garantía_meses = garantía_meses
self._activado = False
# Getters adicionales
def get_garantía_meses(self):
return self._garantía_meses
def está_activado(self):
return self._activado
# Setters adicionales
def set_garantía_meses(self, meses):
if not isinstance(meses, int) or meses < 0:
raise ValueError("Los meses de garantía deben ser un entero positivo")
self._garantía_meses = meses
def activar(self):
self._activado = True
def desactivar(self):
self._activado = False
# Sobrescribir el setter de precio para añadir lógica adicional
def set_precio(self, nuevo_precio):
# Llamamos al setter de la clase padre
super().set_precio(nuevo_precio)
# Lógica adicional específica para productos electrónicos
if nuevo_precio > 1000:
# Productos caros tienen garantía extendida automáticamente
self._garantía_meses = max(self._garantía_meses, 24)
Consideraciones sobre el uso de getters y setters
Aunque los getters y setters son útiles, es importante usarlos con criterio:
- No crees getters y setters para todo: Solo para atributos que necesiten validación o lógica especial.
- Mantén la simplicidad: Si un atributo no necesita validación ni procesamiento, considera si realmente necesita getters y setters.
- Sé consistente: Si decides usar getters y setters, úsalos de manera consistente en toda la clase.
Getters y setters vs. acceso directo
En Python, a diferencia de lenguajes como Java, no es obligatorio usar getters y setters para todos los atributos. Para atributos simples que no requieren validación, a veces es aceptable permitir el acceso directo:
class ConfiguraciónSimple:
def __init__(self):
self.modo_debug = False
self.max_conexiones = 100
self.tiempo_espera = 30
Sin embargo, si más adelante necesitas añadir validación o lógica adicional, tendrás que cambiar la interfaz de la clase. Por eso, para clases que podrían evolucionar, es mejor usar getters y setters desde el principio o, como veremos en la siguiente sección, usar propiedades de Python.
Los getters y setters son una forma tradicional de implementar la encapsulación en Python, proporcionando un control preciso sobre cómo se accede y modifica los datos internos de una clase. Sin embargo, Python ofrece una forma más elegante y "pythónica" de lograr lo mismo: las propiedades, que veremos en la siguiente sección.
Propiedades
Las propiedades en Python representan una forma elegante y más "pythónica" de implementar la encapsulación, superando las limitaciones de los getters y setters tradicionales. Este mecanismo nos permite acceder a atributos privados mediante una sintaxis limpia que parece acceso directo a atributos, pero con todo el control de los métodos.
En esencia, una propiedad es un descriptor que transforma un método en un atributo de solo lectura, lectura-escritura o solo escritura. Esto nos permite mantener la encapsulación sin sacrificar la claridad del código.
Creando propiedades con el decorador @property
La forma más común de crear propiedades en Python moderno es utilizando el decorador @property
:
class Temperatura:
def __init__(self, celsius=0):
self._celsius = celsius
@property
def celsius(self):
"""Obtiene la temperatura en grados Celsius."""
return self._celsius
@celsius.setter
def celsius(self, valor):
"""Establece la temperatura en grados Celsius."""
if valor < -273.15:
raise ValueError("La temperatura no puede ser menor que el cero absoluto")
self._celsius = valor
@property
def fahrenheit(self):
"""Obtiene la temperatura en grados Fahrenheit."""
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, valor):
"""Establece la temperatura en grados Fahrenheit."""
celsius = (valor - 32) * 5/9
if celsius < -273.15:
raise ValueError("La temperatura no puede ser menor que el cero absoluto")
self._celsius = celsius
Ahora podemos usar esta clase con una sintaxis muy natural:
# Crear un objeto temperatura
temp = Temperatura(25)
# Acceder a las propiedades como si fueran atributos
print(f"Temperatura: {temp.celsius}°C") # 25°C
print(f"Temperatura: {temp.fahrenheit}°F") # 77°F
# Modificar las propiedades
temp.celsius = 30
print(f"Nueva temperatura: {temp.celsius}°C") # 30°C
print(f"Nueva temperatura: {temp.fahrenheit}°F") # 86°F
# Modificar usando fahrenheit
temp.fahrenheit = 68
print(f"Temperatura actualizada: {temp.celsius}°C") # 20°C
# Intentar establecer una temperatura imposible
try:
temp.celsius = -300
except ValueError as e:
print(f"Error: {e}") # Error: La temperatura no puede ser menor que el cero absoluto
Anatomía de una propiedad
Una propiedad completa en Python puede tener hasta tres componentes:
- Getter (
@property
): Define el método que se llama cuando se accede a la propiedad - Setter (
@nombre.setter
): Define el método que se llama cuando se asigna un valor a la propiedad - Deleter (
@nombre.deleter
): Define el método que se llama cuando se elimina la propiedad
Veamos un ejemplo completo:
class Persona:
def __init__(self, nombre):
self._nombre = nombre
self._amigos = []
@property
def nombre(self):
"""Obtiene el nombre de la persona."""
return self._nombre
@nombre.setter
def nombre(self, valor):
"""Establece el nombre de la persona."""
if not isinstance(valor, str) or not valor:
raise ValueError("El nombre debe ser una cadena no vacía")
self._nombre = valor
@property
def amigos(self):
"""Obtiene la lista de amigos (como copia para evitar modificaciones directas)."""
return self._amigos.copy()
@amigos.deleter
def amigos(self):
"""Elimina todos los amigos."""
self._amigos = []
print("Lista de amigos eliminada")
Uso de esta clase:
# Crear una persona
p = Persona("Carlos")
# Usar el getter
print(p.nombre) # Carlos
# Usar el setter
p.nombre = "Carlos Rodríguez"
print(p.nombre) # Carlos Rodríguez
# Intentar modificar la lista de amigos directamente (no afecta al original)
amigos = p.amigos
amigos.append("Ana")
print(p.amigos) # [] - La lista original no se modificó
# Usar el deleter
del p.amigos
Propiedades de solo lectura
Para crear una propiedad de solo lectura, simplemente omitimos el setter:
class Círculo:
def __init__(self, radio):
self._radio = radio
@property
def radio(self):
return self._radio
@radio.setter
def radio(self, valor):
if valor <= 0:
raise ValueError("El radio debe ser positivo")
self._radio = valor
@property
def área(self):
"""Área del círculo (propiedad de solo lectura)."""
import math
return math.pi * self._radio ** 2
@property
def perímetro(self):
"""Perímetro del círculo (propiedad de solo lectura)."""
import math
return 2 * math.pi * self._radio
En este ejemplo, radio
es una propiedad de lectura-escritura, mientras que área
y perímetro
son propiedades de solo lectura:
c = Círculo(5)
print(f"Radio: {c.radio}") # 5
print(f"Área: {c.área:.2f}") # 78.54
print(f"Perímetro: {c.perímetro:.2f}") # 31.42
# Podemos cambiar el radio
c.radio = 10
print(f"Nuevo radio: {c.radio}") # 10
print(f"Nueva área: {c.área:.2f}") # 314.16
# Pero no podemos cambiar el área directamente
try:
c.área = 100
except AttributeError as e:
print(f"Error: {e}") # Error: can't set attribute 'área'
Propiedades calculadas
Una de las ventajas más importantes de las propiedades es que pueden calcular valores dinámicamente:
class Empleado:
def __init__(self, nombre, salario_base, horas_extra=0, tarifa_extra=0):
self._nombre = nombre
self._salario_base = salario_base
self._horas_extra = horas_extra
self._tarifa_extra = tarifa_extra
@property
def nombre(self):
return self._nombre
@property
def salario_base(self):
return self._salario_base
@salario_base.setter
def salario_base(self, valor):
if valor < 0:
raise ValueError("El salario base no puede ser negativo")
self._salario_base = valor
@property
def horas_extra(self):
return self._horas_extra
@horas_extra.setter
def horas_extra(self, valor):
if valor < 0:
raise ValueError("Las horas extra no pueden ser negativas")
self._horas_extra = valor
@property
def tarifa_extra(self):
return self._tarifa_extra
@tarifa_extra.setter
def tarifa_extra(self, valor):
if valor < 0:
raise ValueError("La tarifa extra no puede ser negativa")
self._tarifa_extra = valor
@property
def salario_total(self):
"""Calcula el salario total incluyendo las horas extra."""
return self._salario_base + (self._horas_extra * self._tarifa_extra)
Uso de esta clase:
# Crear un empleado
emp = Empleado("Laura Martínez", 2000, 10, 15)
# Acceder a propiedades básicas
print(f"Empleado: {emp.nombre}")
print(f"Salario base: {emp.salario_base}€")
print(f"Horas extra: {emp.horas_extra}")
print(f"Tarifa extra: {emp.tarifa_extra}€/hora")
# Acceder a la propiedad calculada
print(f"Salario total: {emp.salario_total}€") # 2150€
# Modificar algunos valores
emp.horas_extra = 15
emp.tarifa_extra = 20
# La propiedad calculada se actualiza automáticamente
print(f"Nuevo salario total: {emp.salario_total}€") # 2300€
Ventajas de las propiedades sobre getters y setters tradicionales
Las propiedades ofrecen varias ventajas significativas:
- Sintaxis más limpia: El código cliente es más legible y natural
- Compatibilidad hacia atrás: Puedes comenzar con atributos públicos y luego convertirlos en propiedades sin cambiar la interfaz
- Encapsulación mejorada: Ocultas los detalles de implementación mientras mantienes una interfaz simple
- Código más pythónico: Sigue el principio de "lo explícito es mejor que lo implícito"
Migración de getters/setters a propiedades
Una ventaja importante de las propiedades es que permiten refactorizar código existente sin romper la compatibilidad:
# Versión inicial con atributos públicos
class ProductoV1:
def __init__(self, nombre, precio):
self.nombre = nombre
self.precio = precio
# Versión intermedia con getters y setters
class ProductoV2:
def __init__(self, nombre, precio):
self._nombre = nombre
self._precio = precio
def get_nombre(self):
return self._nombre
def set_nombre(self, valor):
self._nombre = valor
def get_precio(self):
return self._precio
def set_precio(self, valor):
if valor < 0:
raise ValueError("El precio no puede ser negativo")
self._precio = valor
# Versión final con propiedades
class ProductoV3:
def __init__(self, nombre, precio):
self._nombre = nombre
self._precio = precio
@property
def nombre(self):
return self._nombre
@nombre.setter
def nombre(self, valor):
self._nombre = valor
@property
def precio(self):
return self._precio
@precio.setter
def precio(self, valor):
if valor < 0:
raise ValueError("El precio no puede ser negativo")
self._precio = valor
La versión con propiedades (ProductoV3
) mantiene la misma interfaz que la versión original (ProductoV1
), pero con toda la validación y encapsulación de la versión intermedia (ProductoV2
).
Propiedades en clases heredadas
Las propiedades también funcionan bien con la herencia. Podemos sobrescribir propiedades en subclases o añadir nuevas:
class Producto:
def __init__(self, nombre, precio):
self._nombre = nombre
self._precio = precio
@property
def nombre(self):
return self._nombre
@nombre.setter
def nombre(self, valor):
self._nombre = valor
@property
def precio(self):
return self._precio
@precio.setter
def precio(self, valor):
if valor < 0:
raise ValueError("El precio no puede ser negativo")
self._precio = valor
@property
def info(self):
return f"{self._nombre}: {self._precio}€"
class ProductoDigital(Producto):
def __init__(self, nombre, precio, tamaño_mb):
super().__init__(nombre, precio)
self._tamaño_mb = tamaño_mb
@property
def tamaño_mb(self):
return self._tamaño_mb
@tamaño_mb.setter
def tamaño_mb(self, valor):
if valor <= 0:
raise ValueError("El tamaño debe ser positivo")
self._tamaño_mb = valor
# Sobrescribir la propiedad info
@property
def info(self):
return f"{self._nombre}: {self._precio}€ ({self._tamaño_mb} MB)"
Uso de estas clases:
# Crear productos
p1 = Producto("Teclado", 49.99)
p2 = ProductoDigital("Ebook Python", 19.99, 15.5)
# Usar propiedades
print(p1.info) # Teclado: 49.99€
print(p2.info) # Ebook Python: 19.99€ (15.5 MB)
# Modificar propiedades
p2.tamaño_mb = 20
p2.precio = 24.99
print(p2.info) # Ebook Python: 24.99€ (20 MB)
Buenas prácticas con propiedades
Para aprovechar al máximo las propiedades en Python:
- Usa propiedades para validación: Cuando necesites validar datos antes de asignarlos
- Usa propiedades para cálculos: Para valores que se pueden derivar de otros atributos
- Mantén las propiedades simples: Evita operaciones costosas en los getters
- Documenta tus propiedades: Usa docstrings para explicar qué hace cada propiedad
- Sé consistente: Si una clase tiene algunas propiedades, considera usar propiedades para todos los atributos públicos
Las propiedades son una herramienta fundamental para implementar la encapsulación en Python de manera elegante y efectiva, permitiéndonos escribir código más limpio y mantenible mientras protegemos los datos internos de nuestras clases.
Métodos privados
Al igual que con los atributos, la encapsulación en Python también se aplica a los métodos de una clase. Los métodos privados son aquellos que están destinados a ser utilizados únicamente dentro de la propia clase, formando parte de su implementación interna y no de su interfaz pública.
Estos métodos funcionan como "ayudantes internos" que realizan operaciones específicas necesarias para el funcionamiento de la clase, pero que no tienen sentido exponer al exterior. Imagina los métodos privados como los engranajes internos de un reloj: fundamentales para su funcionamiento, pero que el usuario final no necesita manipular directamente.
Convención para métodos privados
En Python, se utiliza la misma convención de nomenclatura para los métodos privados que para los atributos:
- Un guion bajo (
_método
) indica un método protegido (por convención) - Dos guiones bajos (
__método
) crean un método privado con name mangling
class Autenticador:
def __init__(self, usuario, contraseña):
self._usuario = usuario
self._contraseña_hash = self.__generar_hash(contraseña)
def __generar_hash(self, contraseña):
"""Método privado para generar un hash de la contraseña."""
import hashlib
return hashlib.sha256(contraseña.encode()).hexdigest()
def verificar_contraseña(self, contraseña_ingresada):
"""Método público que utiliza el método privado internamente."""
hash_ingresado = self.__generar_hash(contraseña_ingresada)
return hash_ingresado == self._contraseña_hash
En este ejemplo, __generar_hash
es un método privado que solo debe ser llamado desde dentro de la clase. El método público verificar_contraseña
utiliza este método privado como parte de su implementación.
Casos de uso para métodos privados
Los métodos privados son especialmente útiles en varios escenarios:
- Dividir algoritmos complejos en pasos más pequeños y manejables
- Evitar la duplicación de código dentro de una clase
- Ocultar detalles de implementación que podrían cambiar en el futuro
- Simplificar la interfaz pública de la clase
Ejemplo: Procesamiento de datos en etapas
Veamos un ejemplo más elaborado donde los métodos privados ayudan a organizar un proceso complejo:
class ProcesadorTexto:
def __init__(self):
self._texto = ""
self._estadísticas = {}
def procesar_archivo(self, ruta_archivo):
"""Método público que procesa un archivo de texto."""
try:
texto = self.__leer_archivo(ruta_archivo)
self._texto = self.__normalizar_texto(texto)
self._estadísticas = self.__calcular_estadísticas(self._texto)
return True
except Exception as e:
print(f"Error al procesar el archivo: {e}")
return False
def __leer_archivo(self, ruta):
"""Método privado para leer el contenido de un archivo."""
with open(ruta, 'r', encoding='utf-8') as archivo:
return archivo.read()
def __normalizar_texto(self, texto):
"""Método privado para normalizar el texto."""
# Convertir a minúsculas
texto = texto.lower()
# Eliminar caracteres especiales
import re
texto = re.sub(r'[^\w\s]', '', texto)
# Eliminar espacios extra
texto = re.sub(r'\s+', ' ', texto).strip()
return texto
def __calcular_estadísticas(self, texto):
"""Método privado para calcular estadísticas del texto."""
palabras = texto.split()
estadísticas = {
'total_palabras': len(palabras),
'palabras_únicas': len(set(palabras)),
'longitud_promedio': sum(len(p) for p in palabras) / len(palabras) if palabras else 0
}
return estadísticas
def obtener_estadísticas(self):
"""Método público para acceder a las estadísticas calculadas."""
return self._estadísticas.copy()
def obtener_texto_procesado(self):
"""Método público para obtener el texto procesado."""
return self._texto
En este ejemplo:
- El método público
procesar_archivo
coordina todo el proceso - Los métodos privados
__leer_archivo
,__normalizar_texto
y__calcular_estadísticas
realizan pasos específicos - Los métodos públicos
obtener_estadísticas
yobtener_texto_procesado
proporcionan acceso a los resultados
Esta estructura hace que el código sea más modular, mantenible y con una interfaz pública clara y concisa.
Métodos privados vs. funciones auxiliares
A veces surge la duda de si usar métodos privados o simplemente definir funciones auxiliares dentro de otros métodos. Veamos las diferencias:
class Ejemplo1:
def método_público(self, datos):
# Función auxiliar definida dentro del método
def función_auxiliar(x):
return x * 2
resultado = [función_auxiliar(x) for x in datos]
return resultado
class Ejemplo2:
def método_público(self, datos):
resultado = [self.__función_auxiliar(x) for x in datos]
return resultado
def __función_auxiliar(self, x):
return x * 2
Consideraciones para elegir:
- Métodos privados: Mejor cuando la funcionalidad se usa en múltiples métodos de la clase o cuando necesita acceder a
self
- Funciones auxiliares internas: Mejor para operaciones muy específicas de un solo método y que no necesitan acceder a
self
Métodos privados en herencia
Los métodos privados con doble guion bajo (__método
) no son heredados directamente por las subclases debido al name mangling:
class Base:
def __init__(self):
self.público = "Accesible para todos"
def método_público(self):
print("Método público llamando a método privado:")
self.__método_privado()
def __método_privado(self):
print("Este es un método privado de Base")
class Derivada(Base):
def nuevo_método(self):
print("Intentando llamar al método privado del padre:")
try:
self.__método_privado() # Esto fallará
except AttributeError as e:
print(f"Error: {e}")
def __método_privado(self):
print("Este es un método privado de Derivada")
Si ejecutamos este código:
base = Base()
base.método_público() # Funciona correctamente
derivada = Derivada()
derivada.método_público() # Funciona, llama al __método_privado de Base
derivada.nuevo_método() # Falla al intentar llamar a __método_privado de Base
Esto demuestra que:
- El método
__método_privado
deBase
es accesible desde los métodos deBase
- El método
método_público
heredado enDerivada
sigue accediendo al__método_privado
deBase
- La clase
Derivada
no puede acceder directamente al__método_privado
deBase
- La clase
Derivada
puede definir su propio__método_privado
sin conflicto
Métodos protegidos para herencia
Si necesitas que los métodos sean accesibles para las subclases pero no para el código externo, es mejor usar la convención de un solo guion bajo para crear métodos protegidos:
class Forma:
def __init__(self):
self._tipo = "Forma genérica"
def calcular_área(self):
"""Método público que utiliza un método protegido."""
return self._obtener_área()
def _obtener_área(self):
"""Método protegido que las subclases deben sobrescribir."""
raise NotImplementedError("Las subclases deben implementar este método")
def _validar_dimensiones(self, valor):
"""Método protegido útil para las subclases."""
if not isinstance(valor, (int, float)) or valor <= 0:
raise ValueError("Las dimensiones deben ser números positivos")
return True
class Círculo(Forma):
def __init__(self, radio):
super().__init__()
self._tipo = "Círculo"
self._validar_dimensiones(radio) # Usando el método protegido de la clase base
self._radio = radio
def _obtener_área(self):
"""Implementación del método protegido de la clase base."""
import math
return math.pi * self._radio ** 2
class Rectángulo(Forma):
def __init__(self, ancho, alto):
super().__init__()
self._tipo = "Rectángulo"
self._validar_dimensiones(ancho) # Usando el método protegido de la clase base
self._validar_dimensiones(alto)
self._ancho = ancho
self._alto = alto
def _obtener_área(self):
"""Implementación del método protegido de la clase base."""
return self._ancho * self._alto
En este ejemplo:
_obtener_área
es un método protegido que cada subclase debe implementar_validar_dimensiones
es un método protegido útil que las subclases pueden reutilizarcalcular_área
es un método público que forma parte de la interfaz estable
Ejemplo práctico: Validación de datos complejos
Los métodos privados son especialmente útiles para dividir procesos de validación complejos:
class Formulario:
def __init__(self):
self._datos = {}
self._errores = {}
def validar(self, datos):
"""Método público para validar todos los datos del formulario."""
self._datos = datos.copy()
self._errores = {}
# Usar métodos privados para cada tipo de validación
self.__validar_campos_requeridos()
self.__validar_email()
self.__validar_contraseña()
self.__validar_edad()
return len(self._errores) == 0
def obtener_errores(self):
"""Método público para obtener los errores de validación."""
return self._errores.copy()
def __validar_campos_requeridos(self):
"""Método privado para validar campos obligatorios."""
campos_requeridos = ['nombre', 'email', 'contraseña']
for campo in campos_requeridos:
if campo not in self._datos or not self._datos[campo]:
self._errores[campo] = f"El campo {campo} es obligatorio"
def __validar_email(self):
"""Método privado para validar formato de email."""
if 'email' in self._datos and self._datos['email']:
import re
patron = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if not re.match(patron, self._datos['email']):
self._errores['email'] = "El formato del email no es válido"
def __validar_contraseña(self):
"""Método privado para validar seguridad de contraseña."""
if 'contraseña' in self._datos and self._datos['contraseña']:
contraseña = self._datos['contraseña']
if len(contraseña) < 8:
self._errores['contraseña'] = "La contraseña debe tener al menos 8 caracteres"
elif not any(c.isupper() for c in contraseña):
self._errores['contraseña'] = "La contraseña debe contener al menos una mayúscula"
elif not any(c.isdigit() for c in contraseña):
self._errores['contraseña'] = "La contraseña debe contener al menos un número"
def __validar_edad(self):
"""Método privado para validar la edad."""
if 'edad' in self._datos:
try:
edad = int(self._datos['edad'])
if edad < 18:
self._errores['edad'] = "Debes ser mayor de edad"
elif edad > 120:
self._errores['edad'] = "La edad ingresada no es válida"
except ValueError:
self._errores['edad'] = "La edad debe ser un número"
Este enfoque modular hace que el código sea más legible y mantenible, ya que cada método privado se encarga de una responsabilidad específica.
Buenas prácticas para métodos privados
Para usar métodos privados de manera efectiva:
- Mantén los métodos privados enfocados en una sola tarea
- Usa nombres descriptivos que indiquen claramente lo que hace el método
- Documenta el propósito de cada método privado con docstrings
- Prefiere métodos protegidos (
_método
) sobre privados (__método
) cuando trabajes con herencia - Evita métodos privados muy largos - si un método privado es muy extenso, considera dividirlo aún más
Los métodos privados son una herramienta poderosa para implementar la encapsulación en Python, permitiéndote organizar el código interno de tus clases de manera lógica mientras mantienes una interfaz pública limpia y bien definida.
Otros ejercicios de programación de Python
Evalúa tus conocimientos de esta lección Encapsulación con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Módulo math
Reto herencia
Excepciones
Introducción a Python
Reto variables
Funciones Python
Reto funciones
Módulo datetime
Reto acumulación
Reto estructuras condicionales
Polimorfismo
Módulo os
Reto métodos dunder
Diccionarios
Reto clases y objetos
Reto operadores
Operadores
Estructuras de control
Funciones lambda
Reto diccionarios
Reto función lambda
Encapsulación
Reto coleciones
Reto funciones auxiliares
Crear módulos y paquetes
Módulo datetime
Excepciones
Operadores
Diccionarios
Reto map, filter
Reto tuplas
Proyecto gestor de tareas CRUD
Tuplas
Variables
Tipos de datos
Conjuntos
Reto mixins
Módulo csv
Módulo json
Herencia
Análisis de datos de ventas con Pandas
Reto fechas y tiempo
Reto estructuras de iteración
Funciones
Reto comprehensions
Variables
Reto serialización
Módulo csv
Reto polimorfismo
Polimorfismo
Clases y objetos
Reto encapsulación
Estructuras de control
Importar módulos y paquetes
Módulo math
Funciones lambda
Reto excepciones
Listas
Reto archivos
Encapsulación
Reto conjuntos
Clases y objetos
Instalación de Python y creación de proyecto
Reto listas
Tipos de datos
Crear módulos y paquetes
Tuplas
Herencia
Reto acceso a sistema
Proyecto sintaxis calculadora
Importar módulos y paquetes
Clases y objetos
Módulo os
Listas
Conjuntos
Reto tipos de datos
Reto matemáticas
Módulo json
Todas las lecciones de Python
Accede a todas las lecciones de Python y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Python
Introducción
Instalación Y Creación De Proyecto
Introducción
Tema 2: Tipos De Datos, Variables Y Operadores
Introducción
Instalación De Python
Introducción
Tipos De Datos
Sintaxis
Variables
Sintaxis
Operadores
Sintaxis
Estructuras De Control
Sintaxis
Funciones
Sintaxis
Estructuras Control Iterativo
Sintaxis
Estructuras Control Condicional
Sintaxis
Testing Con Pytest
Sintaxis
Listas
Estructuras De Datos
Tuplas
Estructuras De Datos
Diccionarios
Estructuras De Datos
Conjuntos
Estructuras De Datos
Comprehensions
Estructuras De Datos
Clases Y Objetos
Programación Orientada A Objetos
Excepciones
Programación Orientada A Objetos
Encapsulación
Programación Orientada A Objetos
Herencia
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
Mixins Y Herencia Múltiple
Programación Orientada A Objetos
Métodos Especiales (Dunder Methods)
Programación Orientada A Objetos
Composición De Clases
Programación Orientada A Objetos
Funciones Lambda
Programación Funcional
Aplicación Parcial
Programación Funcional
Entrada Y Salida, Manejo De Archivos
Programación Funcional
Decoradores
Programación Funcional
Generadores
Programación Funcional
Paradigma Funcional
Programación Funcional
Composición De Funciones
Programación Funcional
Funciones Orden Superior Map Y Filter
Programación Funcional
Funciones Auxiliares
Programación Funcional
Reducción Y Acumulación
Programación Funcional
Archivos Comprimidos
Entrada Y Salida Io
Entrada Y Salida Avanzada
Entrada Y Salida Io
Archivos Temporales
Entrada Y Salida Io
Contexto With
Entrada Y Salida Io
Módulo Csv
Biblioteca Estándar
Módulo Json
Biblioteca Estándar
Módulo Datetime
Biblioteca Estándar
Módulo Math
Biblioteca Estándar
Módulo Os
Biblioteca Estándar
Módulo Re
Biblioteca Estándar
Módulo Random
Biblioteca Estándar
Módulo Time
Biblioteca Estándar
Módulo Collections
Biblioteca Estándar
Módulo Sys
Biblioteca Estándar
Módulo Statistics
Biblioteca Estándar
Módulo Pickle
Biblioteca Estándar
Módulo Pathlib
Biblioteca Estándar
Importar Módulos Y Paquetes
Paquetes Y Módulos
Crear Módulos Y Paquetes
Paquetes Y Módulos
Entornos Virtuales (Virtualenv, Venv)
Entorno Y Dependencias
Gestión De Dependencias (Pip, Requirements.txt)
Entorno Y Dependencias
Python-dotenv Y Variables De Entorno
Entorno Y Dependencias
Acceso A Datos Con Mysql, Pymongo Y Pandas
Acceso A Bases De Datos
Acceso A Mongodb Con Pymongo
Acceso A Bases De Datos
Acceso A Mysql Con Mysql Connector
Acceso A Bases De Datos
Novedades Python 3.13
Características Modernas
Operador Walrus
Características Modernas
Pattern Matching
Características Modernas
Instalación Beautiful Soup
Web Scraping
Sintaxis General De Beautiful Soup
Web Scraping
Tipos De Selectores
Web Scraping
Web Scraping De Html
Web Scraping
Web Scraping Para Ciencia De Datos
Web Scraping
Autenticación Y Acceso A Recursos Protegidos
Web Scraping
Combinación De Selenium Con Beautiful Soup
Web Scraping
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender la convención de atributos privados y protegidos en Python.
- Aprender a implementar getters y setters para controlar el acceso a atributos.
- Conocer el uso de propiedades para una encapsulación más elegante y pythónica.
- Entender la definición y utilidad de métodos privados y protegidos en clases.
- Aplicar buenas prácticas para organizar y proteger la implementación interna de las clases.