Wikipedia
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.
class Numero:
pi = 3.1415 # Atributo de clase
Numero.pi # podemos acceder sin problemas al atributo de clase desde fuera de la clase
3.1415
Ejemplo 2
Con atributos de instancia.
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
Alberto Alberto José
Ejemplo 3
Podemos acceder a los métodos de una clase desde fuera de ésta.
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())
Alberto Alberto José
# 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
'Alberto José'
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.
_
__
¶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.
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())
1234 Alberto José
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.
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
'Buenos días'
#hola.__cafe() # ERROR El método no es accesible
hola.desayuno() # Estoy tomando un café
Estoy tomando un café
Podemos ver los métodos y atributos accesibles de la instancia hola perteneciente a la clase Saludo, usando dir
.
dir(hola)
['_Saludo__cafe', '_Saludo__tardes', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'desayuno', 'dias']
Vemos muchos métodos especiales y además:
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.
hola._Saludo__tardes
'Buenas tardes'
hola._Saludo__cafe()
Estoy tomando un café
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.
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'
marianela.public() # podemos acceder perfectamente al método public de la superclase
Mi color favorito es el ROJO
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
Mi color favorito es el AZUL
dir(marianela) # se renombra, ahora el método se llama _Caperucita__colorSecreto
['_Caperucita__colorSecreto', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'public']
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.
dir(pitufina)
['_Caperucita__colorSecreto', '_Pitufo__colorSecreto', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'public']
Traslademos estas ideas a la clase Socio.
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)
pedro = Socio("Pedro", "1234")
pedro.password # muestra el hash almacenado, pero no la password original
'5773a02052b08a9af8885dc6b46e6c60b52d9cbcf1cb0111671637a2f3f95345'
pedro.check_password("1234") # comprueba que la clave es correcta
True
pedro.change_password('abcde') # cambiamos la password
pedro.password # con la password nueva cambia el hash
'11f6d55713fd13945e3551d8040cf297a47b75198887cd9c47d71e330bf5cd34'
pedro.change_password('1234') # volvemos a la password antigua
pedro.password # comprobamos que el hash es el antiguo
'5773a02052b08a9af8885dc6b46e6c60b52d9cbcf1cb0111671637a2f3f95345'