13. As classes genéricas [BaseEntity] e [MyException]
Vamos agora definir duas classes que iremos utilizar regularmente daqui em diante.

13.1. A classe MyException
A classe [MyException] (MyException.py) fornece uma classe de exceção personalizada:
# 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
- linha 2: a classe [MyException] deriva da classe predefinida [BaseException];
- linha 4: o construtor aceita dois parâmetros:
- [code]: um código de erro inteiro;
- [message]: uma mensagem de erro;
- linha 6: a mensagem de erro é passada para a classe pai;
- linhas 14–27: o atributo [code] é acedido através de um getter/setter;
- linhas 23–24: a validade do atributo [code] é verificada: deve ser um inteiro maior que 0;
13.2. A classe [BaseEntity]
A classe [BaseEntity] será a classe pai da maioria das classes que criamos para encapsular informações sobre um objeto. Daqui em diante, utilizaremos principalmente dois tipos de classes:
- classes cujo único objetivo é encapsular informações sobre um único objeto num único local. Estas não terão comportamentos (métodos) além de getters/setters e uma função de exibição (__str__). Se houver N objetos para gerir, estas classes serão instanciadas N vezes. [BaseEntity] será a classe pai deste tipo de classe;
- classes cuja função principal é encapsular métodos e muito pouca informação. Estas classes serão instanciadas apenas uma vez (singleton). A sua função é implementar os algoritmos de uma aplicação;
A classe [BaseEntity] é a seguinte:
Comentários
- O objetivo da classe [BaseEntity] é facilitar as conversões entre Objeto/Dicionário e Objeto/JSON. Ela disponibiliza os seguintes métodos:
- [asdict]: devolve um dicionário com as propriedades do objeto;
- [fromdict]: cria um objeto a partir de um dicionário;
- [asjson]: devolve a cadeia JSON do objeto, semelhante à função [__str__];
- [fromjson]: constrói um objeto a partir da sua string JSON;
- A classe [BaseEntity] destina-se a ser derivada e não a ser utilizada tal como está;
- linhas 22–25: a classe [BaseEntity] tem apenas uma propriedade, o inteiro [id]. Esta propriedade é o identificador do objeto. Na prática, é frequentemente útil poder distinguir entre instâncias da mesma classe. Faremos isso utilizando esta propriedade, que é única para cada instância. Além disso, os objetos provêm frequentemente de bases de dados onde são identificados por uma chave primária, tipicamente um inteiro. Nesses casos, [id] servirá como chave primária;
- linhas 27–40: o setter para a propriedade [id]. Verificamos se é um inteiro >= 0. Se não for esse o caso, é lançada uma exceção do tipo [MyException] (linha 39);
- linha 10: [excluded_keys] é um atributo de classe, não um atributo de instância. Por isso, escrevemos [BaseEntity.excluded_keys]. Este atributo de classe é uma lista que contém as propriedades da classe que não participam nas conversões Object/Dictionary e Object/JSON;
- linhas 12–16: [get_allowed_keys] devolve a lista das propriedades da classe. Numa conversão de Dicionário → Objeto ou JSON → Objeto, apenas as chaves presentes nesta lista serão aceites. Cada classe derivada da classe [BaseEntity] terá de redefinir esta lista;
É importante compreender aqui que as propriedades e funções da classe [BaseEntity] são acessíveis às classes derivadas de [BaseEntity]. Este é o ponto-chave a compreender.
Vamos agora examinar o código da classe [BaseEntity] em detalhe. É bastante avançado. Os leitores principiantes podem simplesmente ler a descrição da função de cada função sem se aprofundarem no código em si.
13.2.1. O método [BaseEntity.fromdict]
13.2.1.1. Definição
O método [fromdict] permite-lhe inicializar um objeto [BaseEntity] ou um objeto derivado a partir de um dicionário:
Comentários
- linha 1: a função recebe o dicionário [state] como parâmetro, a partir do qual o objeto atual será inicializado;
- linha 4: chamamos a função estática [get_allowed_keys] da classe que chamou a função [fromdict]. Se estivermos a lidar com uma classe derivada de [BaseEntity] e essa classe derivada tiver redefinido a função estática [get_allowed_keys], então é a função [get_allowed_keys] que é chamada. Cada classe derivada redefine esta função estática para declarar as suas propriedades;
- linha 6: percorremos as chaves e os valores do dicionário [state];
- linha 8: se a chave [key] não for uma das propriedades da classe, então:
- ela é ignorada;
- é lançada uma exceção (linha 10). O programador especifica a sua preferência passando o parâmetro [silent] apropriado (linha 1). O valor padrão de [silent] faz com que seja lançada uma exceção se for feita uma tentativa de inicializar o objeto com uma propriedade que este não possui;
- linha 14: se a chave estiver entre as propriedades do objeto, então é atribuída ao objeto [self] utilizando a função predefinida [setattr];
- linha 16: a função devolve o objeto inicializado;
13.2.1.2. Exemplos

