Skip to content

13. Las clases genéricas [BaseEntity] y [MyException]

Ahora definimos dos clases que utilizaremos habitualmente a partir de ahora.

Image

13.1. La clase MyException

La clase [MyException] (MyException.py) proporciona una clase de excepciones propia:


# une classe d'exception propriétaire dérivant de [BaseException]
class MyException(BaseException):
    # constructeur
    def __init__(self: object, code: int, message: str):
        # parent
        BaseException.__init__(self, message)
        # code erreur
        self.code = code

    # toString
    def __str__(self):
        return f"MyException[{self.code}, {super().__str__()}]"

    # getter
    @property
    def code(self) -> int:
        return self.__code

    # setter
    @code.setter
    def code(self, code: int):
        # le code d'erreur doit être un entier positif
        if isinstance(code, int) and code > 0:
            self.__code = code
        else:
            # exception
            raise BaseException(f"code erreur {code} incorrect")

Notas

  • línea 2: la clase [MyException] deriva de la clase predefinida [BaseException];
  • línea 4: el constructor admite dos parámetros:
    • [code]: un código de error entero;
    • [message]: un mensaje de error;
  • línea 6: se pasa el mensaje de error a la clase padre;
  • líneas 14-27: el atributo [code] se maneja mediante un getter/setter;
  • líneas 23-24: se comprueba la validez del atributo [code]: debe ser un entero >0;

13.2. La clase [BaseEntity]

La clase [BaseEntity] será la clase padre de la mayoría de las clases que crearemos para encapsular información sobre un objeto. A continuación utilizaremos principalmente dos tipos de clases:

  • clases cuyo único objetivo es encapsular en un mismo lugar la información sobre un mismo objeto. Estas no tendrán comportamientos (métodos) distintos de los getter/setter y una función de visualización (__str__). Si hay N objetos que gestionar, estas clases se instancian N veces. [BaseEntity] será la clase padre de este tipo de clases;
  • clases cuya función principal es encapsular métodos y muy poca información. Estas clases solo se instanciarán una vez (singleton). Su función es implementar los algoritmos de una aplicación;

La clase [BaseEntity] es la siguiente:


# importaciones
import json
import re

from MyException import MyException


class BaseEntity(object):
    # propiedades excluidas del estado de la clase
    excluded_keys = []

    # propiedades de la clase
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identificador del objeto
        return ["id"]

    # toString
    def __str__(self) -> str:
        return self.asjson()

    # getter
    @property
    def id(self) -> int:
        return self.__id

    #  setter
    @id.setter
    def id(self, id):
        #: id debe ser un entero >=0
        try:
            id = int(id)
            erreur = id < 0
        except:
            erreur = True
        # ¿error?
        if erreur:
            raise MyException(1, f"L'identifiant d'une entité {self.__class__} doit être un entier >=0")
        else:
            self.__id = id

    def fromdict(self, state: dict, silent=False):
        

    def set_value(self, key: str, value, new_attributes) -> dict:
        

    def asdict(self, included_keys: list = None, excluded_keys: list = []) -> dict:
        

    def asjson(self, excluded_keys: list = []) -> str:
        

    def fromjson(self, json_state: str):
        

Comentarios

  • El objetivo de la clase [BaseEntity] es facilitar las conversiones Objeto / Diccionario y Objeto / jSON. De este modo, se ofrecen los siguientes métodos:
    • [asdict]: devuelve el diccionario de propiedades del objeto;
    • [fromdict]: crea un objeto a partir de un diccionario;
    • [asjson]: devuelve la cadena jSON del objeto, al igual que la función [__str__];
    • [fromjson]: crea un objeto a partir de su cadena jSON;
  • la clase [BaseEntity] está pensada para ser derivada y no para ser utilizada tal cual;
  • líneas 22-25: la clase [BaseEntity] solo tiene una propiedad, el entero [id]. Esta propiedad es el identificador del objeto. En la práctica, a menudo resulta útil poder diferenciar las instancias de una misma clase. Lo haremos con esta propiedad, única para cada instancia. Por otra parte, los objetos suelen proceder de bases de datos donde se identifican mediante una clave primaria, generalmente un entero. En estos casos, [id] será la clave primaria;
  • líneas 27-40: el setter de la propiedad [id]. Se comprueba que sea un entero >=0. Si no es así, se lanza una excepción de tipo [MyException] (línea 39);
  • línea 10: [excluded_keys] es un atributo de clase y no de instancia. Por lo tanto, se escribirá [BaseEntity.excluded_keys]. Este atributo de clase es una lista que contiene las propiedades de clase que no participan en las conversiones Objeto / Diccionario y Objeto / jSON;
  • líneas 12-16: [get_allowed_keys] devuelve la lista de propiedades de la clase. En una conversión Diccionario -> Objeto o jSON -> Objeto, solo se aceptarán las claves presentes en esta lista. Cada clase derivada de la clase [BaseEntity] deberá redefinir esta lista;

