Skip to content

13. Le classi generiche [BaseEntity] e [MyException]

Ora definiremo due classi che useremo regolarmente d'ora in poi.

Image

13.1. La classe MyException

La classe [MyException] (MyException.py) fornisce una classe di eccezione personalizzata:


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

Note

  • riga 2: la classe [MyException] deriva dalla classe predefinita [BaseException];
  • riga 4: il costruttore accetta due parametri:
    • [code]: un codice di errore intero;
    • [message]: un messaggio di errore;
  • riga 6: il messaggio di errore viene passato alla classe padre;
  • righe 14–27: si accede all'attributo [code] tramite un getter/setter;
  • righe 23–24: viene verificata la validità dell'attributo [code]: deve essere un numero intero maggiore di 0;

13.2. La classe [BaseEntity]

La classe [BaseEntity] sarà la classe padre della maggior parte delle classi che creeremo per incapsulare le informazioni relative a un oggetto. Andando avanti, utilizzeremo principalmente due tipi di classi:

  • classi il cui unico scopo è incapsulare le informazioni su un singolo oggetto in un unico posto. Queste non avranno comportamenti (metodi) diversi dai getter/setter e da una funzione di visualizzazione (__str__). Se ci sono N oggetti da gestire, queste classi vengono istanziate N volte. [BaseEntity] sarà la classe padre di questo tipo di classe;
  • classi il cui ruolo principale è incapsulare metodi e pochissime informazioni. Queste classi saranno istanziate una sola volta (singleton). Il loro ruolo è implementare gli algoritmi di un'applicazione;

La classe [BaseEntity] è la seguente:

#  imports
import json
import re

from MyException import MyException


class BaseEntity(object):
    #  properties excluded from class state
    excluded_keys = []

    #  class properties
    @staticmethod
    def get_allowed_keys() -> list:
        #  id: object identifier
        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 must be an integer >=0
        try:
            id = int(id)
            erreur = id < 0
        except:
            erreur = True
        #  mistake?
        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):
        

Commenti

  • Lo scopo della classe [BaseEntity] è quello di facilitare le conversioni da oggetto a dizionario e da oggetto a JSON. Essa fornisce i seguenti metodi:
    • [asdict]: restituisce un dizionario delle proprietà dell'oggetto;
    • [fromdict]: crea un oggetto da un dizionario;
    • [asjson]: restituisce la stringa JSON dell'oggetto, in modo simile alla funzione [__str__];
    • [fromjson]: costruisce un oggetto dalla sua stringa JSON;
  • La classe [BaseEntity] è pensata per essere derivata e non utilizzata così com'è;
  • righe 22–25: la classe [BaseEntity] ha una sola proprietà, l'intero [id]. Questa proprietà è l'identificatore dell'oggetto. In pratica, è spesso utile poter distinguere tra istanze della stessa classe. Lo faremo utilizzando questa proprietà, che è unica per ogni istanza. Inoltre, gli oggetti provengono spesso da database in cui sono identificati da una chiave primaria, tipicamente un numero intero. In tali casi, [id] fungerà da chiave primaria;
  • righe 27–40: il setter per la proprietà [id]. Verifichiamo che sia un numero intero >= 0. Se così non fosse, viene generata un'eccezione di tipo [MyException] (riga 39);
  • riga 10: [excluded_keys] è un attributo di classe, non un attributo di istanza. Pertanto, scriviamo [BaseEntity.excluded_keys]. Questo attributo di classe è un elenco contenente le proprietà della classe che non partecipano alle conversioni da oggetto a Dizionario e da oggetto a JSON;
  • righe 12–16: [get_allowed_keys] restituisce l'elenco delle proprietà della classe. In una conversione Dizionario → Oggetto o JSON → Oggetto, saranno accettate solo le chiavi presenti in questo elenco. Ogni classe derivata dalla classe [BaseEntity] dovrà ridefinire questo elenco;

È importante comprendere che le proprietà e le funzioni della classe [BaseEntity] sono accessibili alle classi derivate da [BaseEntity]. Questo è il punto chiave da cogliere.

Esamineremo ora in dettaglio il codice della classe [BaseEntity]. Si tratta di un argomento piuttosto avanzato. I lettori principianti possono limitarsi a leggere la descrizione del ruolo di ciascuna funzione senza approfondire il codice stesso.

13.2.1. Il metodo [BaseEntity.fromdict]