13.2.1.2.1. A classe [Utils]
A classe [Utils] (Utils.py) é a seguinte:
Define, nas linhas 3–11, um método estático que retorna um valor booleano verdadeiro se o seu parâmetro [str] for uma string não vazia;
13.2.1.2.2. A classe [Person]
A classe [Person] (Person.py) deriva da classe [BaseEntity]:
- linha 8: a classe [Person] deriva da classe [BaseEntity];
- linhas 8–65: mantivemos a maior parte da classe [Person] encontrada anteriormente. As diferenças são as seguintes:
- a classe já não tem um construtor;
- a classe utiliza a exceção [MyException], por exemplo, linha 65;
- possui um método estático, [get_allowed_keys], linhas 17–20, que define a lista das suas propriedades. As propriedades específicas da classe [Person] são adicionadas às da classe pai [BaseEntity];
- possui uma lista estática [excluded_keys], à qual voltaremos mais tarde;
13.2.1.2.3. A classe [Teacher]
A classe [Teacher] (Teacher.py) deriva da classe [Person]:
- linha 8: a classe [Professor] estende (ou deriva de) a classe [Pessoa];
- linhas 18–21: definem a lista de propriedades da classe;
- linhas 37–38: o método [show] exibe a identidade do professor;
13.2.1.2.4. A configuração [config]
Os scripts de exemplo utilizam a seguinte configuração [config]:
- linhas 8–10: os diretórios que contêm as dependências do projeto;
- linhas 14–15: o Python Path é construído;
- linha 18: retorna um dicionário vazio (não há outras configurações além do syspath);
13.2.1.2.5. O script [fromdict_01]
O script [fromdict_01] é o seguinte:
- Linha 10: Criamos um objeto [Teacher] a partir de um dicionário. Para isso, usamos o construtor padrão da classe para criar um objeto [Teacher] ao qual aplicamos o método [fromdict]. É importante compreender que, neste caso, o método [fromdict] que está a ser executado é o da classe pai [BaseEntity]. Na verdade:
- o método [fromdict] é primeiro procurado na classe [Teacher]. Como não existe;
- é então procurado na classe pai [Person]. Não existe;
- é então procurado na classe pai [BaseEntity]. Existe;
- linha 11: o objeto [Teacher] é exibido;
Os resultados são os seguintes:
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. O script [fromdict_02]
O script [fromdict_02] é o seguinte:
- Linha 10: Criamos um professor com um nome próprio vazio. Isto deve gerar uma exceção, porque a classe [Person] não aceita nomes próprios vazios. Este exemplo ilustra a diferença entre um dicionário e um objeto. Este último pode validar as suas propriedades, enquanto o dicionário não pode;
Os resultados são os seguintes:
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. O script [fromdict_03]
O script [fromdict_03] é o seguinte:
- Linha 10: Criamos um Professor a partir de um dicionário que contém uma chave (sexo) que não pertence à classe [Professor]. Deveria, então, ser levantada uma exceção;
Os resultados são os seguintes:
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. O script [fromdict_04]
O script [fromdict_04] é uma cópia do [fromdict_03] com uma pequena diferença:
- linha 10: utilizámos o parâmetro [silent=True] para indicar que, se uma chave do dicionário não for uma propriedade da classe [Teacher], deve simplesmente ser ignorada. Neste caso, não será levantada nenhuma exceção;
Os resultados são os seguintes:
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. O método [BaseEntity.asdict]
13.2.2.1. Definição
O método [BaseEntity.asdict] devolve um dicionário cujas chaves são as propriedades do 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
Comentários
- linha 1: a função [asdict] devolve o dicionário das propriedades do objeto;
- linha 1: [included_keys]: a lista de chaves a incluir no dicionário;
- linha 1: [excluded_keys]: a lista de chaves a excluir do dicionário;
- linha 3: a propriedade [self.__dict__] devolve o dicionário de propriedades do objeto. Os nomes das propriedades são as chaves e os seus valores são os valores do dicionário. Um objeto pode conter referências a outros objetos. Nesse caso, os nomes das propriedades são precedidos pelo nome da classe a que pertencem. Isto é algo que não queremos. Queremos as propriedades sem o seu prefixo;
- linha 3: é importante compreender aqui que, se a função [asdict] for executada dentro de uma classe derivada de [BaseEntity], a propriedade [self.__dict__] devolve o dicionário de propriedades do objeto derivado;
- linha 5: o dicionário que vamos construir;
- linha 7: iteramos sobre os valores de [self.__dict__] na forma (chave, valor);
- linha 9: se a chave atual pertencer à lista de chaves a incluir, então é adicionada ao dicionário [new_attributes] utilizando a função [set_value], que descreveremos em breve;
- linha 12: se o parâmetro [included_keys] não estiver presente, então é utilizado o parâmetro [excluded_keys]. Se a propriedade não estiver entre as propriedades a excluir, então é adicionada ao dicionário [new_attributes];
- linha 12: existem várias formas de excluir uma propriedade do dicionário:
- foi definida ao nível do atributo de classe [excluded_keys];
- foi definida na lista [excluded_keys] passada à função [asdict];
- o parâmetro [included_keys] está presente e não inclui a propriedade;
- linha 15: devolvemos o dicionário [new_attributes]
A função [set_value] nas linhas 10 e 13 é a seguinte:
Comentários
- linha 4: verifique se a chave está no formato __Class_key. Este é o formato que assume se pertencer a um objeto incluído no objeto principal. Neste caso, queremos manter apenas a string [key];
- linha 7: mantemos apenas a cadeia de caracteres que se segue aos dois últimos caracteres sublinhados da cadeia;
- linhas 8–10: se a chave não estiver no formato __Class_key, mantemo-la tal como está;
- linhas 11–14: o valor associado à chave [newkey] é calculado pelo método estático [BaseEntity.check_value];
O método estático [BaseEntity.check_value] é o seguinte:
- linha 1: o método [check_value] é estático (um método de classe, não um método de instância). Recebe como parâmetro o valor a ser associado a uma chave do dicionário:
- linha 17: se este valor for de um tipo simples, permanece inalterado;
- linhas 5-6: se este valor for do tipo BaseEntity, o valor é substituído pelo seu dicionário. Isto resulta numa chamada recursiva;
- linhas 8–9: se este valor for uma lista, é substituído pelo valor [BaseEntity.list2list];
- linhas 11–12: se este valor for um dicionário, é substituído pelo valor [BaseEntity.dict2dict];
O método estático [BaseEntity.list2list] é o seguinte:
- linha 2: o método recebe uma lista e devolve uma lista;
- linhas 5-6: cada valor na lista passada como parâmetro é substituído pelo valor devolvido pelo método estático [BaseEntity.check_value]. Trata-se, portanto, de uma chamada recursiva. O método estático [BaseEntity.check_value] é chamado até que o seu parâmetro [value] seja um tipo simples (não um tipo BaseEntity, lista ou dicionário);
O método estático [BaseEntity.dict2dict] é o seguinte:
- linha 2: o método recebe um dicionário e devolve um dicionário;
- linhas 5-6: cada valor no dicionário passado como parâmetro é substituído pelo valor devolvido pelo método estático [BaseEntity.check_value]. Trata-se, portanto, de uma chamada recursiva. O método estático [BaseEntity.check_value] é chamado até que o seu parâmetro [value] seja um tipo simples (não uma BaseEntity, lista ou dicionário);
13.2.2.2. Exemplos
O script [asdict_01] demonstra várias utilizações do método [asdict]:
Os resultados da execução são os seguintes:
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
- A linha 4 demonstra a vantagem do método [asdict] em relação ao uso da propriedade [__dict__]. As propriedades são despojadas do seu prefixo de classe. Isto torna-as mais fáceis de visualizar;
- Existem várias formas de utilizar o método [asdict]:
- se quiser todas as propriedades: utilize o método [asdict] sem parâmetros;
- se quiser apenas determinadas propriedades:
- se houver mais propriedades para incluir do que para excluir: utilize o único parâmetro [excluded_keys];
- Se houver menos propriedades para incluir do que para excluir: usaremos apenas o parâmetro [included_keys];
13.2.3. O método [BaseEntity.asjson]
Este método devolve a cadeia JSON de um objeto [BaseEntity] ou das suas classes derivadas. Apresenta a cadeia JSON do dicionário devolvido pelo método [asdict]. O seu código é o seguinte:
- linha 1: os parâmetros do método [asjson] são os do método [asdict];
Aqui está um exemplo (asjson_01) que utiliza este método:
Os resultados são os seguintes:
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
O método [BaseEntity.__str__] utiliza o método [asjson] para exibir a identidade do objeto [BaseEntity] ou dos seus objetos derivados:
# toString
def __str__(self) -> str:
return self.asjson()
13.2.4. O método [BaseEntity.fromjson]
O método [BaseEntity.fromjson] permite-lhe inicializar um objeto do tipo [BaseEntity] ou de um tipo derivado a partir de um dicionário JSON. O seu código é o seguinte:
- Linha 1: O método aceita dois parâmetros:
- [json_state]: o dicionário JSON utilizado para inicializar o objeto [BaseEntity];
- [silent]: para indicar se a presença no dicionário JSON de uma chave que não pode ser aceite como propriedade do objeto [BaseEntity] desencadeia uma exceção (silent=False) ou é simplesmente ignorada (silent=True);
- Linha 3: Começamos por construir o dicionário Python que representa o dicionário JSON e, em seguida, utilizamos o método [fromdict] para inicializar o objeto [BaseEntity] a partir deste dicionário Python;
Aqui está um exemplo (fromjson_01):
- linha 11: criamos a cadeia JSON de um dicionário;
- linha 12: um objeto [Enseignant] é inicializado com esta string;
- linha 13: o professor é apresentado;
Os resultados são os seguintes:
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. O script [main]
O script [main] resume os vários métodos encontrados:
Os resultados da execução são os seguintes:
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
Observe a linha 2 dos resultados: é a propriedade [ChildEntity.__dict__] (linha 38 do código) que nos permite determinar os nomes das propriedades a incluir nas listas [included_keys] e [excluded_keys]. Observe, ainda na linha 2 dos resultados, que, dependendo de a propriedade ser definida dentro da classe através de um getter/setter ou criada como se criasse uma chave de dicionário, ela pode ou não ser prefixada com o nome da classe [ChildEntity].