Hay que entender aquí que las propiedades y funciones de la clase [BaseEntity] son accesibles para las clases derivadas de [BaseEntity]. Este es el punto importante que hay que comprender.

Vamos a detallar el código de la clase [BaseEntity]. Es bastante avanzado. El lector principiante puede limitarse a leer la función de cada método sin detenerse en su código.

13.2.1. El método [BaseEntity.fromdict]

13.2.1.1. Définition

El método [fromdict] permite inicializar un objeto [BaseEntity] o derivado a partir de un diccionario:


def fromdict(self, state: dict, silent=False):
        # se actualiza el objeto
        # claves permitidas
        allowed_keys = self.__class__.get_allowed_keys()
        # recorre las claves de estado
        for key, value in state.items():
            # ¿Está autorizada la clave?
            if key not in allowed_keys:
                if not silent:
                    raise MyException(2, f"la clé {key} n'est pas autorisée")
            else:
                # se intenta asignar el valor a la clave
                # se permite que se propague la posible excepción
                setattr(self, key, value)
        # se devuelve el objeto
        return self

Comentarios

  • línea 1: la función recibe como parámetro el diccionario [state] a partir del cual se inicializará el objeto actual;
  • línea 4: se invoca la función estática [get_allowed_keys] de la clase que llamó a la función [fromdict]. Si se trata de una clase derivada de [BaseEntity] y dicha clase derivada ha redefinido la función estática [get_allowed_keys], entonces se llama a la función [get_allowed_keys]. Cada clase derivada redefine esta función estática para declarar en ella sus propiedades;
  • línea 6: se recorren las claves y los valores del diccionario [state];
  • línea 8: si la clave [key] no forma parte de las propiedades de la clase, entonces:
    • se ignora;
    • se lanza una excepción (línea 10). El desarrollador indica lo que desea pasando el parámetro correcto [silent] (línea 1). El valor por defecto de [silent] hace que se lance una excepción si se intenta inicializar el objeto con una propiedad que no tiene;
  • línea 14: si la clave forma parte de las propiedades del objeto, entonces se le asigna al objeto [self] mediante la función predefinida [setattr];
  • línea 16: la función devuelve el objeto inicializado;

13.2.1.2. Exemples

Image

13.2.1.2.1. La clase [Utils]

La clase [Utils] (Utils.py) es la siguiente:


class Utils:
    # método estático
    @staticmethod
    def is_string_ok(string: str) -> bool:
        # ¿Es string una cadena?
        erreur = not isinstance(string, str)
        if not erreur:
            # ¿está vacía la cadena?
            erreur = string.strip() == ''
        # resultado
        return not erreur

En las líneas 3-11, define un método estático que devuelve un valor booleano verdadero si su parámetro [str] es una cadena de caracteres no vacía;

13.2.1.2.2. La clase [Personne]

La clase [Personne] (Personne.py) deriva de la clase [BaseEntity]:


# importaciones
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils


