Encapsulación¶
La Encapsulación es la propiedad que permite asegurar que la información y los métodos de un objeto están ocultos del mundo exterior.
Cuando mediante el proceso de abstracción diseñamos una clase puede darse el caso de que incluya atributos o métodos que, por seguridad o para evitar inconsistencias, solo queremos que sean accedidos internamente en el objeto, pero no de forma externa.
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.
- Asignaré 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'
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.
Acceso externo a miembros protegidos¶
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)
>>> est.mostrar_estudiante()
Bill tiene 25 años 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
Lo que conseguimos dando protección a ciertos miembros de una clase es encapsular su información y comportamiento y restringir el acceso externo a determinados miembros de la clase que acceden de forma controlada.
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."
Decoradores¶
Para poder entender el siguiente apartado debemos antes entender qué son los decoradores.
Un decorador en Python es una función que recibe otra función 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 una metáfora, imagina que tienes una taza. Si le pones una funda o decoración:
- La taza sigue siendo la misma.
- Pero ahora tiene algo extra: está más caliente, tiene un dibujo, etc.
Un decorador es esa funda que envuelve una función, añadiéndole algo más.
La función original no se toca, pero ahora:
- se ejecuta con más comportamiento,
- o con verificaciones,
- o con mensajes extra,
- o con controles.
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¶
Python ofrece una forma más elegante de controlar el acceso a los atributos protegidos o privados: usando el decorador @property convertimos el atributo protegido o privado en una propiedad que aunque accedemos al mismo como su fuera un atributo normal, realmente es un método el que ejecuta código para leerlo o modificarlo.
getter con @property:¶
Es un decorador que viene incluido con Python. Lo que hace es convertir el método nota() en un getter, es decir, en el código que se ejecuta cuando hacemos:
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
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 alguien hace:
Es un setter: controla cómo se escribe el valor. En este caso solo se modifica el atributo si cumple con el filtro 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.
Esto es encapsulación: impedir que el objeto adopte valores incorrectos o incoherentes.