Disponemos de la clase Carrera, por ejemplo, ingeniería, en la que vamos a añadir en un diccionario las materias (álgebra, física, química, ...) con un código identificador de esta materia.
Creamos el método agregarMateria para que dado el nombre de la materia y el código podamos añadir los elementos clave (que es el código) y valor (que es el nombre de la materia) al diccionario. Si hubiéramos usado listas se haría con append, pero al tratarse de un diccionario se hace con el nombre del diccionario (materias) así:
materias[codigo] = materia
por ejemplo
materias[111] = "cálculo"
Luego, tenemos la clase Materia con dos atributos en el constructor: materia y profesor.
Nos gustaría poder proteger (ocultar) el acceso a algunos atributos y métodos.
class Carrera:
def __init__(self, nombre):
self.nombre = nombre
self.materias = {} # diccionario clave:valor
def agregarMateria(self, materia, codigo):
self.materias[codigo] = materia # como es un diccionario se agrega así.
class Materia:
def __init__(self, nombre, profesor):
self.nombre = nombre
self.profesor = profesor
ing = Carrera("Ingeniería")
algebra = Materia("Álgebra", "Rosa López")
fisica = Materia("Física", "Roberto Ruiz")
quimica = Materia("Química", "Sonia Arce")
ing.agregarMateria(algebra, 345)
ing.materias # vemos que se ha agregado una materia
{345: <__main__.Materia at 0x782829e80d90>}
ing.agregarMateria(fisica, 589)
ing.materias # vemos que se han agregado dos materias
{345: <__main__.Materia at 0x782829e80d90>, 589: <__main__.Materia at 0x782829e801f0>}
Métodos getter y setter: usados para gestionar el ocultamiento. Los métodos Get y Set están traídos de la programación al estilo JAVA.
En Python se usan más las properties (propiedades) esto supone que a partir de los atributos vamos a crear propiedades que son las que definen a un objeto. Vamos a enmascarar estos atributos dentro de propiedades. Podemos hacerlo con todos los atributos o con algunos concretos.
Vamos a crear una property. Supongamos que definimos un nuevo atributo llamado inicio que recoge el año en el que se inició la materia. Luego lo que haremos es impedir que una materia se pueda iniciar antes del año 2019 que fue el año de creación de la titulación.
Al añadir un decorador, cada vez que queramos saber la fecha de inicio de una materia tendremos que pasar por el método inicio que nos la proporcionará. Para comprobar que esto es así, añadimos un print en el método que indique que está actuando el método inicio. La property se articula creando un método que se llamará igual que el atributo inicio.
class Carrera:
def __init__(self, nombre):
self.nombre = nombre
self.materias = {}
def agregarMateria(self, materia, codigo): # nuevo atributo inicio
self.materias[codigo] = materia
class Materia:
def __init__(self, nombre, profesor, fecha):
self.nombre = nombre
self.profesor = profesor
self.inicio = fecha # año de inicio de una materia
@property # decorador (decorator)
def inicio(self): # este método lo único que hace es devolver el atributo
print("El método inicio es el que nos proporciona el año de inicio para esta materia.")
return self._inicio # OJO el atributo lleva un prefijo de barra baja que indica
# que es privado (oculto, protegido) pero esto solo es un convenio
@inicio.setter
def inicio(self, fecha):
if fecha < 2019:
self._inicio = 2019 # cambiamos la fecha para que no sea anterior al año de creación
print(f"La titulación comenzó en 2019, por lo que la materia no puede ser de {fecha}.")
else:
self._inicio = fecha # dejamos la fecha que nos dan
ing = Carrera("Ingeniería")
algebra = Materia("Álgebra", "Rosa López", 2004)
fisica = Materia("Física", "Roberto Ruiz", 2019)
quimica = Materia("Química", "Sonia Arce", 2021)
La titulación comenzó en 2019, por lo que la materia no puede ser de 2004.
algebra.inicio # no imprime directamente la fecha de inicio, primero pasa por la
# property e imprime la frase indicada
El método inicio es el que nos proporciona el año de inicio para esta materia.
2019
La property enmascara o envuelve al atributo dentro de una propiedad.
El setter es el que ha validado la fecha, comprobando si se trataba de un año anterior o no a 2019, año de creación de la titulación, para evitar que la fecha de inicio de una materia pueda llegar a introducirse por error con un año anterior al inicio de esa carrera.
quimica.inicio # imprime el año 2021 que es el proporcionado,
# ya que no es anterior a 2019
El método inicio es el que nos proporciona el año de inicio para esta materia.
2021
Esta es una forma de ocultamiento de los datos. Si solicitamos el año de inicio de una materia directamente nos dirá que esa variable no existe, dando un error.
algebra.nombre
'Álgebra'
algebra.profesor
'Rosa López'
#algebra.fecha # AttributeError: 'Materia' object has no attribute 'fecha'
Nos gustaría poder proteger el acceso directo al diccionario de materias evitando crear una materia nueva sin necesidad de pasar por el método agregarMateria, que hasta ahora es privado por convenio pero se puede saltar así.
ing.materias[111] = "cálculo" # así conseguimos acceder directamente al diccionario sin pasar
# por el método agregarMateria
ing.materias # veamos que hemos borrado el contenido anterior del diccionario
# ha quedado solo la nueva materia inclida directamente
{111: 'cálculo'}
ing.materias[999] = fisica # añadiendo ahora el objeto fisica
len(ing.materias)
2
ing.materias # podemos ver que es un lio tener objetos creados por dos métodos
{111: 'cálculo', 999: <__main__.Materia at 0x782829e816c0>}
Lo mejor es ocultar o proteger la posibilidad de que el usuario u otros programadores puedan acceder a los atributos y métodos de forma directa.
__atributo
¶class Carrera:
def __init__(self, nombre):
self.nombre = nombre
self.__materias = {} # prefijo __
def agregarMateria(self, materia, codigo):
self.__materias[codigo] = materia # prefijo __
def getMaterias(self): # nuevo método para listar todas las materias
print(self.__materias.items())
class Materia:
def __init__(self, nombre, profesor, fecha):
self.nombre = nombre
self.profesor = profesor
self.inicio = fecha
@property
def inicio(self):
return self._inicio
@inicio.setter
def inicio(self, fecha):
if fecha < 2019:
self._inicio=2019
else:
self._inicio=fecha
ing = Carrera("Ingeniería")
algebra = Materia("Álgebra", "Rosa López", 2004)
fisica = Materia("Física", "Roberto Ruiz", 2019)
quimica = Materia("Química", "Sonia Arce", 2021)
Cuanto se pone un prefijo de dos barras bajas a un atributo el intérprete de Python renombra el atributo.
Veamos que ahora el atributo __materias se llama _Carrera__materias
dir(ing)
['_Carrera__materias', '__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__', 'agregarMateria', 'getMaterias', 'nombre']
ing.agregarMateria(algebra, 345)
ing.agregarMateria(fisica, 589)
#ing.__materias # AttributeError: 'Carrera' object has no attribute '__materias'
Aunque podemos hacer trampa y acceder de todas formas sabiendo que el atributo ahora se llama _Carrera__materias
ing._Carrera__materias # vemos que está funcionando
{345: <__main__.Materia at 0x782829e827d0>, 589: <__main__.Materia at 0x782829e810f0>}
Intentemos hacer una copia del atributo _Carrera__materias perteneciente al objeto ing.
duplicadoMaterias = ing._Carrera__materias # vemos que funciona, no da error
El problema de los duplicados de un diccionario, o lista, es que la copia ocupa el mismo lugar en memoria. De esta forma, si se modifica o borra la copia, al original le sucederá lo mismo. Esto es bastante grave, ya que otro programador podría hacer una copia de ese diccionario, si tiene acceso a él y modificarlo, alterando completamente nuestra aplicación.
Lo que se debe hacer es ocultar bien, ese método incluso de otros programadores, o de nosotros mismos que en el futuro podamos olvidar los detalles de la aplicación que estamos creando.
duplicadoMaterias # mismas direcciones de memoria que el original
{345: <__main__.Materia at 0x782829e827d0>, 589: <__main__.Materia at 0x782829e810f0>}
Si agregamos una nueva materia (química) al diccionario de materias, podemos comprobar que también se ha añadido a la copia.
ing._Carrera__materias[888] = quimica # añadimos la materia química al diccionario
ing._Carrera__materias
{345: <__main__.Materia at 0x782829e827d0>, 589: <__main__.Materia at 0x782829e810f0>, 888: <__main__.Materia at 0x782829e829b0>}
duplicadoMaterias # la copia tiene tb 3 materias con la misma dirección de memoria
{345: <__main__.Materia at 0x782829e827d0>, 589: <__main__.Materia at 0x782829e810f0>, 888: <__main__.Materia at 0x782829e829b0>}
duplicadoMaterias.pop(345) # eliminamos un elemento del diccionario duplicado
duplicadoMaterias
{589: <__main__.Materia at 0x782829e810f0>, 888: <__main__.Materia at 0x782829e829b0>}
ing._Carrera__materias # también se ha eliminado la materia en el diccionario original
{589: <__main__.Materia at 0x782829e810f0>, 888: <__main__.Materia at 0x782829e829b0>}
ing.getMaterias() # invocamos el método que nos lista internamente las materias
dict_items([(589, <__main__.Materia object at 0x782829e810f0>), (888, <__main__.Materia object at 0x782829e829b0>)])