# clase Persona
class Personne(BaseEntity):
    # propiedades excluidas del estado de la clase
    excluded_keys = []

    # Propiedades de la clase
    # id: identificador de la persona
    # nombre: nombre de la persona
    # apellido: apellido de la persona
    # edad: edad de la persona
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identificador del objeto
        return BaseEntity.get_allowed_keys() + ["nom", "prénom", "âge"]

    # getters
    @property
    def prénom(self) -> str:
        return self.__prénom

    @property
    def nom(self) -> str:
        return self.__nom

    @property
    def âge(self) -> int:
        return self.__âge

    # setters
    @prénom.setter
    def prénom(self, prénom: str):
        # el nombre no puede estar vacío
        if Utils.is_string_ok(prénom):
            self.__prénom = prénom.strip()
        else:
            raise MyException(11, "Le prénom doit être une chaîne de caractères non vide")

    @nom.setter
    def nom(self, nom: str):
        # el nombre no debe estar vacío
        if Utils.is_string_ok(nom):
            self.__nom = nom.strip()
        else:
            raise MyException(12, "Le nom doit être une chaîne de caractères non vide")

    @âge.setter
    def âge(self, âge: int):
        # la edad debe ser un número entero >=0
        erreur = False
        if isinstance(âge, int):
            if âge >= 0:
                self.__âge = âge
            else:
                erreur = True
        else:
            erreur = True
        # ¿error?
        if erreur:
            raise MyException(13, "L'âge doit être un entier >=0")
  • línea 8: la clase [Personne] deriva de la clase [BaseEntity];
  • líneas 8-65: se ha conservado lo esencial de la clase [Personne] ya vista. Las diferencias son las siguientes:
    • la clase ya no tiene constructor;
    • la clase utiliza la excepción [MyException], ejemplo en la línea 65;
    • tiene un método estático, [get_allowed_keys], líneas 17-20, que define la lista de sus propiedades. Las propiedades propias de la clase [Personne] se añaden a las de la clase padre [BaseEntity];
    • tiene una lista estática [excluded_keys] sobre la que volveremos más adelante;
13.2.1.2.3. La clase [Enseignant]

La clase [Enseignant] (Enseignant.py) deriva de la clase [Personne]:


# importaciones
from MyException import MyException
from Personne import Personne
from Utils import Utils


# clase Profesor
class Enseignant(Personne):
    # propiedades excluidas del estado de la clase
    excluded_keys = []

    # propiedades de la clase
    # id: identificador de la persona
    # nombre: nombre de la persona
    # apellido: apellido de la persona
    # edad: edad de la persona
    # disciplina: disciplina impartida
    @staticmethod
    def get_allowed_keys() -> list:
        # id: identificador del objeto
        return Personne.get_allowed_keys() + ["discipline"]

    # propiedades
    @property
    def discipline(self) -> str:
        return self.__discipline

    @discipline.setter
    def discipline(self, discipline: str):
        # la disciplina debe ser una cadena no vacía
        if Utils.is_string_ok(discipline):
            self.__discipline = discipline
        else:
            raise MyException(21, "La discipline doit être une chaîne de caractères non vide")

    # método show
    def show(self):
        print(f"Enseignant[{self.id}, {self.prénom}, {self.nom}, {self.âge}]")
  • línea 8: la clase [Enseignant] extiende (o deriva) de la clase [Personne];
  • líneas 18-21: definen la lista de propiedades de la clase;
  • líneas 37-38: el método [show] muestra la identidad del profesor;
13.2.1.2.4. La configuración [config]

Los scripts de ejemplo utilizan la siguiente configuración [config]:


def configure():
    import os

    # carpeta del archivo de configuración
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # rutas absolutas de las carpetas que se deben incluir en el syspath
    absolute_dependencies = [
        # la clase BaseEntity
        f"{script_dir}/entities",
    ]

    # actualización de syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # se realiza la configuración
    return {}
  • líneas 8-10: las carpetas que contienen las dependencias del proyecto;
  • líneas 14-15: se compila el Python Path;
  • línea 18: se devuelve un diccionario vacío (no hay más configuraciones aparte de la de syspath);
13.2.1.2.5. El script [fromdict_01]

El script [fromdict_01] es el siguiente:


# se configura la aplicación
import config

config = config.configure()

# el syspath está configurado: ya se pueden realizar las importaciones
from Enseignant import Enseignant

# un profesor
enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "paul", "âge": 56})
enseignant1.show()
  • Línea 10: se crea un objeto [Enseignant] a partir de un diccionario. Para ello, se utiliza el constructor predeterminado de la clase para crear un objeto [Enseignant] al que se le aplica el método [fromdict]. Hay que tener en cuenta que, en este caso, el método [fromdict] que se ejecuta es el de la clase padre [BaseEntity]. De hecho:
    • el método [fromdict] se busca primero en la clase [Enseignant]. No existe;
    • a continuación, se busca en la clase padre [Personne]. No existe;
    • luego se busca en la clase padre [BaseEntity]. Existe;
  • línea 11: se muestra el objeto [Enseignant];

