#!/usr/bin/env python # coding: utf-8 # Open In Colab # # Encapsulamiento en la POO # [Wikipedia](https://es.wikipedia.org/wiki/Encapsulamiento_(inform%C3%A1tica)) # En programación orientada a objetos, se denomina encapsulamiento al ocultamiento del estado interno de una clase al exterior. Encapsular consiste en hacer que los atributos y/o métodos internos a una clase no se puedan acceder ni modificar desde fuera de la propia clase. # # Cada objeto está aislado del exterior. El aislamiento protege a los datos asociados de un objeto contra su modificación por quien no tenga derecho a acceder a ellos, eliminando efectos secundarios e interacciones. Se evita que el usuario pueda cambiar su estado de maneras imprevistas e incontroladas. # # En JAVA las variables se indican si son públicas (Public) o privadas (Private). En Python por defecto todas los atributos y métodos de la clase son públicos, esto es, son accesibles desde fuera de la clase simplemente invocándolos. # # **Ejemplo 1** # Con un atributo de clase. # In[1]: class Numero: pi = 3.1415 # Atributo de clase Numero.pi # podemos acceder sin problemas al atributo de clase desde fuera de la clase # **Ejemplo 2** # Con atributos de instancia. # In[2]: class Socio: def __init__(self, nombre): self.nombre = nombre alberto = Socio('Alberto') # creamos una instancia de la clase Socio print(alberto.nombre) # imprimimos el atributo de instancia alberto.nombre = 'Alberto José' # podemos cambiar el atributo desde fuera de la clase sin problemas print(alberto.nombre) # podemos acceder el atributo desde fuera de la clase sin problemas # **Ejemplo 3** # Podemos acceder a los métodos de una clase desde fuera de ésta. # In[3]: class Socio: def __init__(self, nombre): self.nombre = nombre def set_nombre(self, nombre): self.nombre = nombre def get_nombre(self): return self.nombre alberto = Socio('Alberto') print(alberto.get_nombre()) # accedemos, sin problemas, a un método desde fuera de la clase alberto.set_nombre('Alberto José') # accedemos, sin problemas, a un método desde fuera de la clase print(alberto.get_nombre()) # In[4]: # También podemos acceder a un atributo desde fuera de la clase, no está protegido, no es privado alberto.nombre # accedemos también al atributo nombre desde fuera de la clase # El código anterior se asemeja al estilo JAVA con los setter y los getter. # # Son **públicos** tanto los métodos *set_nombre* y *get_nombre*, como el atributo *nombre*. # # Modificadores de acceso ```_``` ```__``` # ## _privado # # Elementos privados usando como prefijo una barra baja _ # # Por convenio, si el programador pone una barra baja precediendo el nombre de los atributos o métodos está indicando a otros desarrolladores, o a sí mismo, que esos elementos no deben ser accesibles desde fuera de la clase, no deben ser expuestos ni modificados externamente. Pero esto es solo una sugerencia que el intérprete de Python no considera como obligación y no impide el acceso desde fuera de la clase. # # Veamos un ejemplo donde pese a la barra baja como prefijo se puede acceder perfectamente desde fuera de la clase a los atributos y a los métodos. # In[5]: class Socio: def __init__(self, nombre, password): self.nombre = nombre self._password = password def _set_nombre(self, nombre): self.nombre = nombre def get_nombre(self): return self.nombre alberto = Socio('Alberto', '1234') print(alberto._password) # nada nos impide acceder desde fuera de la clase a un atributo con prefijo _ # incluso podemos acceder desde fuera a un atributo tan sensible como password alberto._set_nombre('Alberto José') # nada nos impide acceder desde fuera de la clase a un método con prefijo _ print(alberto.get_nombre()) # ## __protegido # # Elementos protegidos usando como prefijo dos barras bajas __ # # Protegeremos los métodos y atributos cuando deseemos evitar que un usuario modifique el estado interno de la instancia. Protegiéndolos nos asegurarnos que el atributo no pueda ser modificado ni accedido desde fuera de la clase. # # **Nota** # Como en Python no existen atributos y métodos privados propiamente dichos, muchos desarrolladores de Python denominan como Privados a los elementos que llevan las dos barras bajas como prefijo. Muchos programadores llaman privados a los elementos que van con dos barras bajas como prefijo, porque esto evita que estos métodos y atributos sean accedidos o modificados externamente. # In[6]: class Saludo: dias = "Buenos días" # accesible desde el exterior __tardes = "Buenas tardes" # no accesible def __cafe(self): # no accesible desde el exterior print("Estoy tomando un café") def desayuno(self): # accesible desde el exterior self.__cafe() # el método si es accesible desde el interior hola = Saludo() hola.dias # Buenos días #hola.__tardes # ERROR El atributo no es accesible # In[7]: #hola.__cafe() # ERROR El método no es accesible hola.desayuno() # Estoy tomando un café # Podemos ver los métodos y atributos accesibles de la instancia *hola* perteneciente a la clase *Saludo*, usando ```dir```. # In[8]: dir(hola) # Vemos muchos métodos especiales y además: # * vemos el método *desayuno* # * vemos el atributo de clase *dias* # * no podemos encontrar el método *\__cafe* → → → pero aparece otro método llamado _Saludo__cafe # * no podemos encontrar el atributo *\__tardes* → → → pero aparece otro atributo llamado _Saludo__tardes # # Si podemos acceder a estos métodos y atributos que inicialmente parecen ocultos, lo que sucede es que el intérprete de Python los ha cambiado de nombre para ocultarlos y evitar su uso. Podemos llamarlos de la siguiene manera aunque **no se recomienda su uso**. # In[9]: hola._Saludo__tardes # In[10]: hola._Saludo__cafe() # ## Proteger el acceso # Proteger el acceso usando la doble barra baja como prefijo tanto para métodos como para atributos. # # Originariamente la doble barra baja se estableció para que el intérprete de Python renombra el elemento (fuera método o atributo) para evitar colisiones con las subclases. Aunque la idea original era evitar colisiones, pronto los desarrolladores comprendieron que el uso de la doble barra baja servía para prevenir accesos no autorizados. # In[11]: class Caperucita: def __colorSecreto(self): print('Mi color favorito es el ROJO') def public(self): self.__colorSecreto() class Pitufo(Caperucita): def __colorSecreto(self): # la subclase tiene el mismo nombre de método que la clase print('Mi color favorito es el AZUL') def public(self): # la subclase tiene el mismo nombre de método que la clase self.__colorSecreto() marianela=Caperucita() # pocos saben que el verdadero nombre de Caperucita Roja era Marianela #marianela.__colorSecreto() # ERROR no se puede acceder a un método protegido con dos barras bajas # AttributeError: 'Caperucita' object has no attribute '__colorSecreto' # In[12]: marianela.public() # podemos acceder perfectamente al método public de la superclase # In[13]: pitufina = Pitufo() # creamos una instancia de la subclase pitufina.public() # la instancia de la subclase llama a su método public, no hay colisión # In[14]: dir(marianela) # se renombra, ahora el método se llama _Caperucita__colorSecreto # Al poner la doble barra baja en el método \__colorSecreto la instancia Marianela no puede acceder a él, obteniendo el error *AtributeError*, dando a entender que este elemento no existe. Lo que ha sucedido es que tiene otro nombre. Al listar los atributos y métodos de la clase Caperucita vemos que fue renombrado, ahora se llama _Caperucita__colorSecreto. # Vamos a listar los métodos y atributos de pitufina que es una instancia de la clase hija. # Podemos observar dos métodos colorSecreto uno de la clase padre y otro de la clase hija. # * _Caperucita__colorSecreto # * _Pitufo__colorSecreto # In[15]: dir(pitufina) # Traslademos estas ideas a la clase Socio. # In[16]: import hashlib class Socio: def __init__(self, nombre, password): '''Construcctor. La password se encriptará antes de ser almacenada.''' self.nombre = nombre self.password = self.__encripta_pw(password) def __encripta_pw(self, password): # método privado (protegido) '''Encripta la password con el nombre de usuario y retorna el sha.''' hash_string = (self.nombre + password) hash_string = hash_string.encode("utf8") return hashlib.sha256(hash_string).hexdigest() def check_password(self, password): '''Retorna True si la password es válida para el usuario, en caso contrario retorna False.''' encriptada = self.__encripta_pw(password) return encriptada == self.password def change_password(self, password): '''Permite cambiar la password de un usuario, alterando el hash.''' self.password = self.__encripta_pw(password) # In[17]: pedro = Socio("Pedro", "1234") pedro.password # muestra el hash almacenado, pero no la password original # In[18]: pedro.check_password("1234") # comprueba que la clave es correcta # In[19]: pedro.change_password('abcde') # cambiamos la password pedro.password # con la password nueva cambia el hash # In[20]: pedro.change_password('1234') # volvemos a la password antigua pedro.password # comprobamos que el hash es el antiguo