Contenido del capítulo

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
Logo

    ¿Qué es el encapsulamiento?

    El encapsulamiento en Python
    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:

    usuario_1 = Usuario(1, "Enrique", "Barros Fernández")
    
    print(usuario_1.id)
    Resultado en la consola
    1

    Bien, hemos podido acceder al atributo.

    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.

    De esta forma, evitamos muchos descuidos.

    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.

    Para acceder a los miembros privados, lo podemos hacer de dos formas. Son las que verás a continuación, pero antes, debes comprender lo que es la interfaz pública.

    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.

    _KT = TypeVar("_KT")
    _VT = TypeVar("_VT")
    _S = TypeVar("_S")
    _T1 = TypeVar("_T1")
    _T2 = TypeVar("_T2")
    _T3 = TypeVar("_T3")

    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.

    Analogía restaurante con interfaces de programación

    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():

    print(dir(usuario_1))
    Resultado en la consola
    ['_Usuario__apellidos', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'nombre']

    Uno de los usos de la función predefinida dir(), es devolver una lista que contiene los nombres de los atributos y métodos de un objeto.

    Piensa que salen muchas cosas que no están en la propia clase a la que pertenece el objeto, porque se hereda implícitamente de la clase object.

    Vamos a intentar acceder con esta nueva técnica, desde fuera de la clase, al atributo privado __apellidos, de la clase Usuario:

    class Usuario:
        def __init__(self, nombre, apellidos):
            # Miembro público
            self.nombre = nombre
            # Miembro privado
            self.__apellidos = apellidos
    
    usuario_1 = Usuario("Enrique", "Barros Fernández")
    
    print(usuario_1._Usuario__apellidos)
    Resultado en la consola
    Barros Fernández

    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.")

    Creo un objeto de tipo Perro y llamo al método:

    perro_1 = Perro("Chester", 3, "Husky")
    
    perro_1.mostrar_edad()
    Error en la consola
    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.

    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.")
    
    perro_1 = Perro("Chester", 3, "Husky")
    
    perro_1.mostrar_edad()
    Resultado en la consola
    Tengo 3 años.

    No obstante, vuelvo a recalcar lo de que se trata de convenciones, ya que este miembro protegido, lo puedo utilizar como público; aunque no deba:

    print(perro_1._edad)
    Resultado en la consola
    3

    Métodos con modificadores de acceso

    Con los nombres de los métodos también podemos utilizar modificadores de acceso:

    class Animal:
        def __init__(self, nombre, edad):
            self.nombre = nombre
            self.edad = edad
    
        def _mostrar_edad(self):
            print(f"Tengo {self.edad} años.")
    
    class Perro(Animal):
        def __init__(self, nombre, edad, raza):
            super().__init__(nombre, edad)
            self.raza = raza

    Con esto, estamos indicando que el método _mostrar_edad, solo debería utilizarse desde dentro de la clase, y en las posibles subclases que tenga.

    En cambio, si lo ponemos así (__), estaríamos indicando que la restricción sea a nivel de clase; creando un método privado:

    def __mostrar_edad(self):
        print(f"Tengo {self.edad} años.")

    Es posible acceder al método desde fuera de la clase, con la misma sintaxis que con los atributos:

    _Clase__método

    Para acceder desde fuera de la clase al método del ejemplo, lo haríamos de esta forma:

    animal_1 = Animal("Chester", 3)
    
    animal_1._Animal__mostrar_edad()
    Resultado en la consola
    Tengo 3 años.

    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__:

    • Nombre (público)
    • Apellidos (público)
    • Edad (público)
    • Teléfono (protegido)
    • Contraseña (privado)

    Esta es la solución:

    class Usuario:
        def __init__(self, nombre, apellidos, edad, telefono, contrasena):
            self.nombre = nombre
            self.apellidos = apellidos
            self.edad = edad
            self._telefono = telefono
            self.__contrasena = contrasena

    23. Crea dos objetos de la clase Usuario.

    Como puedes apreciar, la instanciación se hace exactamente igual que con una clase que no utilice miembros protegidos y/o privados.

    usuario_1 = Usuario("Enrique",
                        "Barros Fernández",
                        32,
                        "123456789",
                        "3u{]%M90Vd;%")
    
    usuario_2 = Usuario("Gabriela",
                        "Saura Cuevas",
                        22,
                        "987654321",
                        "M26[BY*0^bF0")

    24. Añade un método getter que permita imprimir el teléfono de los usuarios al llamarlo.

    Haz la prueba con uno de los dos objetos.

    Creamos el método getter en la clase:

    # Getter
    def obtener_telefono(self):
        print(f"El teléfono es: {self._telefono}")

    Y finalmente, lo utilizamos con uno de los dos objetos:

    usuario_1.obtener_telefono()
    Resultado en la consola
    El teléfono es: 123456789

    25. Añade un método setter que permita modificar la contraseña de los usuarios.

    Creamos el método setter:

    def modificar_contrasena(self, contrasena):
        self.__contrasena = contrasena

    26. Añade un método getter que permita ver la contraseña de los usuarios.

    Añadimos el método getter:

    def obtener_contrasena(self):
        print(f"La contraseña es: {self.__contrasena}")

    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.

    usuario_2.modificar_contrasena("alGaSHanecIN")
    usuario_2.obtener_contrasena()
    Resultado en la consola
    La contraseña es: alGaSHanecIN

    28. Unifica el método setter y getter del ejercicio anterior, en una mezcla de ambos. Este método hará las dos tareas de una vez.

    Este método, combina las dos habilidades. Primero establece la contraseña, y luego indica que se estableció correctamente, y cuál es.

    def modificar_contrasena(self, contrasena):
        self.__contrasena = contrasena
        print("Contraseña establecida con éxito.")
        print(f"La nueva contraseña es: {self.__contrasena}")

    Al llamar al método, este es el resultado:

    usuario_2.modificar_contrasena("alGaSHanecIN")
    Resultado en la consola
    Contraseña establecida con éxito.
    La nueva contraseña es: alGaSHanecIN

    29. Crea un método getter que muestre los siguientes detalles de los usuarios:

    • Nombre
    • Apellidos
    • Edad

    Con este método getter, mostramos de una vez todos los atributos públicos del objeto:

    def mostrar_datos(self):
        print(f"Nombre: {self.nombre}.")
        print(f"Apellidos: {self.apellidos}.")
        print(f"Edad: {self.edad}.")

    Al llamarlo, verás una salida en consola como esta:

    usuario_1.mostrar_datos()
    Resultado en la consola
    Nombre: Enrique.
    Apellidos: Barros Fernández.
    Edad: 32.

    Te dejo el código que he creado yo, por si no te funciona alguna parte. Así puedes analizarlo bien:

    class Usuario:
        def __init__(self, nombre, apellidos, edad, telefono, contrasena):
            self.nombre = nombre
            self.apellidos = apellidos
            self.edad = edad
            self._telefono = telefono
            self.__contrasena = contrasena
    
        # Getters
        def obtener_telefono(self):
            print(f"El teléfono es: {self._telefono}")
    
        def mostrar_datos(self):
            print(f"Nombre: {self.nombre}.")
            print(f"Apellidos: {self.apellidos}.")
            print(f"Edad: {self.edad}.")
    
        # Setter
        def modificar_contrasena(self, contrasena):
            self.__contrasena = contrasena
            print("Contraseña establecida con éxito.")
            print(f"La nueva contraseña es: {self.__contrasena}")
    
    usuario_1 = Usuario("Enrique",
                        "Barros Fernández",
                        32,
                        "123456789",
                        "3u{]%M90Vd;%")
    
    usuario_2 = Usuario("Gabriela",
                        "Saura Cuevas",
                        22,
                        "987654321",
                        "M26[BY*0^bF0")


    Espacio publicitario