Los resultados son los siguientes:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/fromdict_01.py
Enseignant[1, paul, lourou, 56]

Process finished with exit code 0
13.2.1.2.6. El script [fromdict_02]

El script [fromdict_02] es el siguiente:


# se configura la aplicación
import config

config = config.configure()

# la ruta del sistema está configurada; se pueden realizar las importaciones
from Enseignant import Enseignant

# un profesor
enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "", "âge": 56})
enseignant1.show()
  • línea 10: se crea un profesor con el nombre en blanco. Esto debe generar una excepción, ya que la clase [Personne] no admite nombres en blanco. Este ejemplo muestra la diferencia entre un diccionario y un objeto. Este último puede verificar la validez de sus propiedades, pero el diccionario no;

Los resultados son los siguientes:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/fromdict_02.py
Traceback (most recent call last):
  File "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/fromdict_02.py", line 10, in <module>
    enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "", "âge": 56})
  File "C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\classes\02/entities\BaseEntity.py", line 55, in fromdict
    setattr(self, key, value)
  File "C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\classes\02/entities\Personne.py", line 42, in prénom
    raise MyException(11, "Le prénom doit être une chaîne de caractères non vide")
MyException.MyException: MyException[11, Le prénom doit être une chaîne de caractères non vide]

Process finished with exit code 1
13.2.1.2.7. El script [fromdict_03]

El script [fromdict_03] es el siguiente:


# se configura la aplicación
import config

config = config.configure()

# el syspath está configurado; se pueden realizar las importaciones
from Enseignant import Enseignant

# un profesor
enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "albert", "âge": 56, "sexe": "M"})
enseignant1.show()
  • línea 10: se crea un profesor a partir de un diccionario que contiene una clave (sexo) que no pertenece a la clase [Enseignant]. En ese caso, debería lanzarse una excepción;

Los resultados son los siguientes:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/fromdict_03.py
Traceback (most recent call last):
  File "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/fromdict_03.py", line 10, in <module>
    enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "albert", "âge": 56, "sexe": "M"})
  File "C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\classes\02/entities\BaseEntity.py", line 51, in fromdict
    raise MyException(2, f"la clé [{key}] n'est pas autorisée")
MyException.MyException: MyException[2, la clé [sexe] n'est pas autorisée]

Process finished with exit code 1
13.2.1.2.8. El script [fromdict_04]

El script [fromdict_04] es una copia de [fromdict_03] con una pequeña diferencia:


# se configura la aplicación
import config

config = config.configure()

# el syspath está configurado; se pueden realizar las importaciones
from Enseignant import Enseignant

# un profesor
enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "albert", "âge": 56, "sexe": "M"}, silent=True)
enseignant1.show()
  • línea 10: se ha utilizado el parámetro [silent=True] para indicar que, si una clave del diccionario no es una propiedad de la clase [Enseignant], simplemente debe ignorarse. En este caso, no se lanzará ninguna excepción;

Los resultados son los siguientes:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/fromdict_04.py
Enseignant[1, albert, lourou, 56]

Process finished with exit code 0

13.2.2. El método [BaseEntity.asdict]

13.2.2.1. Définition

El método [BaseEntity.asdict] devuelve un diccionario cuyas claves son las propiedades del objeto:


    def asdict(self, included_keys: list = None, excluded_keys: list =[]) -> dict:
        # attributs de l'objet
        attributes = self.__dict__
        # les nouveaux attributs
        new_attributes = {}
        # on parcourt les attributs
        for key, value in attributes.items():
            # si la clé est explicitement demandée
            if included_keys and key in included_keys:
                self.set_value(key, value, new_attributes)
            # sinon, si la clé n'est pas exclue
            elif not included_keys and key not in self.__class__.excluded_keys and key not in excluded_keys:
                self.set_value(key, value, new_attributes)
        # on rend le dictionnaire des attributs
        return new_attributes

