Continuamos con el segundo de los pilares de la
programación orientada a objetos; el encapsulamiento.
En esta completa guía de encapsulamiento en Python
aprenderás los conceptos base para seguir progresando en
la programación orientada a objetos.
Duración estimada de todo el contenido:
Duración del vídeo:
Contiene 8
ejercicios
Contiene 1
vídeo.
Tabla de
contenidos
¿Qué es el encapsulamiento?
El encapsulamiento en Python
El encapsulamiento es el concepto de agrupar atributos y
métodos en un mismo conjunto.
Entonces, al crear una clase, estamos implementando este
concepto.
Podríamos decir, que estamos “poniéndolo todo en una
cápsula”.
Encapsulamiento se dice
encapsulation en
inglés.
Espacio publicitario
Gracias al encapsulamiento, agrupamos en un solo lugar,
los estados y comportamientos.
Por ejemplo, queremos generar usuarios; entonces, que
mejor que tener todos sus atributos y métodos relacionados
en una misma cápsula o clase.
El encapsulamiento en la mayoría de los lenguajes de
programación implica controlar el acceso a los atributos y
métodos internos de una clase, utilizando diferentes
niveles de visibilidad haciendo uso de modificadores de
acceso. Mediante estos, controlamos los niveles de acceso,
haciendo que ciertos detalles de implementación queden
ocultos para el usuario. Así le facilitamos el uso y
evitamos que haga cosas inesperadas.
Hay lenguajes de programación como Java, que son muy
estrictos en este tema. Sin embargo, verás que Python es
laxo cuando hablamos de encapsulamiento.
Lo que vas a ver aquí, realmente se trata de meras
convenciones, que serán útiles para añadir legibilidad a
distintos propósitos con los miembros de las clases.
Recuerda que los miembros de las clases, son tanto sus
atributos como sus métodos.
Modificadores de acceso
En un lenguaje de programación como es Java, tenemos
palabras reservadas del lenguaje, para representar los
modificadores de acceso.
En Python, vamos a utilizar las convenciones de nombres
con un guion bajo (_) o dos
(__).
Podemos distinguir tres tipos de acceso a miembros:
Público
Protegido (_)
Privado (__)
En la siguiente clase, tienes un ejemplo de cada tipo:
class Usuario:
def __init__(self, id, nombre, apellidos):
self.id = id # Público
self._nombre = nombre # Protegido
self.__apellidos = apellidos # Privado
Los miembros públicos serán accesibles desde incluso
fuera de la clase.
Hagamos una prueba rápida:
class Usuario:
id = 1 # Público
# Accedemos al atributo público
Usuario.id = 1000
# Instanciamos un objeto
usuario_1 = Usuario()
# Comprobamos el valor de id
print(usuario_1.id)
Resultado en la
consola
1000
He podido acceder al atributo desde fuera de la clase, y
no solo eso, también lo he podido modificar. Esto
constituye un mal uso del encapsulamiento, puesto que se
está modificando el valor del atributo id de la propia
clase, desde fuera de ella.
Espacio publicitario
Esta acción, aunque no parece grave, implica que desde la
línea de código en que se modifica el atributo, todos los
objetos instanciados, vendrán con ese valor de id
especificado fuera de la clase.
Esto generará posibles confusiones y errores inesperados
en muchas ocasiones; crearemos código inconsistente y de
muy baja calidad.
Lo que quiero decir con esto, es que no deberías acceder a
los atributos de clase de forma directa (salvo en ciertas
pruebas durante el desarrollo). Que algo se pueda hacer,
no implica que se deba hacer.
Los miembros protegidos, según la convención, podrán
ser accedidos en su clase y en las subclases que tengan
herencia.
Los miembros privados, solo serán accesibles en la
propia clase.
Continuemos con el ejemplo de código anterior:
class Usuario:
def __init__(self, id, nombre, apellidos):
self.id = id # Público
self._nombre = nombre # Protegido
self.__apellidos = apellidos # Privado
Creemos un objeto de la clase Usuario, y
probemos con él, a acceder al atributo público llamado
id:
Ahora, intentemos hacer lo mismo para el atributo
protegido _nombre. Este, en teoría, es solo accesible
desde la clase o sus subclases.
print(usuario_1._nombre)
Resultado en la
consola
Enrique
Se ha podido acceder sin problemas desde fuera. Ya he
comentado que Python es laxo en este tema; queda de
nuestra parte utilizar correctamente las convenciones de
modificadores de acceso, para hacer el código más legible
y menos propenso a fallos.
Como recomendación, cuando se empieza a crear una clase,
es conveniente que se especifiquen todos los miembros
como privados, y que luego se vayan cambiando solo los
que se necesiten en otro tipo de acceso.
Nos queda probar el miembro privado. Veamos que ocurre:
print(usuario_1.__apellidos)
Error en la
consola
AttributeError: 'Usuario' object has no attribute '__apellidos'Error de atributo: el objeto de la clase 'Usuario' no tiene el atributo '__apellidos'
El error es el mismo que aparece cuando intentamos
acceder a un miembro inexistente; en este caso, está
funcionando la modificación del acceso.
No obstante, esta restricción se puede saltar muy
fácilmente; no impide el acceso realmente, como si lo
hace un lenguaje como Java.
Espacio publicitario
Interfaz pública
Las API, módulos o incluso el propio lenguaje de
programación Python, cuentan con código que está listo
para ser utilizado fácilmente por los desarrolladores.
Está pensado para que accedan a dicho código.
A esto se le conoce como interfaz pública.
Interfaz pública se dice
public interface en
inglés.
Por otro lado, lo que no está en la interfaz pública, es
el código de funcionamiento interno, el cual no está
pensado para que se acceda a él, sino que trabaje de fondo
para las partes de la interfaz pública.
Aquí tienes un ejemplo con el código interno de Python. Si
nos vamos al archivo
builtins.py, podremos
ver constantes especificadas expresamente como no
públicas; protegidas concretamente.
Estas están pensadas para el funcionamiento interno del
lenguaje, y no deben ser accedidas por los
desarrolladores, ya que no están diseñadas para ello.
En cambio, si nos vamos a una clase como str,
veremos que tiene métodos como count()o
capitalize(), entre muchos otros, que son
públicos, y que están destinados a utilizarse en cualquier
programa de forma directa.
Público y no público
En Python, debido al comportamiento de sus modificadores
de acceso, nos referimos de forma general a dos tipos de
acceso, lo hacemos con público y no público.
Cuando hable de público, me estaré refiriendo a cualquier
miembro que se quiera dejar para la interfaz pública.
Cuando hable de no público, me referiré a cualquier
miembro que no se deba acceder desde fuera, y que, por lo
tanto, no pertenecerá a esa interfaz pública.
Espacio publicitario
Miembros y documentación
Ahora, imagina que estás creando una biblioteca con
Python. Esta deberá llevar una parte pública y otra no
pública.
La parte pública llevará todos los elementos que
quieras que se utilicen por los desarrolladores. La
parte no pública todo lo contrario.
Los desarrolladores sabrán qué cosas deben utilizar, al
ver los nombres empezando por esos guiones bajos (si
consultan el código interno), y más importante, lo sabrán
basándose en la documentación que hayas creado.
A la hora de crear la documentación, se especifica en PEP
8 (guía de estilo de código para Python), que los
elementos que sean públicos, se deben documentar. En
cambio, no sería necesario hacer lo mismo con los
elementos de funcionamiento interno.
En la documentación, se explicarán y detallarán las partes
que se tienen que acceder, y como se tienen que acceder.
Por ejemplo, en una documentación para usuarios (si haces
una biblioteca, los usuarios serán los desarrolladores),
tendrás que detallar cosas como estas:
Clases
Funciones y métodos
Módulos
Posibles errores
etc.
En cada caso, explicarás para qué usar cada componente, y
como usarlo.
Sin embargo, no debes añadir complejidad a la
documentación, explicando los atributos internos de las
clases, que no sean para uso público; siempre que no sea
absolutamente relevante por algún motivo.
Por ejemplo, si utilizas una biblioteca que hace
operaciones con bases de datos, no necesitas saber como
conecta con una base de datos, sino más bien, saber qué
elemento de la biblioteca utilizar y como, para que se
encargue de conectarla.
En un restaurante, el menú es como la interfaz pública.
Te dice qué platos están disponibles (Por ejemplo,
podrían ser los métodos).
Los chefs, y la cocina, son como la implementación
interna. No necesitas saber cómo cocinan los platos
(detalles técnicos) para comer la comida (usar la
biblioteca).
La cocina sería la interfaz no pública, y la sala de
comensales, sería la interfaz pública.
Con esto, lo que se busca es mejorar la legibilidad del
código, evitar los conflictos de nombres, y evitar un uso
indebido de ciertas partes del código.
Algunos programadores, entre los que me incluyo, echamos
en falta la dureza de lenguajes como Java, al aplicar sus
modificadores de acceso, pero siguiendo bien las
convenciones, podemos conseguir los mismos resultados,
aunque sea de otra forma.
Espacio publicitario
Método público para acceso privado
Ahora sí, veamos como crear un método para la interfaz
pública, que utilice la parte no pública en una clase.
Se puede crear un método público, que sirva para acceder
desde dentro de una clase, a un miembro privado. De esa
forma, en la interfaz pública se utilizará el método
público y no el atributo privado de forma directa.
Estaremos implementando una mejor forma de acceso, y menos
propensa a fallos.
class Usuario:
def __init__(self, id, nombre, apellidos):
# Miembros públicos
self.id = id
self.nombre = nombre
# Miembro privado
self.__apellidos = apellidos
# Método público de instancia
def muestra_apellidos(self):
#__apellidos es accesible dentro de la clase
print(f"Los apellidos son: {self.__apellidos}.")
usuario_1 = Usuario(1, "Enrique", "Barros Fernández")
usuario_1.muestra_apellidos()
Con este método de acceso al atributo, estoy creando una
funcionalidad pública, para que se pueda acceder al
atributo privado. Es decir, implemento y determino de qué
forma se accede al atributo.
Si utilizaras mi código, para mostrar los apellidos de los
usuarios, tendrías que llamar al método
muestra_apellidos(), en lugar de acceder de
otra forma. Eso, por supuesto, si decides seguir la
convención de Python y mi hipotética documentación.
Con el resto de atributos se podría hacer perfectamente lo
mismo.
Todo lo que quieras que no se acceda desde fuera de la
clase o sus subclases, déjalo en la interfaz no pública
(miembros protegidos (_) o privados
(__)), y haces con ello lo que quieras.
Name mangling
Name mangling, es lo que se refiere a la
modificación de nombres de variables y métodos de una
clase, para evitar conflictos con nombres externos o
accesos accidentales.
El término
name mangling se
puede traducir al español de muchas formas. La que
personalmente me parece que se ajusta mejor, sería
modificación de nombres.
Esta técnica se logra poniendo el nombre de un miembro,
con dos guiones bajos de prefijo. Sencillamente, hacer
miembros privados como el de apellidos:
self.__apellidos = apellidos
Al emplear esta técnica, realmente vemos el nombre de
atributo o método así:
__apellidos
Sin embargo, el intérprete de Python lo convierte en esto:
__Clase_miembro
Por eso, si intentamos acceder directamente desde fuera de
la clase, nos dará el siguiente error:
print(usuario_1.__apellidos)
Error en la
consola
AttributeError: 'Usuario' object has no attribute '__apellidos'Error de atributo: el objeto de la clase 'Usuario' no tiene el atributo '__apellidos'
No obstante, anteriormente he indicado que es muy fácil
acceder a este miembro privado, que no es estrictamente
privado como ocurre en otros lenguajes de programación.
Espacio publicitario
Sabiendo de qué forma maneja estos miembros el intérprete
de Python, conseguimos acceder en la interfaz pública, a
algo que es privado.
Puedes comprobar que esto ocurre así, con la función
predefinida dir():
Con esto, puedes ver que Python define una serie de
convenciones para el encapsulamiento, pero que no es
estricto como otros lenguajes de programación, y permite
saltárselas.
Miembros privados y la herencia
He dicho, que los miembros privados (__),
solo pueden accederse desde dentro de las clases, pero no
se puede desde las subclases. Vamos a probarlo.
En el siguiente código, contamos con una clase llamada
Animal, que tiene un miembro privado llamado
__edad, y una subclase llamada
Perro.
Desde la subclase, estoy intentando acceder al miembro
privado:
class Animal:
def __init__(self, nombre, edad):
self.nombre = nombre
self.__edad = edad
class Perro(Animal):
def __init__(self, nombre, edad, raza):
super().__init__(nombre, edad)
self.raza = raza
def mostrar_edad(self):
print(f"Tengo {self.__edad} años.")
AttributeError: 'Perro' object has no attribute '_Perro__edad'Error de atributo: el objeto de la clase 'Perro' no tiene el atributo '_Perro__edad'
Está funcionando. No me deja acceder de esta forma, como
cabría esperar.
En este caso, si queremos acceder desde una subclase
siguiendo las convenciones, debemos utilizar los miembros
protegidos (_), en lugar de los privados.
Espacio publicitario
Miembros protegidos y la herencia
Esta vez, pongo el atributo como protegido, en lugar de
privado.
Entonces, según la convención, estoy indicando que ese
miembro solo sea accedido desde la clase o las subclases,
pero no desde fuera, como se podría hacer con un miembro
público.
Pero insisto por última vez, no lo hagas de esta forma. En
su lugar, crea un método público si quieres utilizar de
alguna forma el método privado en la interfaz pública.
Aquí tienes un ejemplo:
class Persona:
def __init__(self, nombre, edad, altura_cm):
self.nombre = nombre
self.edad = edad
self.altura_cm = altura_cm
# Calcula y devuelve la altura en metros
def __calcular_altura_metros(self):
return self.altura_cm / 100
# Método público para mostrar los resultados
def obtener_altura_metros(self):
altura_metros = self.__calcular_altura_metros()
print(f"La altura de {self.nombre} es de {altura_metros} metros.")
# Ejemplo de uso
persona = Persona("Ana", 30, 170)
persona.obtener_altura_metros()
Resultado en la
consola
La altura de Ana es de 1.7 metros.
Espacio publicitario
Métodos getters y setters
En Python, es posible utilizar las técnicas denominadas
getters y setters.
Los términos get y
set, se pueden
traducir respectivamente como obtener y
establecer. Entonces, si hablamos de un método
getter, estaremos
hablando de un método obtenedor. En cambio,
cuando hablamos de un método
setter, estaremos
hablando de un método establecedor.
Gracias a estas técnicas, podemos crear métodos
getters que sirvan para acceder a los valores de
una clase, y setters para modificarlos.
A continuación, tienes un método de cada tipo:
class Usuario:
def __init__(self, id, nombre, edad):
self.id = id
self.nombre = nombre
self.__edad = edad
# Getter
def obtener_edad(self):
return self.__edad
# Setter
def establecer_edad(self, edad):
self.__edad = edad
El método obtener_edad(), es un
getter. Este sirve para obtener la edad del usuario
llamándolo. Así la puedes guardar, por ejemplo, en una
variable.
El método establecer_edad(), es un
setter. Sirve para modificar la edad desde la
interfaz pública.
Utilicemos primero el método gettercon un objeto:
usuario_1 = Usuario(1, "Enrique", 32)
# Almacenamos el valor de retorno
edad = usuario_1.obtener_edad()
print(edad)
Resultado en la
consola
32
Ahora, utilicemos el método setter y el
método getter en conjunto:
usuario_1 = Usuario(1, "Enrique", 32)
# Establece una edad
usuario_1.establecer_edad(25)
# Obtenemos la edad
print(usuario_1.obtener_edad())
Resultado en la
consola
25
Espacio publicitario
Espacio publicitario
Ejercicios de Python para resolver
22. Crea una clase llamada Usuario que
contenga los siguientes atributos en un
__init__:
27. Mediante los métodos getter y
setter creados, primero cambia la contraseña de uno
de los objetos, y luego, comprueba si se ha establecido
correctamente.
¡Importante! Referente
al dato del atributo contraseña, no es seguro almacenar
contraseñas de esta forma, es solo una práctica para
ayudarte a entender como funciona el tema del
encapsulamiento.
Con el método setter cambiamos la contraseña, y
luego con el método getter, la obtenemos.