13.2.1.1. Definizione

Il metodo [fromdict] consente di inizializzare un oggetto [BaseEntity] o un oggetto derivato da un dizionario:

def fromdict(self, state: dict, silent=False):
        #  object is updated
        #  authorized keys
        allowed_keys = self.__class__.get_allowed_keys()
        #  state key traversal
        for key, value in state.items():
            #  is the key authorized?
            if key not in allowed_keys:
                if not silent:
                    raise MyException(2, f"la clé {key} n'est pas autorisée")
            else:
                #  we try to assign the value to the key
                #  we let any exception go up
                setattr(self, key, value)
        #  we return the object
        return self

Commenti

  • riga 1: la funzione riceve il dizionario [state] come parametro, dal quale verrà inizializzato l'oggetto corrente;
  • riga 4: chiamiamo la funzione statica [get_allowed_keys] della classe che ha chiamato la funzione [fromdict]. Se abbiamo a che fare con una classe derivata da [BaseEntity] e tale classe derivata ha ridefinito la funzione statica [get_allowed_keys], allora è la funzione [get_allowed_keys] che viene chiamata. Ogni classe derivata ridefinisce questa funzione statica per dichiarare le proprie proprietà;
  • riga 6: si esegue un'iterazione sulle chiavi e sui valori del dizionario [state];
  • riga 8: se la chiave [key] non è una delle proprietà della classe, allora:
    • viene ignorata;
    • viene generata un'eccezione (riga 10). Lo sviluppatore specifica la propria preferenza passando il parametro [silent] appropriato (riga 1). Il valore predefinito di [silent] fa sì che venga generata un'eccezione se si tenta di inizializzare l'oggetto con una proprietà che non possiede;
  • riga 14: se la chiave è tra le proprietà dell'oggetto, allora viene assegnata all'oggetto [self] utilizzando la funzione predefinita [setattr];
  • riga 16: la funzione restituisce l'oggetto inizializzato;

13.2.1.2. Esempi

Image

13.2.1.2.1. La classe [Utils]

La classe [Utils] (Utils.py) è la seguente:

class Utils:
    #  static method
    @staticmethod
    def is_string_ok(string: str) -> bool:
        #  string is a string
        erreur = not isinstance(string, str)
        if not erreur:
            #  is the chain empty?
            erreur = string.strip() == ''
        #  result
        return not erreur

Definisce, alle righe 3–11, un metodo statico che restituisce un valore booleano true se il suo parametro [str] è una stringa non vuota;

13.2.1.2.2. La classe [Person]

La classe [Person] (Person.py) deriva dalla classe [BaseEntity]:

#  imports
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils


#  person class
class Personne(BaseEntity):
    #  properties excluded from class state
    excluded_keys = []

    #  class properties
    #  id: person's identifier
    #  first name: person's first name
    #  name: person's name
    #  age: age of the person
    @staticmethod
    def get_allowed_keys() -> list:
        #  id: object identifier
        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):
        #  first name must be non-empty
        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):
        #  first name must be non-empty
        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):
        #  age must be an integer >=0
        erreur = False
        if isinstance(âge, int):
            if âge >= 0:
                self.__âge = âge
            else:
                erreur = True
        else:
            erreur = True
        #  mistake?
        if erreur:
            raise MyException(13, "L'âge doit être un entier >=0")
  • riga 8: la classe [Person] deriva dalla classe [BaseEntity];
  • righe 8–65: abbiamo mantenuto la maggior parte della classe [Person] incontrata in precedenza. Le differenze sono le seguenti:
    • la classe non ha più un costruttore;
    • la classe utilizza l'eccezione [MyException], ad esempio alla riga 65;
    • ha un metodo statico, [get_allowed_keys], righe 17–20, che definisce l'elenco delle sue proprietà. Le proprietà specifiche della classe [Person] vengono aggiunte a quelle della classe padre [BaseEntity];
    • ha un elenco statico [excluded_keys], su cui torneremo più avanti;
13.2.1.2.3. La classe [Teacher]

La classe [Teacher] (Teacher.py) deriva dalla classe [Person]:

#  imports
from MyException import MyException
from Personne import Personne
from Utils import Utils