Comentarios

  • línea 1: la función [asdict] devuelve el diccionario de propiedades del objeto;
  • línea 1: [included_keys]: la lista de claves que se deben incluir en el diccionario;
  • línea 1: [excluded_keys]: la lista de claves que se deben excluir del diccionario;
  • línea 3: la propiedad [self.__dict__] devuelve el diccionario de propiedades del objeto. Los nombres de las propiedades son las claves y sus valores, los valores del diccionario. Un objeto puede contener referencias a otros objetos. En ese caso, los nombres de las propiedades van precedidos por el nombre de la clase a la que pertenecen. Esto es algo que no queremos. Queremos las propiedades sin su prefijo;
  • línea 3: hay que entender aquí que si la función [asdict] se ejecuta dentro de una clase derivada de [BaseEntity], la propiedad [self.__dict__] devuelve el diccionario de propiedades del objeto derivado;
  • línea 5: el diccionario que vamos a construir;
  • línea 7: se recorren los valores de [self.__dict__] en forma de (clave, valor);
  • línea 9: si la clave actual figura en la lista de claves que deben incluirse, se añade al diccionario [new_attributes] mediante la función [set_value], que describiremos a continuación;
  • línea 12: si el parámetro [included_keys] no está presente, se utiliza el parámetro [excluded_keys]. Si la propiedad no forma parte de las propiedades que deben excluirse, se añade al diccionario [new_attributes];
  • línea 12: hay varias formas de excluir una propiedad del diccionario:
    • se ha definido en el atributo de clase [excluded_keys];
    • se ha definido en la lista [excluded_keys] pasada a la función [asdict];
    • el parámetro [included_keys] está presente y no incluye la propiedad;
  • línea 15: se devuelve el diccionario [new_attributes]

La función [set_value] de las líneas 10 y 13 es la siguiente:


    @staticmethod
    def set_value(key: str, value, new_attributes: dict):
        # las claves pueden tener el formato __Class__key
        match = re.match("^.*?__(.*?)$", key)
        if match:
            # se anota la nueva clave
            newkey = match.groups()[0]
        else:
            # la clave permanece sin cambios
            newkey = key
        # se inserta la nueva clave en el diccionario [new_attributes]
        # transformando, si es necesario, el valor asociado a uno de los tipos
        # dict, list, tipo simple
        new_attributes[newkey] = BaseEntity.check_value(value)

Comentarios

  • línea 4: se comprueba si la clave tiene el formato __Class_key. Este es el formato que tiene si pertenece a un objeto incluido en el objeto principal. En este caso, solo se quiere conservar la cadena [key];
  • línea 7: solo se conserva la cadena que sigue a los dos últimos caracteres subrayados de la cadena;
  • líneas 8-10: si la clave no tiene el formato __Class_key, se conserva tal cual;
  • líneas 11-14: el valor asociado a la clave [newkey] se calcula mediante el método estático [BaseEntity.check_value];

El método estático [BaseEntity.check_value] es el siguiente:


    @staticmethod
    def check_value(value):
        # el valor puede ser de tipo BaseEntity, lista, diccionario o un tipo simple
        # ¿Es el valor una instancia de BaseEntity?
        if isinstance(value, BaseEntity):
            value2 = value.asdict()
        # value de tipo lista?
        elif isinstance(value, list):
            value2 = BaseEntity.list2list(value)
        #es de tipo diccionario?
        elif isinstance(value, dict):
            value2 = BaseEntity.dict2dict(value)
        #es de tipo simple?
        else:
            value2 = value
        # se devuelve el resultado
        return value2
  • línea 1: el método [check_value] es estático (método de clase y no de instancia). Recibe como parámetro el valor que se va a asociar a una clave del diccionario:
    • línea 17: si este valor es de tipo simple, permanece sin cambios;
    • líneas 5-6: si este valor es de tipo BaseEntity, el valor se sustituye por su diccionario. Se produce entonces una llamada recursiva;
    • líneas 8-9: si este valor es una lista, entonces se sustituye por el valor [BaseEntity.list2list];
    • líneas 11-12: si este valor es un diccionario, se sustituye por el valor [BaseEntity.dict2dict];

