Encapsulación¶
La Encapsulación es el principio de la POO que consiste en:
- Agrupar datos (atributos) y comportamiento (métodos) dentro de una clase.
- Controlar el acceso a esos datos para proteger el estado interno del objeto.
- Exponer una interfaz (métodos) para usar el objeto , es decir, un conjunto de métodos y/o propiedades bien definidos que permiten usar el objeto desde fuera sin conocer ni modificar su funcionamiento interno.
Tipos de acceso¶
En la POO se distinguen tres niveles de acceso:
- Público
- Los datos y los métodos de una clase son accesibles desde cualquier parte del programa.
- Es el nivel más bajo de protección.
- Asignaremos esta protección a la parte de mis objetos que puede ser accedida de forma externa.
- Protegido
- Los datos y métodos con este nivel de protección no son accesibles externamente.
- Solo pueden ser accedidos desde la propia clase o desde subclases (más adelante veremos las subclases)
- Privado
- Es el nivel más alto de protección
- Los datos y métodos con este nivel de protección solo son accesibles desde la propia clase.
En muchos lenguajes de programación se usan modificadores (palabras reservadas) que se ponen al principio de la definición del atributo o método para asignarle un nivel de protección. Por ejemplo, en Java se usan los modificadores public, protected y private
Cuando hablamos de miembros de una clase estamos hablando de los atributos y métodos que incluye
Miembros públicos en Python¶
En Python los atributos y métodos son públicos por defecto. Cualquier miembro puede ser accedido desde el exterior con la notación punteada como hemos visto hasta ahora.
Podemos acceder a los atributos y métodos de la clase e incluso modificar su valor:
>>> sebastian = Estudiante("Sebastián", 15)
>>> print(sebastian.edad)
15
>>> sebastian.nombre = "Antonio"
>>> print(sebastian.mostrar_nombre())
'Antonio'
>>> print(sebastian.nombre_colegio)
'Colegio XYZ'
Miembros privados¶
Como hemos visto es el nivel más alto de protección y los métodos y atributos así definidos solo pueden ser accedidos internamente.
En Python no existe un modificador que nos permita definir con dicha protección un miembro, para conseguirlo se sigue el convenio de que el nombre del mismo debe tener como prefijo un doble guión bajo (__)
Para comprobarlo:
>>> est = Estudiante("Bill", 25)
>>> est.__nombre_colegio
AttributeError: 'Estudiante' object has no attribute '__nombre_colegio'
>>> est.__nombre
AttributeError: 'Estudiante' object has no attribute '__nombre'
>>> est.__obtener_edad()
AttributeError: 'Estudiante' object has no attribute '__obtener_edad'
Cuando Python ve que un miembro empieza con __ lo que hace es enmascararlos utilizando un mecanismo que se llama name mangling. Lo que hace es renombrarlos internamente añadiendo al nombre el prefijo _NombreClase.
Para la clase anterior:
>>> est = Estudiante("Bill", 25)
>>> est.__nombre
AttributeError: 'Estudiante' object has no attribute '__nombre'
# Con el prefijo _Estudiante sí podemos acceder a los miembros
>>> est._Estudiante__nombre
'Bill'
>>> est._Estudiante__nombre = 'Steve'
>>> est._Estudiante__nombre
'Steve'
>>> est._Estudiante__obtener_edad()
25
Fíjate que al atributo
__nombrey al método__obtener_edad()se les ha añadido el prefijo_Estudiantey son accesibles externamente mediante_Estudiante__nombrey_Estudiante__obtener_edad().El objetivo no es impedir el acceso, sino que el programador conozca el comportamiento y no intente acceder a miembros si su definición especifica que no se debería acceder.
Miembros protegidos¶
Cómo vimos, son los atributos que queremos que sean accedidos solo internamente o desde las clases heredadas (subclases)
En breve veremos la herencia en POO
Para definir un miembro como protegido seguimos el convenio de poner en su nombre como prefijo el carácter subrayado (_)
En este caso no se realiza ninguna traducción interna por parte de Python, por tanto aunque formalmente el miembro es protegido, realmente si es accesible externamente.
>>> est = Estudiante("Swati")
>>> est._nombre
'Swati'
>>> est._nombre = 'Dipa'
>>> est._nombre
'Dipa'
Los creadores de Python siguieron este esquema por conveniencia y siguieron la regla de que debe ser el programador el que siga dicho convenio y no intente acceder desde donde no debe a los miembros así definidos si quiere que si aplicación sea coherente en su comportamiento.
Extraido de comentario en stackoverflow.com: "Python does not support access protection as C++/Java/C# does. Everything is public. The motto is, "We're all adults here." Document your classes, and insist that your collaborators read and follow the documentation.
The culture in Python is that names starting with underscores mean, "don't use these unless you really know you should." You might choose to begin your "protected" methods with underscores. But keep in mind, this is just a convention, it doesn't change how the method can be accessed.
Names beginning with double underscores (__name) are mangled, so that inheritance hierarchies can be built without fear of name collisions. Some people use these for "private" methods, but again, it doesn't change how the method can be accessed."
Ventajas¶
La encapsulación es importante porque permite diseñar clases seguras, fáciles de usar y de mantener. Sus principales ventajas son:
- Protección de los datos: evita que los atributos se modifiquen directamente desde fuera de la clase, lo que podría dejar al objeto en un estado incorrecto. Ejemplo: en una clase
CuentaBancaria, si el saldo fuera público, podría hacersecuenta.saldo = -100. Al ser protegido/privado, solo puede modificarse mediante métodos controlados. - Prevención de estados inválidos: al obligar a usar métodos o propiedades para modificar el estado del objeto, se pueden añadir validaciones (por ejemplo, impedir valores negativos o fuera de rango) que permiten asegurarnos que los datos no tomen valores no deseados. Ejemplo: una propiedad
edadpodemos antes de asignarle un nuevo valor comprobar que este no es negativos o mayor de120, con lo que nos aseguramos que el objeto siempre tenga datos coherentes. - Facilidad de mantenimiento: si la lógica interna cambia, el código que usa la clase no se ve afectado mientras la interfaz se mantenga igual. Ejemplo: si una cuenta pasa de guardar el saldo en euros a guardarlo en céntimos, el usuario seguirá usando
depositar()yretirar()de la misma forma. Los cambios los haremos en la lógica interna de la clase. - Código más claro y ordenado: se separa qué partes son internas de la clase y cuáles forman parte de su interfaz pública.Ejemplo: métodos como
calcular_total()forman parte de la interfaz, mientras que atributos como_descuentoquedan ocultos al exterior.
En resumen, encapsular ayuda a escribir programas más robustos, seguros y fáciles de modificar.
Uso de miembros protegidos vs privados¶
En caso de que queramos restringir el acceso externo a los miembros (atributos y métodos) de un objeto se recomienda de forma predeterminada utilizar miembros protegidos y reservar los miembros privados solo para cuando el atributo o el método: - No queremos que sea modificado ni siquiera de forma accidental - No debe ser accedido por subclases.
Acceso externo¶
A la hora de acceder a los miembros protegidos o privados de un objeto hay dos enfoques o maneras de hacerlo
Acceso a miembros privados o protegidos usando métodos¶
Cuando diseñamos nuestros objetos definimos como privados los miembros que no queremos que sean accedidos directamente desde fuera del objeto.
Si queremos que externamente se pueda acceder a determinada información lo que hacemos es crear metodos públicos que accedan a los miembros privados de la clase.
Si por ejemplo queremos crear un método que permita externamente mostrar la información de un estudiante:
>>> est = Estudiante("Bill", 25, "Alergia ácaros")
>>> est.mostrar_estudiante()
Bill tiene 25 años. Sus datos médicos son: Alergia ácaros y pertenece a Colegio XYZ
Fíjate que para que un método pueda acceder a los atributos y métodos de la clase debe utilizar la notación punteada poniendo en primer lugar
selfpara indicar que los atributos y métodos a los que queremos acceder son del propio objeto
Acceso con propiedades @property¶
Antes de explicar las propiedades, es importante entender qué es un decorador, ya que @property es uno de ellos.
¿Qué es un decorador?¶
Un decorador es una forma especial de escribir código en Python que permite modificar el comportamiento de una función o método sin cambiar su nombre ni la forma en que se usa.
Se escribe con el símbolo @ justo encima de la definición de la función o método al que afecta.
Desde un punto de vista teórico (sin entrar aún en detalles técnicos), puede entenderse así:
- Un decorador añade funcionalidad extra a una función o método.
- El código que usa esa función no nota el cambio.
- Sirve para mantener el código más claro y organizado.
*una función o método que recibe otra función o método y devuelve una función modificada.
La idea es que permite añadir funcionalidades nuevas a una función sin cambiar su código por dentro.
Veámoslo con un ejemplo:
Lo que ocurre es que @decorador hace que saludar() pase por dentro del decorador. Por lo que el resultado del código anterior sería:
Los decoradores sirven para:
- añadir comportamiento sin tocar la función original
- reutilizar lógica común
- “envolver” funciones para controlar cosas como:
- permisos
- validaciones
- tiempos de ejecución
- logs
- escribir código más limpio y ordenado
Propiedades en objetos con @property y @setter¶
Se les llama propiedades porque, desde el punto de vista del código que usa la clase, se comportan como atributos, no como métodos.
Aunque internamente las propiedades están implementadas mediante métodos, para el usuario del objeto son una propiedad del mismo (por ejemplo, precio, edad, saldo), no una acción.
Esta idea es importante a nivel teórico:
- Un atributo representa una característica o cualidad del objeto.
- Un método representa una acción.
- Una propiedad representa una característica, pero con lógica y control internos.
Por eso, cuando un dato necesita validación o cálculo, pero conceptualmente sigue siendo una característica del objeto, se modela como propiedad y no como método.
Permiten implementar lo que en otros lenguajes de programación se conoce como getters y setters.
Para entenderlo mejor veamos un ejemplo:
getter con @property:¶
En el ejemplo anterior el decorador @property hace de getter del atributo. Lo que conseguimos es convertir el método nota() en un getter, es decir, en el código que se ejecuta cuando se lee un atributo:
Parece que estamos leyendo un atributo, pero en realidad se está llamando al método nota(). Estamos accediendo a un atributo protegido, pero de forma segura a través de un getter.
setter con @atributo.setter¶
@nota.setter linenums="1" hl_lines="1 4"
def nota(self, valor):
if 0 <= valor <= 10:
self._nota = valor
else:
print("Error: la nota debe estar entre 0 y 10")
Este decorador indica que este método debe ejecutarse cuando queremos modificar un atributo:
Es un setter: controla cómo se escribe el valor. En este caso solo se modifica el atributo si cumple con la validación de que el el valor está entre 0 y 10. Si no → se muestra un error y no se cambia la nota.
Modificando la nota correctamente
NO modifica directamente el atributo. Llama al setter nota(self, valor). Como 9 es válido, _nota cambia a 9.
Intento con un valor inválido
Actúa el setter. No cumple 0 <= valor <= 10.
Muestra:
Y la nota no cambia.
Aunque en los ejemplos estamos usando
Ejemplo final¶
A continuación, un ejemplo completo en una clase Coche que demuestra todos los conceptos de encapsulación.
- Atributos públicos (
marca): Se accede y modifica libremente. Útil para datos que no necesitan control. - Atributos protegidos (
_modelo): Pensados para uso interno o en subclases. Python permite acceso, pero la convención dice "no lo hagas" para mantener la encapsulación. - Atributos privados (
__precio): Ocultos mediante name mangling (Python cambia el nombre internamente a_Clase__atributo). Esto previene accesos accidentales, promoviendo el uso de getters/setters. - Métodos públicos (
describir(),precio_total()): Exponen funcionalidad segura. - Métodos protegidos (
_calcular_impuesto): Para lógica interna, accesibles pero no recomendados desde fuera. - Métodos privados (
__actualizar_precio): Solo para uso dentro de la clase. - @property y setter: Proporcionan encapsulación "pythonica". Permiten tratar atributos como públicos pero con validaciones internas, ocultando la implementación.
Aunque en el ejemplo anterior se han usado atributos y métodos privados para ilustrar su funcionamiento, en un caso real es recomendable usar solo miembros protegidos y reservar los privados para los casos especiales que se indicaron anteriormente.