#  class Teacher
class Enseignant(Personne):
    #  properties excluded from class state
    excluded_keys = []

    #  class properties
    #  id: person's identifier
    #  first name: person's first name
    #  name: person's name
    #  age: age of the person
    #  discipline: discipline taught
    @staticmethod
    def get_allowed_keys() -> list:
        #  id: object identifier
        return Personne.get_allowed_keys() + ["discipline"]

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

    @discipline.setter
    def discipline(self, discipline: str):
        #  the discipline must be a non-empty string
        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")

    #  show method
    def show(self):
        print(f"Enseignant[{self.id}, {self.prénom}, {self.nom}, {self.âge}]")
  • riga 8: la classe [Insegnante] estende (o deriva da) la classe [Persona];
  • righe 18–21: definiscono l'elenco delle proprietà della classe;
  • righe 37–38: il metodo [show] visualizza l'identità dell'insegnante;
13.2.1.2.4. La configurazione [config]

Gli script di esempio utilizzano la seguente configurazione [config]:

def configure():
    import os

    #  configuration file folder
    script_dir = os.path.dirname(os.path.abspath(__file__))

    #  absolute paths of folders to put in the syspath
    absolute_dependencies = [
        #  the BaseEntity class
        f"{script_dir}/entities",
    ]

    #  update syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  we return the configuration
    return {}
  • righe 8–10: le directory contenenti le dipendenze del progetto;
  • righe 14–15: viene costruito il Python Path;
  • riga 18: restituisce un dizionario vuoto (non ci sono altre configurazioni oltre al syspath);
13.2.1.2.5. Lo script [fromdict_01]

Lo script [fromdict_01] è il seguente:

#  configure the application
import config

config = config.configure()

#  syspath is configured - imports can be made
from Enseignant import Enseignant

#  a teacher
enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "paul", "âge": 56})
enseignant1.show()
  • Riga 10: creiamo un oggetto [Insegnante] da un dizionario. Per farlo, utilizziamo il costruttore predefinito della classe per creare un oggetto [Insegnante] al quale applichiamo il metodo [fromdict]. È importante comprendere che, in questo caso, il metodo [fromdict] eseguito è quello della classe padre [BaseEntity]. Infatti:
    • il metodo [fromdict] viene prima cercato nella classe [Teacher]. Non esiste;
    • viene quindi cercato nella classe padre [Person]. Non esiste;
    • viene quindi cercato nella classe padre [BaseEntity]. Esiste;
  • riga 11: viene visualizzato l'oggetto [Teacher];

I risultati sono i seguenti:


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. Lo script [fromdict_02]

Lo script [fromdict_02] è il seguente:

#  configure the application
import config

config = config.configure()

#  syspath is configured - imports can be made
from Enseignant import Enseignant

#  a teacher
enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "", "âge": 56})
enseignant1.show()
  • Riga 10: creiamo un insegnante con il nome vuoto. Ciò dovrebbe generare un'eccezione poiché la classe [Person] non accetta nomi vuoti. Questo esempio illustra la differenza tra un dizionario e un oggetto. Quest'ultimo può convalidare le proprie proprietà, mentre il dizionario non può;

I risultati sono i seguenti:


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. Lo script [fromdict_03]

Lo script [fromdict_03] è il seguente:

#  configure the application
import config

config = config.configure()

#  syspath is configured - imports can be made
from Enseignant import Enseignant

#  a teacher
enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "albert", "âge": 56, "sexe": "M"})
enseignant1.show()
  • Riga 10: creiamo un insegnante da un dizionario contenente una chiave (sesso) che non appartiene alla classe [Insegnante]. Dovrebbe quindi essere generata un'eccezione;

I risultati sono i seguenti:


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. Lo script [fromdict_04]

Lo script [fromdict_04] è una copia di [fromdict_03] con una piccola differenza:

#  configure the application
import config

config = config.configure()

#  syspath is configured - imports can be made
from Enseignant import Enseignant

#  a teacher
enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "albert", "âge": 56, "sexe": "M"}, silent=True)
enseignant1.show()
  • riga 10: abbiamo utilizzato il parametro [silent=True] per indicare che, se una chiave del dizionario non è una proprietà della classe [Teacher], deve essere semplicemente ignorata. In questo caso, non verrà generata alcuna eccezione;

I risultati sono i seguenti:


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. Il metodo [BaseEntity.asdict]

13.2.2.1. Definizione

Il metodo [BaseEntity.asdict] restituisce un dizionario le cui chiavi sono le proprietà dell'oggetto:


    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