El método estático [BaseEntity.list2list] es el siguiente:


    @staticmethod
    def list2list(liste: list) -> list:
        # se inspeccionan los elementos de la lista
        newlist = []
        for value in liste:
            newlist.append(BaseEntity.check_value(value))
        # se devuelve la nueva lista
        return newlist
  • línea 2: el método recibe una lista y devuelve una lista;
  • líneas 5-6: se sustituye cada valor de la lista recibida como parámetro por el valor devuelto por el método estático [BaseEntity.check_value]. Por lo tanto, se trata de una llamada recursiva. El método estático [BaseEntity.check_value] se invoca hasta que su parámetro [value] sea un tipo simple (no un tipo BaseEntity, ni una lista ni un diccionario);

El método estático [BaseEntity.dict2dict] es el siguiente:


    @staticmethod
    def dict2dict(dictionary: dict) -> dict:
        # se inspeccionan los elementos del diccionario
        newdict = {}
        for key, value in dictionary.items():
            newdict[key] = BaseEntity.check_value(value)
        # se devuelve el nuevo diccionario
        return newdict
  • línea 2: el método recibe un diccionario y devuelve un diccionario;
  • líneas 5-6: se sustituye cada valor del diccionario recibido como parámetro por el valor devuelto por el método estático [BaseEntity.check_value]. Por lo tanto, se trata de una llamada recursiva. El método estático [BaseEntity.check_value] se invoca hasta que su parámetro [value] sea un tipo simple (no un tipo BaseEntity, ni una lista ni un diccionario);

13.2.2.2. Exemples

El script [asdict_01] muestra diversos usos del método [asdict]:


# se configura la aplicación
import config
config = config.configure()

# el syspath está configurado; ya se pueden realizar las importaciones
from Enseignant import Enseignant
from BaseEntity import BaseEntity

# un profesor
enseignant1 = Enseignant().fromdict({"id"1"nom""lourou""prénom""paul""âge"56})
dict1 = enseignant1.asdict()
print(type(dict1))
print(enseignant1.__dict__)
print(dict1)
print(enseignant1.asdict(excluded_keys=["_Personne__âge"]))
Enseignant.excluded_keys = ["_Personne__prénom"]
print(enseignant1)
# otro profesor
enseignant2 = Enseignant().fromdict({"id"2"nom""abélard""prénom""béatrice""âge"57})
print(enseignant2.asdict())
print(enseignant2.asdict(included_keys=["_Personne__nom"]))
# una lista de entidades en una entidad
Enseignant.excluded_keys = []
entity1 = BaseEntity()
enseignants = [enseignant1, enseignant2]
setattr(entity1, "enseignants", enseignants)
print(entity1.asdict())
# un diccionario de entidades en una entidad
matières = {"maths": enseignant1, "français": enseignant2}
setattr(entity1, "matières", matières)
print(entity1.asdict())

Los resultados de la ejecución son los siguientes:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/asdict_01.py
<class 'dict'>
{'_BaseEntity__id': 1, '_Personne__nom': 'lourou', '_Personne__prénom': 'paul', '_Personne__âge': 56}
{'id': 1, 'nom': 'lourou', 'prénom': 'paul', 'âge': 56}
{'id': 1, 'nom': 'lourou', 'prénom': 'paul'}
{"id": 1, "nom": "lourou", "âge": 56}
{'id': 2, 'nom': 'abélard', 'âge': 57}
{'nom': 'abélard'}
{'enseignants': [{'id': 1, 'nom': 'lourou', 'prénom': 'paul', 'âge': 56}, {'id': 2, 'nom': 'abélard', 'prénom': 'béatrice', 'âge': 57}]}
{'enseignants': [{'id': 1, 'nom': 'lourou', 'prénom': 'paul', 'âge': 56}, {'id': 2, 'nom': 'abélard', 'prénom': 'béatrice', 'âge': 57}], 'matières': {'maths': {'id': 1, 'nom': 'lourou', 'prénom': 'paul', 'âge': 56}, 'français': {'id': 2, 'nom': 'abélard', 'prénom': 'béatrice', 'âge': 57}}}

Process finished with exit code 0
  • la línea 4 muestra la ventaja del método [asdict] frente al uso de la propiedad [__dict__]. Las propiedades se eliminan del prefijo de su clase. Esto se adapta mejor a la visualización;
  • hay varias formas de utilizar el método [asdict]:
    • si queremos todas las propiedades: utilizamos el método [asdict] sin parámetros;
    • si solo queremos algunas propiedades:
      • hay más propiedades que incluir que excluir: se utilizará únicamente el parámetro [excluded_keys];
      • hay menos propiedades que incluir que excluir: se utilizará únicamente el parámetro [included_keys];

