13. Le classi generiche [BaseEntity] e [MyException]
Ora definiremo due classi che useremo regolarmente d'ora in poi.

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:
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:
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

13.2.1.2.1. La classe [Utils]
La classe [Utils] (Utils.py) è la seguente:
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]:
- 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]:
- 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]:
- 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:
- 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:
- 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:
- 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:
- 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:
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:
- 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:
- 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:
- 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]:
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:
- riga 1: i parametri del metodo [asjson] sono quelli del metodo [asdict];
Ecco un esempio (asjson_01) che utilizza questo metodo:
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:
- 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):
- 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:
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].