Commenti

  • riga 1: la funzione [asdict] restituisce il dizionario delle proprietà dell'oggetto;
  • riga 1: [included_keys]: l'elenco delle chiavi da includere nel dizionario;
  • riga 1: [excluded_keys]: l'elenco delle chiavi da escludere dal dizionario;
  • riga 3: la proprietà [self.__dict__] restituisce il dizionario delle proprietà dell'oggetto. I nomi delle proprietà sono le chiavi e i loro valori sono i valori del dizionario. Un oggetto può contenere riferimenti ad altri oggetti. In tal caso, i nomi delle proprietà sono preceduti dal nome della classe a cui appartengono. Questo è qualcosa che non vogliamo. Vogliamo le proprietà senza il loro prefisso;
  • riga 3: è importante comprendere qui che se la funzione [asdict] viene eseguita all'interno di una classe derivata da [BaseEntity], la proprietà [self.__dict__] restituisce il dizionario delle proprietà per l'oggetto derivato;
  • riga 5: il dizionario che stiamo per costruire;
  • riga 7: iteriamo sui valori di [self.__dict__] nella forma (chiave, valore);
  • riga 9: se la chiave corrente appartiene all'elenco delle chiavi da includere, allora viene aggiunta al dizionario [new_attributes] utilizzando la funzione [set_value], che descriveremo tra poco;
  • riga 12: se il parametro [included_keys] non è presente, viene utilizzato il parametro [excluded_keys]. Se la proprietà non è tra quelle da escludere, viene aggiunta al dizionario [new_attributes];
  • riga 12: ci sono diversi modi per escludere una proprietà dal dizionario:
    • è stata definita a livello di attributo di classe [excluded_keys];
    • è stata definita nell'elenco [excluded_keys] passato alla funzione [asdict];
    • il parametro [included_keys] è presente e non include la proprietà;
  • riga 15: restituiamo il dizionario [new_attributes]

La funzione [set_value] alle righe 10 e 13 è la seguente:

    @staticmethod
    def set_value(key: str, value, new_attributes: dict):
        #  keys can be of the form __Class__key
        match = re.match("^.*?__(.*?)$", key)
        if match:
            #  note the new key
            newkey = match.groups()[0]
        else:
            #  the key remains unchanged
            newkey = key
        #  insert the new key into the [new_attributes] dictionary
        #  type, transforming if necessary the associated value into one of the
        #  dict, list, simple type
        new_attributes[newkey] = BaseEntity.check_value(value)

Commenti

  • riga 4: controlla se la chiave è nella forma __Class_key. Questa è la forma che assume se appartiene a un oggetto incluso nell'oggetto principale. In questo caso, vogliamo mantenere solo la stringa [key];
  • riga 7: conserviamo solo la stringa che segue gli ultimi due caratteri sottolineati della stringa;
  • righe 8–10: se la chiave non è nella forma __Class_key, la manteniamo così com'è;
  • righe 11–14: il valore associato alla chiave [newkey] viene calcolato dal metodo statico [BaseEntity.check_value];

Il metodo statico [BaseEntity.check_value] è il seguente:

    @staticmethod
    def check_value(value):
        #  the value can be of type BaseEntity, list, dict or a simple type
        #  is value an instance of BaseEntity?
        if isinstance(value, BaseEntity):
            value2 = value.asdict()
        #  is value of type list
        elif isinstance(value, list):
            value2 = BaseEntity.list2list(value)
        #  is value a dict type?
        elif isinstance(value, dict):
            value2 = BaseEntity.dict2dict(value)
        #  value is a simple type
        else:
            value2 = value
        #  we return the result
        return value2
  • riga 1: il metodo [check_value] è statico (un metodo di classe, non un metodo di istanza). Accetta come parametro il valore da associare a una chiave del dizionario:
    • riga 17: se questo valore è di tipo semplice, rimane invariato;
    • righe 5-6: se questo valore è di tipo BaseEntity, il valore viene sostituito dal suo dizionario. Ciò comporta una chiamata ricorsiva;
    • righe 8–9: se questo valore è una lista, viene sostituito dal valore [BaseEntity.list2list];
    • righe 11–12: se questo valore è un dizionario, viene sostituito dal valore [BaseEntity.dict2dict];

Il metodo statico [BaseEntity.list2list] è il seguente:

1
2
3
4
5
6
7
8
    @staticmethod
    def list2list(liste: list) -> list:
        #  inspect list items
        newlist = []
        for value in liste:
            newlist.append(BaseEntity.check_value(value))
        #  return the new list
        return newlist
  • riga 2: il metodo riceve un elenco e restituisce un elenco;
  • righe 5-6: ogni valore nella lista passata come parametro viene sostituito con il valore restituito dal metodo statico [BaseEntity.check_value]. Si tratta quindi di una chiamata ricorsiva. Il metodo statico [BaseEntity.check_value] viene chiamato finché il suo parametro [value] non è un tipo semplice (non un tipo BaseEntity, lista o dizionario);

Il metodo statico [BaseEntity.dict2dict] è il seguente:

1
2
3
4
5
6
7
8
    @staticmethod
    def dict2dict(dictionary: dict) -> dict:
        #  inspect dictionary items
        newdict = {}
        for key, value in dictionary.items():
            newdict[key] = BaseEntity.check_value(value)
        #  the new dictionary is returned
        return newdict
  • riga 2: il metodo riceve un dizionario e restituisce un dizionario;
  • righe 5-6: ogni valore nel dizionario passato come parametro viene sostituito con il valore restituito dal metodo statico [BaseEntity.check_value]. Si tratta quindi di una chiamata ricorsiva. Il metodo statico [BaseEntity.check_value] viene chiamato finché il suo parametro [value] non è un tipo semplice (non una BaseEntity, una lista o un dizionario);

13.2.2.2. Esempi

Lo script [asdict_01] illustra vari utilizzi del metodo [asdict]:

#  configure the application
import config
config = config.configure()

#  syspath is configured - imports can be made
from Enseignant import Enseignant
from BaseEntity import BaseEntity

#  a teacher
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)
#  another teacher
enseignant2 = Enseignant().fromdict({"id": 2, "nom": "abélard", "prénom": "béatrice", "âge": 57})
print(enseignant2.asdict())
print(enseignant2.asdict(included_keys=["_Personne__nom"]))
#  a list of entities within an entity
Enseignant.excluded_keys = []
entity1 = BaseEntity()
enseignants = [enseignant1, enseignant2]
setattr(entity1, "enseignants", enseignants)
print(entity1.asdict())
#  a dictionary of entities within an entity
matières = {"maths": enseignant1, "français": enseignant2}
setattr(entity1, "matières", matières)
print(entity1.asdict())

I risultati dell'esecuzione sono i seguenti:


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 riga 4 mostra il vantaggio del metodo [asdict] rispetto all'uso della proprietà [__dict__]. Le proprietà vengono private del prefisso della classe. Questo le rende più facili da visualizzare;
  • Esistono diversi modi per utilizzare il metodo [asdict]:
    • se si desiderano tutte le proprietà: utilizzare il metodo [asdict] senza parametri;
    • se si desiderano solo determinate proprietà:
      • ci sono più proprietà da includere che da escludere: usa il singolo parametro [excluded_keys];
      • Se le proprietà da includere sono meno di quelle da escludere: useremo solo il parametro [included_keys];

13.2.3. Il metodo [BaseEntity.asjson]

Questo metodo restituisce la stringa JSON di un oggetto [BaseEntity] o delle sue classi derivate. Restituisce la stringa JSON del dizionario restituito dal metodo [asdict]. Il codice è il seguente:

1
2
3
def asjson(self, included_keys: list = None, excluded_keys: list = []) -> str:
        #  the json string
        return json.dumps(self.asdict(included_keys=included_keys, excluded_keys=excluded_keys), ensure_ascii=False)
  • riga 1: i parametri del metodo [asjson] sono quelli del metodo [asdict];

Ecco un esempio (asjson_01) che utilizza questo metodo:

#  configure the application
import config
config = config.configure()

#  syspath is configured - imports can be made
from Enseignant import Enseignant
from BaseEntity import BaseEntity

#  a teacher
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())
#  another teacher
enseignant2 = Enseignant().fromdict({"id": 2, "nom": "abélard", "prénom": "béatrice", "âge": 57})
print(enseignant2.asjson())
print(enseignant2.asjson(included_keys=["_Personne__nom"]))
#  a list of entities within an entity
Enseignant.excluded_keys = []
entity1 = BaseEntity()
enseignants = [enseignant1, enseignant2]
setattr(entity1, "enseignants", enseignants)
print(entity1.asjson())
#  a dictionary of entities within an entity
matières = {"maths": enseignant1, "français": enseignant2}
setattr(entity1, "matières", matières)
print(entity1.asjson())