13.2.3. El método [BaseEntity.asjson]

Este método permite obtener la cadena jSON de un objeto [BaseEntity] o derivado. Muestra la cadena jSON del diccionario devuelto por el método [asdict]. Su código es el siguiente:


def asjson(self, included_keys: list = None, excluded_keys: list = []) -> str:
        # la cadena json
        return json.dumps(self.asdict(included_keys=included_keys, excluded_keys=excluded_keys), ensure_ascii=False)
  • línea 1: los parámetros del método [asjson] son los del método [asdict];

A continuación se muestra un ejemplo (asjson_01) que utiliza este método:


# se configura la aplicación
import config
config = config.configure()

# se configura el syspath; ya se pueden realizar las importaciones
from Enseignant import Enseignant
from BaseEntity import BaseEntity

# un profesor
enseignant1 = Enseignant().fromdict({"id"1"nom""lourou""prénom""paul""âge"56})
print(type(enseignant1.asjson()))
print(enseignant1.asjson(excluded_keys=["_Personne__âge"]))
Enseignant.excluded_keys = ["_Personne__prénom"]
print(enseignant1.asjson())
# otro profesor
enseignant2 = Enseignant().fromdict({"id"2"nom""abélard""prénom""béatrice""âge"57})
print(enseignant2.asjson())
print(enseignant2.asjson(included_keys=["_Personne__nom"]))
# una lista de entidades en una entidad
Enseignant.excluded_keys = []
entity1 = BaseEntity()
enseignants = [enseignant1, enseignant2]
setattr(entity1, "enseignants", enseignants)
print(entity1.asjson())
# un diccionario de entidades en una entidad
matières = {"maths": enseignant1, "français": enseignant2}
setattr(entity1, "matières", matières)
print(entity1.asjson())

Los resultados son los siguientes:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/asjson_01.py
<class 'str'>
{"id": 1, "nom": "lourou", "prénom": "paul"}
{"id": 1, "nom": "lourou", "âge": 56}
{"id": 2, "nom": "abélard", "âge": 57}
{"nom": "abélard"}
{"enseignants": [{"id": 1, "nom": "lourou", "prénom": "paul", "âge": 56}, {"id": 2, "nom": "abélard", "prénom": "béatrice", "âge": 57}]}
{"enseignants": [{"id": 1, "nom": "lourou", "prénom": "paul", "âge": 56}, {"id": 2, "nom": "abélard", "prénom": "béatrice", "âge": 57}], "matières": {"maths": {"id": 1, "nom": "lourou", "prénom": "paul", "âge": 56}, "français": {"id": 2, "nom": "abélard", "prénom": "béatrice", "âge": 57}}}

Process finished with exit code 0

El método [BaseEntity.__str__] utiliza el método [asjson] para mostrar la identidad del objeto [BaseEntity] o derivado:


# toString
    def __str__(self) -> str:
        return self.asjson()

13.2.4. El método [BaseEntity.fromjson]

El método [BaseEntity.fromjson] permite inicializar un objeto de tipo [BaseEntity] o derivado a partir de un diccionario jSON. Su código es el siguiente:


def fromjson(self, json_state: str, silent: bool = False):
        # se actualiza el estado del objeto a partir de la cadena jSON
        return self.fromdict(json.loads(json_state), silent=silent)
  • línea 1: el método admite dos parámetros:
    • [json_state]: el diccionario jSON que se utilizará para inicializar el objeto [BaseEntity];
    • [silent]: para indicar si la presencia en el diccionario jSON de una clave que no puede aceptarse como propiedad del objeto [BaseEntity] provoca una excepción (silent=False) o simplemente se ignora (silent=True);
  • línea 3: se empieza por construir el diccionario Python, imagen del diccionario jSON, y luego se utiliza el método [fromdict] para inicializar el objeto [BaseEntity] a partir de este diccionario Python;

He aquí un ejemplo (fromjson_01):


# se configura la aplicación
import config

config = config.configure()

# se configura la ruta del sistema (syspath); ya se pueden realizar las importaciones
from Enseignant import Enseignant
import json