I risultati sono i seguenti:


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

Il metodo [BaseEntity.__str__] utilizza il metodo [asjson] per visualizzare l'identità dell'oggetto [BaseEntity] o dei suoi oggetti derivati:


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

13.2.4. Il metodo [BaseEntity.fromjson]

Il metodo [BaseEntity.fromjson] consente di inizializzare un oggetto di tipo [BaseEntity] o un tipo derivato da un dizionario JSON. Il codice è il seguente:

1
2
3
def fromjson(self, json_state: str, silent: bool = False):
        #  update object status from jSON string
        return self.fromdict(json.loads(json_state), silent=silent)
  • Riga 1: Il metodo accetta due parametri:
    • [json_state]: il dizionario JSON utilizzato per inizializzare l'oggetto [BaseEntity];
    • [silent]: per indicare se la presenza nel dizionario JSON di una chiave che non può essere accettata come proprietà dell'oggetto [BaseEntity] genera un'eccezione (silent=False) o viene semplicemente ignorata (silent=True);
  • Riga 3: Iniziamo costruendo il dizionario Python che rappresenta il dizionario JSON, quindi utilizziamo il metodo [fromdict] per inizializzare l'oggetto [BaseEntity] a partire da questo dizionario Python;

Ecco un esempio (fromjson_01):

#  configure the application
import config

config = config.configure()

#  syspath is configured - imports can be made
from Enseignant import Enseignant
import json

#  a teacher
json1 = json.dumps({"id": 1, "nom": "lourou", "prénom": "paul", "âge": 56})
enseignant1 = Enseignant().fromjson(json1)
enseignant1.show()
  • riga 11: creiamo la stringa JSON di un dizionario;
  • riga 12: un oggetto [Enseignant] viene inizializzato con questa stringa;
  • riga 13: viene visualizzato l'insegnante;

I risultati sono i seguenti:


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. Lo script [main]

Lo script [main] riassume i vari metodi incontrati:

#  configure the application
import config

config = config.configure()

#  syspath is configured - imports can be made
from BaseEntity import BaseEntity
from MyException import MyException


#  a class
class ChildEntity(BaseEntity):
    #  attributes excluded from class state
    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})")


#  configuration ChildEntity
ChildEntity.excluded_keys = []
#  instance ChildEntity
child = ChildEntity().fromdict({"att1": 1, "att2": 2})
#  pay attention to property names
#  these are the names used in [excluded_keys] and [included_keys]
print(child.__dict__)
#  properties not prefixed by their class
print(child)

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

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

#  exclusion of certain keys from instance status
ChildEntity.excluded_keys = ['att3']
print(child)

#  a key is explicitly excluded from the display
#  it is added to those excluded globally at class level
print(child.asdict(excluded_keys=["_ChildEntity__att1"]))
print(child.asjson(excluded_keys=["att2"]))

#  class interest in the dictionary
#  it can check the validity of its contents
try:
    child = ChildEntity().fromdict({"att1": 20})
except MyException as erreur:
    print(erreur)

#  instance ChildEntity
child1 = ChildEntity().fromdict({"att1": 1, "att2": 2, "att3": 3, "att4": 4})
#  instance ChildEntity containing another instance ChildEntity
child2 = ChildEntity().fromdict({"att1": 10, "att2": 20, "att3": 30, "att4": child1})
print(child2)

#  included_keys has priority over excluded_keys which are then ignored
ChildEntity.excluded_keys = ['_ChildEntity__att1', 'att2']
print(child.asdict(included_keys=["_ChildEntity__att1", "att3"], excluded_keys=["att3", "att4"]))

I risultati dell'esecuzione sono i seguenti:


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

Si noti la riga 2 dei risultati: è la proprietà [ChildEntity.__dict__] (riga 38 del codice) che ci permette di determinare i nomi delle proprietà da includere nelle liste [included_keys] e [excluded_keys]. Si noti, sempre alla riga 2 dei risultati, che a seconda che la proprietà sia definita all'interno della classe tramite un getter/setter o creata come si creerebbe una chiave di dizionario, può essere o meno preceduta dal nome della classe [ChildEntity].