# un profesor
json1 = json.dumps({"id": 1, "nom": "lourou", "prénom": "paul", "âge": 56})
enseignant1 = Enseignant().fromjson(json1)
enseignant1.show()
  • línea 11: se crea la cadena jSON a partir de un diccionario;
  • línea 12: se inicializa un objeto [Enseignant] con esta cadena;
  • línea 13: se muestra el profesor;

Los resultados son los siguientes:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/fromjson_01.py
Enseignant[1, paul, lourou, 56]

Process finished with exit code 0

13.2.5. El script [main]

El script [main] resume los diferentes métodos encontrados:


# se configura la aplicación
import config

config = config.configure()

# la ruta del sistema está configurada; se pueden realizar las importaciones
from BaseEntity import BaseEntity
from MyException import MyException


# una clase
class ChildEntity(BaseEntity):
    # atributos excluidos del estado de la clase
    excluded_keys = []

    @staticmethod
    def get_allowed_keys():
        return ["att1", "att2", "att3", "att4"]

    @property
    def att1(self) -> int:
        return self.__att1

    @att1.setter
    def att1(self, value: int):
        if 10 >= value >= 1:
            self.__att1 = value
        else:
            raise MyException(1, f"L'attribut [att1] attend une valeur dans l'intervalle [1,10] ({value})")


# configuración ChildEntity
ChildEntity.excluded_keys = []
# instancia ChildEntity
child = ChildEntity().fromdict({"att1": 1, "att2": 2})
# atención a los nombres de las propiedades
# Estos son los nombres que se utilizan en [excluded_keys] y [included_keys]
print(child.__dict__)
# propiedades sin prefijo de su clase
print(child)

# instancia ChildEntity
try:
    child = ChildEntity().fromdict({"att1": 1, "att5": 5})
    print(child)
except MyException as erreur:
    print(erreur)

# instancia ChildEntity
child = ChildEntity().fromdict({"att1": 1, "att2": 2, "att3": 3, "att4": 4})
print(child)

# exclusiones de determinadas claves del estado de las instancias
ChildEntity.excluded_keys = ['att3']
print(child)

# se excluye explícitamente una clave de la visualización
# se añade a las excluidas globalmente a nivel de clase
print(child.asdict(excluded_keys=["_ChildEntity__att1"]))
print(child.asjson(excluded_keys=["att2"]))

# interés de la clase con respecto al diccionario
# puede verificar la validez de su contenido
try:
    child = ChildEntity().fromdict({"att1": 20})
except MyException as erreur:
    print(erreur)

# instancia ChildEntity
child1 = ChildEntity().fromdict({"att1": 1, "att2": 2, "att3": 3, "att4": 4})
# instancia ChildEntity que contiene otra instancia ChildEntity
child2 = ChildEntity().fromdict({"att1": 10, "att2": 20, "att3": 30, "att4": child1})
print(child2)

# included_keys tiene prioridad sobre excluded_keys, que entonces se ignoran
ChildEntity.excluded_keys = ['_ChildEntity__att1', 'att2']
print(child.asdict(included_keys=["_ChildEntity__att1", "att3"], excluded_keys=["att3", "att4"]))

Los resultados de la ejecución son los siguientes:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/main.py
{'_ChildEntity__att1': 1, 'att2': 2}
{"att1": 1, "att2": 2}
MyException[2, la clé [att5] n'est pas autorisée]
{"att1": 1, "att2": 2, "att3": 3, "att4": 4}
{"att1": 1, "att2": 2, "att4": 4}
{'att2': 2, 'att4': 4}
{"att1": 1, "att4": 4}
MyException[1, L'attribut [att1] attend une valeur dans l'intervalle [1,10] (20)]
{"att1": 10, "att2": 20, "att4": {"att1": 1, "att2": 2, "att4": 4}}
{'att1': 1, 'att3': 3}

Process finished with exit code 0

Prestaremos atención a la línea 2 de los resultados: es la propiedad [ChildEntity.__dict__] (línea 38 del código) la que nos permite conocer los nombres de las propiedades que hay que incluir en las listas [included_keys] y [excluded_keys]. Cabe señalar, siempre en la línea 2 de los resultados, que dependiendo de si la propiedad se define dentro de la clase mediante un getter/setter o si se ha creado como se crearía la clave de un diccionario, va o no precedida del nombre de la clase [ChildEntity].