14. Arquitetura em camadas e programação por interfaces
14.1. Introdução
Propomos escrever um aplicativo que permita exibir as notas dos alunos de uma escola de ensino fundamental II. Esse aplicativo pode ter uma arquitetura em camadas:

- a camada [ui] (Interface do Usuário) é a camada que interage com o usuário do aplicativo;
- A camada [métier] implementa as regras de gestão do aplicativo, como o cálculo de um salário ou de uma fatura. Essa camada utiliza dados provenientes do usuário por meio da camada [présentation] e da camada SGBD por meio da camada [dao];
- a camada [dao] (Objetos de Acesso a Dados) gerencia o acesso aos dados da camada SGBD (Sistema de Gerenciamento de Bancos de Dados).
Essa é a arquitetura que foi utilizada no |curso sobre Python 2|. Também é possível introduzir uma variante:

As diferenças em relação à estrutura em camadas anterior são as seguintes:
- um script principal chamado [main], mencionado acima, organiza a instanciação das camadas;
- as camadas [ui, métier, dao] não se comunicam mais necessariamente entre si. Se precisarem, o script [main] fornece a elas as referências das camadas de que precisam;
O código está organizado aqui em centros de competência, com um “maestro”:
- o “maestro” é o script principal [main];
- as camadas [ui], [dao] e [métier] são os centros de competência;
Poderíamos chamar essa organização de organização orquestral.
14.2. Exemplo 1
Vamos ilustrar a arquitetura em camadas com um aplicativo de console simples:
- não haverá banco de dados;
- a camada [dao] gerenciará as entidades Elève, Classe, Matière e Note, permitindo o gerenciamento das notas dos alunos;
- a camada [métier] permitirá calcular indicadores com base nas notas de um aluno específico;
- a camada [ui] será um aplicativo de console que exibirá os resultados dos alunos;
O projeto PyCharm do aplicativo é o seguinte:
![]() |
Observação: as pastas em azul fazem parte do [Sources Root] do projeto PyCharm.
14.2.1. As entidades do aplicativo
Chamaremos de entidades as classes cuja única função é encapsular dados. Para isso, poderíamos utilizar dicionários. O objetivo da classe é permitir testar a validade dos dados armazenados no objeto e fornecer um método que retorne a identidade do objeto na forma de uma sequência de caracteres.
![]() |
14.2.1.1. A entidade [Classe]
A entidade [Classe] (Classe.py) representa uma turma do ensino médio:
# importações
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils
class Classe(BaseEntity):
# atributos excluídos do estado da classe
excluded_keys = []
# propriedades da classe
@staticmethod
def get_allowed_keys() -> list:
# id: identificador da classe
# nome: nome da classe
return BaseEntity.get_allowed_keys() + ["nom"]
# getter
@property
def nom(self: object) -> str:
return self.__nom
# setters
@nom.setter
def nom(self: object, nom: str):
# o nome deve ser uma sequência de caracteres não vazia
if Utils.is_string_ok(nom):
self.__nom = nom
else:
raise MyException(11, f"Le nom de la classe {self.id} doit être une chaîne de caractères non vide")
Notas
- linha 7: a entidade [Classe] deriva da entidade [BaseEntity] analisada no parágrafo |A turma BaseEntity|;
- linhas 11-16: uma classe é definida por um n.º id e um nom (linha 16). A propriedade [id] é fornecida pela classe [BaseEntity] e o nome pela classe [Classe];
- linhas 18-30: getter/setter do atributo [nom];
14.2.1.2. A entidade [Matière]
A classe [Matière] (matière.py) é a seguinte:
# importações
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils
class Matière(BaseEntity):
# atributos excluídos do estado da classe
excluded_keys = []
# propriedades da classe
@staticmethod
def get_allowed_keys() -> list:
# id: identificador da disciplina
# nome: nome da disciplina
# coeficiente: coeficiente da disciplina
return BaseEntity.get_allowed_keys() + ["nom", "coefficient"]
# getter
@property
def nom(self: object) -> str:
return self.__nom
@property
def coefficient(self: object) -> float:
return self.__coefficient
# setters
@nom.setter
def nom(self: object, nom: str):
# o nome deve ser uma sequência de caracteres não vazia
if Utils.is_string_ok(nom):
self.__nom = nom
else:
raise MyException(21, f"Le nom de la matière {self.id} doit être une chaîne de caractères non vide")
@coefficient.setter
def coefficient(self, coefficient: float):
# o coeficiente deve ser um número real >=0
erreur = False
if isinstance(coefficient, (int, float)):
if coefficient >= 0:
self.__coefficient = coefficient
else:
erreur = True
else:
erreur = True
# erro?
if erreur:
raise MyException(22, f"Le coefficient de la matière {self.nom} doit être un réel >=0")
Notas
- linha 7: a classe [Classe] deriva da classe [BaseEntity];
- linhas 11-17: uma disciplina é definida por seu nº [id], seu nome [nom] e seu coeficiente [coefficient];
- linhas 19-50: getters/setters dos atributos da classe;
14.2.1.3. A entidade [Elève]
A classe [Elève] (élève.py) é a seguinte:
# importações
from BaseEntity import BaseEntity
from Classe import Classe
from MyException import MyException
from Utils import Utils
class Elève(BaseEntity):
# atributos excluídos do estado da classe
excluded_keys = []
# propriedades da classe
@staticmethod
def get_allowed_keys() -> list:
# id: identificador do aluno
# sobrenome: sobrenome do aluno
# nome próprio: nome próprio do aluno
# turma: turma do aluno
return BaseEntity.get_allowed_keys() + ["nom", "prénom", "classe"]
# getters
@property
def nom(self: object) -> str:
return self.__nom
@property
def prénom(self: object) -> str:
return self.__prénom
@property
def classe(self: object) -> Classe:
return self.__classe
# setters
@nom.setter
def nom(self: object, nom: str) -> str:
# o sobrenome deve ser uma sequência de caracteres não vazia
if Utils.is_string_ok(nom):
self.__nom = nom
else:
raise MyException(41, f"Le nom de l'élève {self.id} doit être une chaîne de caractères non vide")
@prénom.setter
def prénom(self: object, prénom: str) -> str:
# o nome deve ser uma sequência de caracteres não vazia
if Utils.is_string_ok(prénom):
self.__prénom = prénom
else:
raise MyException(42, f"Le prénom de l'élève {self.id} doit être une chaîne de caractères non vide")
@classe.setter
def classe(self: object, value):
try:
# espera-se um tipo Classe
if isinstance(value, Classe):
self.__classe = value
# ou um tipo `dict`
elif isinstance(value,dict):
self.__classe=Classe().fromdict(value)
# ou um tipo json
elif isinstance(value,str):
self.__classe = Classe().fromjson(value)
except BaseException as erreur:
raise MyException(43, f"L'attribut [{value}] de l'élève {self.id} doit être de type Classe ou dict ou json. Erreur : {erreur}")
Notas
- linha 9: a classe [Elève] deriva da classe [BaseEntity];
- linhas 13-20: um aluno é identificado pelo seu nº [id], seu sobrenome [nom], seu nome próprio [prénom] e sua turma [classe]. Este último parâmetro é uma referência a um objeto [Classe];
- linhas 22-65: getters/setters dos atributos da classe;
14.2.1.4. A entidade [Note]
A classe [Note] (note.py) é a seguinte:
# importações
from BaseEntity import BaseEntity
from Elève import Elève
from Matière import Matière
from MyException import MyException
class Note(BaseEntity):
# atributos excluídos do estado da classe
excluded_keys = []
# propriedades da classe
@staticmethod
def get_allowed_keys() -> list:
# id: identificador da nota
# valor: a própria nota
# aluno: aluno (do tipo Aluno) ao qual a nota se refere
# matéria: matéria (do tipo Matéria) à qual a nota se refere
# o objeto Nota é, portanto, a nota de um aluno em uma disciplina
return BaseEntity.get_allowed_keys() + ["valeur", "élève", "matière"]
# getters
@property
def valeur(self: object) -> float:
return self.__valeur
@property
def élève(self: object) -> Elève:
return self.__élève
@property
def matière(self: object) -> Matière:
return self.__matière
# getters
@valeur.setter
def valeur(self: object, valeur: float):
# a nota deve ser um número real entre 0 e 20
if isinstance(valeur, (int, float)) and 0 <= valeur <= 20:
self.__valeur = valeur
else:
raise MyException(31,
f"L'attribut {valeur} de la note {self.id} doit être un nombre dans l'intervalle [0,20]")
@élève.setter
def élève(self: object, value):
try:
# espera-se um tipo “Aluno”
if isinstance(value, Elève):
self.__élève = value
# ou um tipo dict
elif isinstance(value, dict):
self.__élève = Elève().fromdict(value)
# ou um tipo json
elif isinstance(value, str):
self.__élève = Elève().fromjson(value)
except BaseException as erreur:
raise MyException(32,
f"L'attribut [{value}] de la note {self.id} doit être de type Elève ou dict ou json. Erreur : {erreur}")
@matière.setter
def matière(self: object, value):
try:
# espera-se um tipo “Matière”
if isinstance(value, Matière):
self.__matière = value
# ou um tipo dict
elif isinstance(value, dict):
self.__matière = Matière().fromdict(value)
# ou um tipo json
elif isinstance(value, str):
self.__matière = Matière().fromjson(value)
except BaseException as erreur:
raise MyException(33,
f"L'attribut [{value}] de la note {self.id} doit être de type Matière ou dict ou json. Erreur : {erreur}")
Notas
- linha 8: a classe [Note] deriva da classe [BaseEntity];
- linhas 12-20: um objeto [Note] é caracterizado por seu nº [id], pelo valor da nota [valeur], uma referência [élève] ao aluno que recebeu essa nota e uma referência à disciplina [matière] objeto da nota;
- linhas 22-75: getters/setters dos atributos da classe;
14.2.2. Configuração do aplicativo
![]() |
O arquivo [config.py] configura o ambiente do script principal [main] (1), bem como o dos testes (2). Todos esses scripts possuem uma instrução [import config] no início do código. Vale lembrar que a pasta que contém o script objeto do comando [python script] faz parte automaticamente do Python Path.Si; portanto, como o [config] está na mesma pasta que os scripts com a instrução [import config], ele será encontrado. Os arquivos [1] e [2] são idênticos neste caso. Isso pode não ser o caso.
O arquivo [config.sys] é o seguinte:
def configure():
import os
# caminho absoluto da pasta deste script
script_dir = os.path.dirname(os.path.abspath(__file__))
# root_dir
root_dir="C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes"
# dependências absolutas
absolute_dependencies=[
# pastas locais contendo classes e interfaces
f"{root_dir}/02/entities",
f"{script_dir}/../entities",
f"{script_dir}/../interfaces",
f"{script_dir}/../services",
]
# atualização do syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# fazemos a configuração
return {}
- linhas 11-14: as pastas que devem fazer parte do Python Path (sys.path);
- a pasta [f"{root_dir}/02/entities"] dá acesso às classes [BaseEntity] e [MyException];
- A pasta [f"{script_dir}/../entities"] dá acesso às classes [Elève], [Classe], [Matière] e [Note];
- a pasta [f"{script_dir}/../interfaces",] dá acesso às interfaces do aplicativo;
- a pasta [f"{script_dir}/../services"] dá acesso às classes que implementam as interfaces;
14.2.3. Testes das entidades
![]() |
Aqui, vamos escrever testes executados por uma ferramenta chamada [unittest]. O PyCharm vem com várias estruturas de teste. A escolha de uma delas é feita na configuração do PyCharm:

- no [4], estão disponíveis várias estruturas de teste:

14.2.3.1. A classe de testes [TestBaseEntity]
O script de teste [TestBaseEntity] será o seguinte:
import unittest
# configurando o aplicativo
import config
config = config.configure()
class TestBaseEntity (unittest.TestCase):
def test_note1(self):
# importações
from Note import Note
from Elève import Elève
from Classe import Classe
from Matière import Matière
# geração de uma nota a partir de uma string jSON
note = Note().fromjson(
'{"id": 8, "valor": 12, "aluno": {"id": 42, "sobrenome": "sobrenome4", "nome": "nome4", "turma": {"id": 2, "nome": "turma2"}}, "disciplina": {"id": 2, "nome": "disciplina2", "coeficiente": 2}}')
# verificações
self.assertIsInstance(note, Note)
self.assertIsInstance(note.élève, Elève)
self.assertIsInstance(note.élève.classe, Classe)
self.assertIsInstance(note.matière, Matière)
def test_note2(self):
# importações
from Note import Note
from Elève import Elève
from Classe import Classe
from Matière import Matière
# criação de uma nota a partir de um dicionário
note = Note().fromdict(
{"id": 8, "valeur": 12, "élève": {"id": 42, "nom": "nom4", "prénom": "prénom4",
"classe": {"id": 2, "nom": "classe2"}},
"matière": {"id": 2, "nom": "matière2", "coefficient": 2}})
# verificações
self.assertIsInstance(note, Note)
self.assertIsInstance(note.élève, Elève)
self.assertIsInstance(note.élève.classe, Classe)
self.assertIsInstance(note.matière, Matière)
if __name__ == '__main__':
unittest.main()
Notas
- linha 1: importa-se o módulo [unittest], que fornecerá os diversos métodos de teste;
- linhas 3-6: configura-se o aplicativo para que as classes necessárias aos testes sejam localizadas;
- linha 9: uma classe de teste [unittest] deve estender a classe [unittest.TestCase];
- linhas 11, 27: as funções de teste devem ter um nome que comece por [test]; caso contrário, não serão reconhecidas;
- linhas 13-16: importam-se as classes necessárias;
- nesta classe de teste, queremos verificar o comportamento dos métodos [BaseEntity.fromdict] (linha 34) e [BaseEntity.fromjson] (linha 18). A classe [Note] possui propriedades que são referências a outras classes. Queremos verificar se os dois métodos anteriores criam objetos [Note] válidos;
- linha 18: cria-se um objeto [Note] a partir de um objeto jSON;
- linha 21: verifica-se se o objeto criado é realmente do tipo [Note]. O método [assertIsInstance] é um método da classe [unittest.TestCase], classe pai da classe [TestBaseEntity];
- linha 22: verifica-se se [note.élève] é realmente do tipo [Elève];
- linha 23: verifica-se se [note.élève.classe] é, de fato, do tipo [Classe];
- linha 24: verifica-se se [note.matière] é realmente do tipo [Matière];
- linhas 33-42: faz-se o mesmo com o método [BaseEntity.fromdict];
Existem várias maneiras de executar os testes:
![]() |
- no [1-2], executa-se o [TestBaseEntity] com o framework [UnitTest];
- no [3-5], os testes falham. O [UnitTests] indica que não encontrou nenhum teste para executar;
A falha nos testes se deve à organização do código do [TestBaseEntity]:
import unittest
# configuração do aplicativo
import config
config = config.configure()
class TestBaseEntity(unittest.TestCase):
O que está causando problemas para o framework [UnitTest] é a presença de código executável, nas linhas 3 a 6, antes da definição da classe de teste, na linha 9.
Reorganizamos, então, o código da seguinte maneira:
import unittest
class TestBaseEntity(unittest.TestCase):
def setUp(self):
# configuração do aplicativo
import config
config.configure()
def test_note1(self):
…
def test_note2(self):
…
if __name__ == '__main__':
unittest.main()
- linhas 6-10: define-se uma função [setUp]. Essa função tem uma função específica: ela é executada antes de cada função de teste (test_note1, test_note2);
Feito isso, a execução da classe [TestBaseEntity] produz os seguintes resultados:
![]() |
Desta vez, os dois métodos de teste foram executados e os testes foram bem-sucedidos.
Vamos ver o que acontece quando um teste falha. Modifiquemos o código de [test_note1] da seguinte maneira:
def test_note1(self):
# erro proposital — verifica-se se 1 == 2
self.assertEqual(1,2)
# importações
from Note import Note
…
- linha 2: verifica-se se 1 == 2;
Os resultados da execução são, então, os seguintes:
![]() |
É possível identificar a causa do erro clicando no teste com falha [2]:
![]() |
- em [7-8], a causa do erro;
Outra maneira de executar uma classe de testes é executá-la em um terminal:
![]() |
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\troiscouches\v01\tests>python -m unittest TestBaseEntity.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.026s
OK
A linha 6 indica que os dois testes foram bem-sucedidos (o erro 1==2 foi removido);
Por fim, uma terceira maneira de executar a classe de testes [TestBaseEntity], ainda em um terminal, é a seguinte. Concluímos a classe de testes com as seguintes linhas 6 e 7;
…
self.assertIsInstance(note.élève.classe, Classe)
self.assertIsInstance(note.matière, Matière)
if __name__ == '__main__':
unittest.main()
- linha 6: a variável [__name__] é o nome dado ao script que será executado. Quando o script é aquele iniciado pelo comando [python script.py], a variável [__name__] assume o valor [__main__] (dois caracteres sublinhados antes e depois do identificador). Assim, a linha 7 só é executada quando o script [TestBaseEntity] é iniciado pelo comando [python TestBaseEntity.py]. A instrução [unittest.main()] inicia a execução do script pelo framework [UnitTest]. Veja um exemplo:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\troiscouches\v01\tests>python TestBaseEntity.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.013s
OK
14.2.3.2. A classe de testes [TestEntités]
A classe de testes [TestEntités] é a seguinte:
import unittest
class TestEntités(unittest.TestCase):
def setUp(self):
# configurando o aplicativo
import config
config.configure()
def test_code1a(self):
# importações
from Elève import Elève
from MyException import MyException
# código de erro
code = None
try:
# ID inválido
Elève().fromdict({"id": "x", "nom": "y", "prénom": "z", "classe": "t"})
except MyException as ex:
print(f"\ncode erreur={ex.code}, message={ex}")
code = ex.code
# verificação
self.assertEqual(code, 1)
def test_code41(self):
# importações
from Elève import Elève
from MyException import MyException
# código de erro
code = None
try:
# nome inválido
Elève().fromdict({"id": 1, "nom": "", "prénom": "z", "classe": "t"})
except MyException as ex:
print(f"\ncode erreur={ex.code}, message={ex}")
code = ex.code
# verificação
self.assertEqual(code, 41)
def test_code42(self):
# importações
from Elève import Elève
from MyException import MyException
# código de erro
code = None
try:
# nome inválido
Elève().fromdict({"id": 1, "nom": "y", "prénom": "", "classe": "t"})
except MyException as ex:
print(f"\ncode erreur={ex.code}, message={ex}")
code = ex.code
# verificação
self.assertEqual(code, 42)
def test_code43(self):
# importações
from Elève import Elève
from MyException import MyException
# código de erro
code = None
try:
# classe inválida
Elève().fromdict({"id": 1, "nom": "y", "prénom": "z", "classe": "t"})
except MyException as ex:
print(f"\ncode erreur={ex.code}, message={ex}")
code = ex.code
# verificação
self.assertEqual(code, 43)
def test_code1b(self):
# importações
from Classe import Classe
from MyException import MyException
# código de erro
code = None
try:
# identificador inválido
Classe().fromdict({"id": "x", "nom": "y"})
except MyException as ex:
print(f"\ncode erreur={ex.code}, message={ex}")
code = ex.code
# verificação
self.assertEqual(code, 1)
def test_code11(self):
# importações
from Classe import Classe
from MyException import MyException
# código de erro
code = None
try:
# nome inválido
Classe().fromdict({"id": 1, "nom": ""})
except MyException as ex:
code = ex.code
# verificação
self.assertEqual(code, 11)
def test_code1c(self):
# importações
from Matière import Matière
from MyException import MyException
# código de erro
code = None
try:
# identificador inválido
Matière().fromdict({"id": "x", "nom": "y", "coefficient": "t"})
except MyException as ex:
print(f"\ncode erreur={ex.code}, message={ex}")
code = ex.code
# verificação
self.assertEqual(code, 1)
def test_code21(self):
# importações
from Matière import Matière
from MyException import MyException
# código de erro
code = None
try:
# nome inválido
Matière().fromdict({"id": "1", "nom": "", "coefficient": "t"})
except MyException as ex:
print(f"\ncode erreur={ex.code}, message={ex}")
code = ex.code
# verificação
self.assertEqual(code, 21)
def test_code22(self):
# importações
from Matière import Matière
from MyException import MyException
# código de erro
code = None
try:
# coeficiente inválido
Matière().fromdict({"id": 1, "nom": "y", "coefficient": "t"})
except MyException as ex:
print(f"\ncode erreur={ex.code}, message={ex}")
code = ex.code
# verificação
self.assertEqual(code, 22)
def test_code1d(self):
# importações
from Note import Note
from MyException import MyException
# código de erro
code = None
try:
# identificador inválido
Note().fromdict({"id": "x", "valeur": "x", "élève": "y", "matière": "z"})
except MyException as ex:
print(f"\ncode erreur={ex.code}, message={ex}")
code = ex.code
# verificação
self.assertEqual(code, 1)
def test_code31(self):
# importações
from Note import Note
from MyException import MyException
# código de erro
code = None
try:
# valor inválido
Note().fromdict({"id": 1, "valeur": "x", "élève": "y", "matière": "z"})
except MyException as ex:
print(f"\ncode erreur={ex.code}, message={ex}")
code = ex.code
# verificação
self.assertEqual(code, 31)
def test_code32(self):
# importações
from Note import Note
from MyException import MyException
# código de erro
code = None
try:
# aluno inválido
Note().fromdict({"id": 1, "valeur": 10, "élève": "y", "matière": "z"})
except MyException as ex:
print(f"\ncode erreur={ex.code}, message={ex}")
code = ex.code
# verificação
self.assertEqual(code, 32)
def test_code33(self):
# importações
from Elève import Elève
from Note import Note
from Classe import Classe
from MyException import MyException
# código de erro
code = None
try:
# disciplina inválida
classe = Classe().fromdict({"id": 1, "nom": "x"})
élève = Elève().fromdict({"id": 1, "nom": "a", "prénom": "b", "classe": classe})
Note().fromdict({"id": 1, "valeur": 10, "élève": élève, "matière": "z"})
except MyException as ex:
print(f"\ncode erreur={ex.code}, message={ex}")
code = ex.code
# verificação
self.assertEqual(code, 33)
def test_exception(self):
# importações
from Elève import Elève
# o teste deve ser executado com o tipo [MyException] para ser aprovado
from MyException import MyException
with self.assertRaises(MyException):
# o teste
Elève().fromdict({"id": "x", "nom": "y", "prénom": "z", "classe": "t"})
if __name__ == '__main__':
unittest.main()
- O script de teste tem como objetivo testar os setters das classes: verificar se não é possível atribuir valores incorretos aos atributos das diferentes entidades;
- linhas 11-24: verifica-se se não é possível atribuir um identificador inválido a um aluno. Como o valor 'x', na linha 16, é passado como identificador do aluno, espera-se que ocorra uma exceção. Portanto, deve-se avançar para as linhas 20-22;
- linha 21: exibição da mensagem de erro;
- linha 22: recuperamos o código do erro (ver parágrafo |A entidade MyException|);
- linha 24: verifica-se (assert) se o código de erro é 1. Aqui, verificam-se duas coisas:
- que realmente ocorreu um erro;
- que o código de erro é 1;
- esse processo é repetido com as funções das linhas 24 a 213;
- linhas 215-222: verifica-se se uma ação gera uma exceção de um determinado tipo;
- linha 220: indica-se que o teste foi bem-sucedido se ele lançar uma exceção do tipo [MyException];
Resultados
Executa-se o script de teste:
![]() |
Os resultados obtidos são os seguintes:
Testing started at 09:39 ...
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe "C:\Program Files\JetBrains\PyCharm Community Edition 2020.1.2\plugins\python-ce\helpers\pycharm\_jb_unittest_runner.py" --path C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/TestEntités.py
Launching unittests with arguments python -m unittest C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/TestEntités.py in C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\troiscouches\v01\tests
code erreur=1, message=MyException[1, L'identifiant d'une entité <class 'Elève.Elève'> doit être un entier >=0]
code erreur=1, message=MyException[1, L'identifiant d'une entité <class 'Classe.Classe'> doit être un entier >=0]
code erreur=1, message=MyException[1, L'identifiant d'une entité <class 'Matière.Matière'> doit être un entier >=0]
code erreur=1, message=MyException[1, L'identifiant d'une entité <class 'Note.Note'> doit être un entier >=0]
code erreur=21, message=MyException[21, Le nom de la matière 1 doit être une chaîne de caractères non vide]
code erreur=22, message=MyException[22, Le coefficient de la matière y doit être un réel >=0]
code erreur=31, message=MyException[31, L'attribut x de la note 1 doit être un nombre dans l'intervalle [0,20]]
code erreur=32, message=MyException[32, L'attribut [y] de la note 1 doit être de type Elève ou dict ou json. Erreur : Expecting value: line 1 column 1 (char 0)]
code erreur=33, message=MyException[33, L'attribut [z] de la note 1 doit être de type Matière ou dict ou json. Erreur : Expecting value: line 1 column 1 (char 0)]
code erreur=41, message=MyException[41, Le nom de l'élève 1 doit être une chaîne de caractères non vide]
code erreur=42, message=MyException[42, Le prénom de l'élève 1 doit être une chaîne de caractères non vide]
code erreur=43, message=MyException[43, L'attribut [t] de l'élève 1 doit être de type Classe ou dict ou json. Erreur : Expecting value: line 1 column 1 (char 0)]
Ran 14 tests in 0.040s
OK
Process finished with exit code 0
Aqui, todos os testes foram bem-sucedidos
14.2.4. A camada [dao]

A camada [dao] implementa a interface [InterfaceDao] [1]. Esta é implementada pela classe [Dao] (2). O script [tests_dao] (3) testa os métodos da camada [dao].
14.2.4.1. Interface [InterfaceDao]
Uma interface é um contrato estabelecido entre o código chamador e o código chamado. É o código chamado que oferece a interface:
![]() |
- O código chamador [1] não conhece a implementação do código chamado [3]. Ele conhece apenas a maneira de chamá-lo. É a interface [2] que lhe indica isso. Essa interface define um certo número de métodos/funções a serem utilizados para interagir com o código chamado. Essa interface também é chamada de API (Interface de Programação de Aplicativos);
A camada [dao] oferecerá a seguinte interface:
- [get_classes] exibe a lista das turmas do ensino fundamental II;
- [get_matières] exibe a lista das disciplinas ministradas na escola secundária;
- [get_élèves] exibe a lista dos alunos do ensino fundamental II;
- [get_notes] exibe a lista das notas de todos os alunos;
- [get_notes_for_élève_by_id] exibe as notas de um aluno específico;
- [get_élève_by_id] retorna um aluno identificado pelo seu número;
O código chamador utilizará apenas esses métodos. Ele não precisa saber como eles são implementados. Os dados podem, então, provir de diferentes fontes (armazenados estaticamente, de um banco de dados, de arquivos de texto...) sem que isso afete o código chamador. Isso é chamado de programação por interfaces.
O Python 3 possui um conceito próximo ao de interface: a classe abstrata. Vamos utilizá-la. Vamos agrupar as interfaces deste exemplo na pasta [interfaces].
Definimos uma classe abstrata [InterfaceDao] (InterfaceDao.py) para a camada [dao]:
# importações
from abc import ABC, abstractmethod
# interface DAO
from Elève import Elève
class InterfaceDao(ABC):
# lista de classes
@abstractmethod
def get_classes(self: object) -> list:
pass
# lista de alunos
@abstractmethod
def get_élèves(self: object) -> list:
pass
# lista de disciplinas
@abstractmethod
def get_matières(self: object) -> list:
pass
# lista de notas
@abstractmethod
def get_notes(self: object) -> list:
pass
# lista de notas de um aluno
@abstractmethod
def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
pass
# pesquisar um aluno pelo seu ID
@abstractmethod
def get_élève_by_id(self, élève_id: int) -> Elève:
pass
Notas:
- linha 2: ABC = Classe Base Abstrata. Importa-se do módulo [abc] a classe ABC, bem como o decorador [abstractmethod] utilizado nas linhas 10, 15, 20, 25, 30 e 35;
- linha 8: a classe abstrata se chama [InterfaceDao] e deriva da classe [ABC];
- os métodos da classe abstrata são decorados com o decorador [@abstractmethod], o que transforma o método assim decorado em um método abstrato: seu código não está definido. No entanto, insere-se código nele: a instrução [pass], que não faz nada;
- a classe abstrata [InterfaceDao] não pode ser instanciada. Somente as classes derivadas de [InterfaceDao] que tenham implementado todos os métodos de [InterfaceDao] podem ser instanciadas. Portanto, se criarmos duas classes, [Dao1] e [Dao2], derivadas da classe [InterfaceDao], ambas implementarão os métodos abstratos de [InterfaceDao]. Poderíamos dizer, portanto, que elas implementam a interface [InterfaceDao];
- as linguagens que implementam tanto interfaces quanto classes abstratas atribuem à interface um papel diferente daquele da classe abstrata. Uma interface não possui atributos e não pode ser instanciada. Uma classe pode implementar uma interface definindo todos os métodos desta;
14.2.4.2. Implementação [Dao]
A classe [Dao] (dao.py) implementa a interface [InterfaceDao] da seguinte maneira:
# importação de entidades e interfaces
from Classe import Classe
from Elève import Elève
from InterfaceDao import InterfaceDao
from Matière import Matière
from MyException import MyException
from Note import Note
# A camada [dao] implementa a interface InterfaceDao
class Dao(InterfaceDao):
# construtor
# construímos listas estáticas
def __init__(self):
# instanciamos as classes
classe1 = Classe().fromdict({"id": 1, "nom": "classe1"})
classe2 = Classe().fromdict({"id": 2, "nom": "classe2"})
self.classes = [classe1, classe2]
# as disciplinas
matière1 = Matière().fromdict({"id": 1, "nom": "matière1", "coefficient": 1})
matière2 = Matière().fromdict({"id": 2, "nom": "matière2", "coefficient": 2})
self.matières = [matière1, matière2]
# os alunos
élève11 = Elève().fromdict({"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": classe1})
élève21 = Elève().fromdict({"id": 21, "nom": "nom2", "prénom": "prénom2", "classe": classe1})
élève32 = Elève().fromdict({"id": 32, "nom": "nom3", "prénom": "prénom3", "classe": classe2})
élève42 = Elève().fromdict({"id": 42, "nom": "nom4", "prénom": "prénom4", "classe": classe2})
self.élèves = [élève11, élève21, élève32, élève42]
# as notas dos alunos nas diferentes disciplinas
note1 = Note().fromdict({"id": 1, "valeur": 10, "élève": élève11, "matière": matière1})
note2 = Note().fromdict({"id": 2, "valeur": 12, "élève": élève21, "matière": matière1})
note3 = Note().fromdict({"id": 3, "valeur": 14, "élève": élève32, "matière": matière1})
note4 = Note().fromdict({"id": 4, "valeur": 16, "élève": élève42, "matière": matière1})
note5 = Note().fromdict({"id": 5, "valeur": 6, "élève": élève11, "matière": matière2})
note6 = Note().fromdict({"id": 6, "valeur": 8, "élève": élève21, "matière": matière2})
note7 = Note().fromdict({"id": 7, "valeur": 10, "élève": élève32, "matière": matière2})
note8 = Note().fromdict({"id": 8, "valeur": 12, "élève": élève42, "matière": matière2})
self.notes = [note1, note2, note3, note4, note5, note6, note7, note8]
# -----------
# interface IDao
# -----------
Notas:
- linhas 1-7: importam-se as entidades e a interface [InterfaceDao];
- linha 11: a classe [Dao] deriva da classe abstrata [InterfaceDao]. Diremos que ela implementa a interface [InterfaceDao];
- linha 14: o construtor não possui parâmetros. Ele define estaticamente quatro listas:
- linhas 15-18: a lista de classes;
- linhas 19-22: a lista de disciplinas;
- linhas 23-28: a lista de alunos;
- linhas 29-38: a lista de notas;
- linhas 40-44: implementação dos métodos da interface [Interface Dao]. Aqui, não os definimos para ver a mensagem de erro emitida pelo Python;
Um programa de teste poderia ser o seguinte: [tests-dao.py]:
# configura-se o aplicativo
import config
config = config.configure()
# instanciação da camada [dao]
from Dao import Dao
daoImpl = Dao()
# lista de turmas
for classe in daoImpl.get_classes():
print(classe)
# lista de disciplinas
for matière in daoImpl.get_matières():
print(matière)
# lista de turmas
for élève in daoImpl.get_élèves():
print(élève)
# lista de notas
for note in daoImpl.get_notes():
print(note)
Observação: o script [tests-dao.py] não é um teste [unittest], pois não contém métodos cujo nome comece por [test_].
Os comentários são autoexplicativos. As linhas 11 a 25 utilizam a interface da camada [dao]. Não há aqui nenhuma suposição sobre a implementação real da camada. Na linha 9, instanciamos a camada [dao].
Os resultados da execução deste script 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/troiscouches/v01/tests/tests_dao.py
Traceback (most recent call last):
File "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/tests_dao.py", line 9, in <module>
daoImpl = Dao()
TypeError: Can't instantiate abstract class Dao with abstract methods get_classes, get_matières, get_notes, get_notes_for_élève_by_id, get_élève_by_id, get_élèves
Process finished with exit code 1
Vemos que ocorre um erro logo na instanciação da classe [Dao] (linha 3 acima). O interpretador do Python 3 nos informa que não é possível instanciar a classe, pois não definimos os métodos abstratos [get_classes, get_matières, get_notes, get_notes_for_élève_by_id, get_élève_by_id, get_élèves].
O PyCharm também reconhece o conceito de classe abstrata e nos sugere que definamos os métodos dessa classe:
![]() |
- em [1], clique com o botão direito do mouse no código;
- em [2-3], selecione [Generate / Implement Methods] para implementar os métodos ausentes da classe [Dao];
- em [4], selecione os métodos a serem implementados; neste caso, todos;
Feito isso, a classe [Dao] é complementada pela PyCharm da seguinte maneira:
# -----------
# interface IDao
# -----------
def get_classes(self: object) -> list:
pass
def get_élèves(self: object) -> list:
pass
def get_matières(self: object) -> list:
pass
def get_notes(self: object) -> list:
pass
def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
pass
def get_élève_by_id(self, élève_id: int) -> Elève:
pass
Completamos a classe [Dao] da seguinte maneira:
# -----------
# interface IDao
# -----------
# lista de turmas
def get_classes(self) -> list:
return self.classes
# lista de disciplinas
def get_matières(self) -> list:
return self.matières
# lista de alunos
def get_élèves(self) -> list:
return self.élèves
# lista de notas
def get_notes(self) -> list:
return self.notes
def get_notes_for_élève_by_id(self, élève_id: int) -> dict:
# pesquisar aluno
élève = self.get_élève_by_id(élève_id)
# consultar as notas
notes = list(filter(lambda n: n.élève.id == élève_id, self.get_notes()))
# exibição do resultado
return {"élève": élève, "notes": notes}
def get_élève_by_id(self, élève_id: int) -> Elève:
# filtrar os alunos
élèves = list(filter(lambda e: e.id == élève_id, self.get_élèves()))
# Encontrado?
if not élèves:
raise MyException(10, f"L'élève d'identifiant {élève_id} n'existe pas")
# resultado
return élèves[0]
- as linhas 5 a 19 não apresentam dificuldades;
- linhas 29 a 36: o método que retorna o aluno cujo número é passado. Se o aluno não existir, é lançada uma exceção;
- linha 31: a função [filter] permite filtrar uma lista:
- o primeiro parâmetro é o critério de filtragem;
- o segundo parâmetro é a lista a ser filtrada, neste caso, a lista de alunos;
- linha 31: o critério de filtragem da lista é implementado por meio da função [f(e :Elève)->bool]. Essa função é aplicada a cada um dos elementos da lista a ser filtrada. Se o elemento atender ao critério de filtragem, ele é mantido na lista filtrada; caso contrário, é excluído. Aqui, é possível:
- definir o nome da função f e implementá-la em outro local. A chamada à função [filter] passa então a ser [filter(f,self.get_élèves()];
- definir a função f. A chamada à função [filter] passa então a ser [filter(f(e :Elève){…},self.get_élèves()], onde [e] representa um elemento da lista filtrada, ou seja, um aluno. Foi isso que foi feito aqui. A definição da função f seria, neste caso, [f(e :Elève){return e.id==élève_id)]: um aluno só é selecionado se o número [id] não for igual ao número procurado. Tal função pode ser substituída por uma função chamada lambda: [lambda e: e.id == élève_id]:
- e: representa o parâmetro da função f, neste caso, um aluno. Pode-se usar o nome que se desejar;
- e.id==élève_id é o critério de filtragem: um aluno [e] só é selecionado se seu número [id] corresponder ao número procurado;
- linha 31: a função [filter] retorna a lista filtrada em um tipo que não é o tipo [list], mas que pode ser transformado no tipo [list]. É isso que fazemos aqui com a expressão [list(liste filtrée)];
- linhas 33-34: se a lista filtrada estiver vazia, significa que o aluno procurado não existe. Nesse caso, lançamos uma exceção;
- linha 36: se chegarmos até aqui, é porque não houve nenhuma exceção. Sabemos, então, que recuperamos uma lista com um único elemento (não há dois alunos com o mesmo número [id]). Portanto, retornamos o primeiro elemento da lista;
- linhas 21-27: o método [get_notes_for_élève_by_id] deve retornar as notas do aluno cujo número [id] foi passado a ele;
- linhas 22-23: começamos procurando o aluno com o nº [élève_id] usando o método [get_élève_by_id] que acabamos de comentar. Pode ocorrer uma exceção se o aluno procurado não existir. Como não há um try/catch em torno da instrução da linha 23, a exceção será repassada para o código chamador. É isso que se deseja;
- linhas 24-25: uma vez recuperado o aluno, recuperam-se todas as suas notas. Faz-se isso novamente com um filtro:
- o filtro é [filter(critère, self_getnotes()]. A lista a ser filtrada é, portanto, a lista de todas as notas de todos os alunos do colégio;
- o critério de filtragem é expresso por meio de uma função [lambda]: lambda n: n.élève.id == élève_id. O parâmetro n é um elemento da lista a ser filtrada, portanto, uma nota. O tipo [Note] possui uma propriedade [élève] que representa o aluno proprietário da nota. Portanto, é necessário que [n.élève.id], que representa o número desse aluno, seja igual ao número do aluno procurado;
Em seguida, executamos o script [tests-dao.py].
# configura-se o aplicativo
import config
config = config.configure()
# instanciação da camada [dao]
from Dao import Dao
daoImpl = Dao()
# lista de classes
for classe in daoImpl.get_classes():
print(classe)
# lista de disciplinas
for matière in daoImpl.get_matières():
print(matière)
# lista de turmas
for élève in daoImpl.get_élèves():
print(élève)
# lista de notas
for note in daoImpl.get_notes():
print(note)
# um aluno específico
print(daoImpl.get_élève_by_id(11))
# a lista de suas notas
dict1 = daoImpl.get_notes_for_élève_by_id(11)
print(f"élève n° 11 = {dict1['élève']}")
for note in dict1["notes"]:
print(f"note de l'élève n° 11 = {note}")
Assim, obtemos os seguintes resultados:
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/troiscouches/v01/tests/tests_dao.py
{"id": 1, "nom": "classe1"}
{"id": 2, "nom": "classe2"}
{"id": 1, "nom": "matière1", "coefficient": 1}
{"id": 2, "nom": "matière2", "coefficient": 2}
{"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}
{"id": 21, "nom": "nom2", "prénom": "prénom2", "classe": {"id": 1, "nom": "classe1"}}
{"id": 32, "nom": "nom3", "prénom": "prénom3", "classe": {"id": 2, "nom": "classe2"}}
{"id": 42, "nom": "nom4", "prénom": "prénom4", "classe": {"id": 2, "nom": "classe2"}}
{"id": 1, "valeur": 10, "élève": {"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 1, "nom": "matière1", "coefficient": 1}}
{"id": 2, "valeur": 12, "élève": {"id": 21, "nom": "nom2", "prénom": "prénom2", "classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 1, "nom": "matière1", "coefficient": 1}}
{"id": 3, "valeur": 14, "élève": {"id": 32, "nom": "nom3", "prénom": "prénom3", "classe": {"id": 2, "nom": "classe2"}}, "matière": {"id": 1, "nom": "matière1", "coefficient": 1}}
{"id": 4, "valeur": 16, "élève": {"id": 42, "nom": "nom4", "prénom": "prénom4", "classe": {"id": 2, "nom": "classe2"}}, "matière": {"id": 1, "nom": "matière1", "coefficient": 1}}
{"id": 5, "valeur": 6, "élève": {"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}
{"id": 6, "valeur": 8, "élève": {"id": 21, "nom": "nom2", "prénom": "prénom2", "classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}
{"id": 7, "valeur": 10, "élève": {"id": 32, "nom": "nom3", "prénom": "prénom3", "classe": {"id": 2, "nom": "classe2"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}
{"id": 8, "valeur": 12, "élève": {"id": 42, "nom": "nom4", "prénom": "prénom4", "classe": {"id": 2, "nom": "classe2"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}
{"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}
élève n° 11 = {"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}
note de l'élève n° 11 = {"id": 1, "valeur": 10, "élève": {"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 1, "nom": "matière1", "coefficient": 1}}
note de l'élève n° 11 = {"id": 5, "valeur": 6, "élève": {"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}
Process finished with exit code 0
É possível observar que, ao exibir uma nota (para os outros objetos, o procedimento é semelhante), também temos:
- o aluno proprietário da nota;
- a disciplina à qual a nota se refere;
É a função [BaseEntity.asdict] que produz esse resultado (ver parágrafo “link”).
14.2.5. A camada [métier]
![]() | ![]() |
- [InterfaceMétier] é a interface da camada [métier];
- [Métier] é a classe de implementação da camada [métier];
- [Testmétier] é uma classe de teste [UnitTest] da classe [Métier];
14.2.5.1. Interface [InterfaceMétier]
A camada [métier] implementará a seguinte interface [InterfaceMétier] (InterfaceMétier.py):
# importações
from abc import ABC, abstractmethod
from StatsForElève import StatsForElève
# interface de negócios
class InterfaceMétier(ABC):
# cálculo de estatísticas para um aluno
@abstractmethod
def get_stats_for_élève(self, idElève: int) -> StatsForElève:
pass
- [get_stats_for_élève] retorna as notas do aluno nº idElève, bem como informações sobre elas: média ponderada, nota mais baixa, nota mais alta. Essas informações estão encapsuladas em um objeto do tipo [StatsForElève];
14.2.5.2. A entidade [StatsForElève]
O tipo [StatsForElève] (StatsForElève.py), que encapsula as estatísticas (notas, mínima, máxima, média ponderada) de um aluno, é o seguinte:
# importações
from BaseEntity import BaseEntity
# estatísticas de um aluno específico
class StatsForElève(BaseEntity):
# atributos excluídos do relatório da turma
excluded_keys = []
# propriedades da turma
@staticmethod
def get_allowed_keys() -> list:
# id: identificador da nota
# aluno: o aluno em questão
# notas: suas notas
# moyennePondérée: sua média ponderada pelos coeficientes das disciplinas
# min: sua nota mínima
# máx.: sua nota máxima
return BaseEntity.get_allowed_keys() + ["élève", "notes", "moyenne_pondérée", "min", "max"]
# toString
def __str__(self) -> str:
# caso do aluno sem notas
if len(self.notes) == 0:
return f"Elève={self.élève}, notes=[]"
# caso do aluno com notas
str = ""
for note in self.notes:
str += f"{note.valeur} "
return f"Elève={self.élève}, notes=[{str.strip()}], max={self.max}, min={self.min}, " \
f"moyenne pondérée={self.moyenne_pondérée:4.2f}"
Notas:
- linha 8: a classe [StatsForElève] deriva da classe [BaseEntity];
- linhas 13-22: as propriedades da classe;
- um identificador [id] proveniente de [BaseEntity];
- o aluno [élève], cujas estatísticas são encapsuladas;
- suas notas [notes];
- sua média ponderada [moyenne_pondérée];
- sua nota mínima [min];
- sua nota máxima [max];
- não se definem getters/setters para esses atributos. Parte-se do princípio de que é a camada [métier] que cria objetos desse tipo e que ela não cria objetos inválidos;
- linhas 23-33: a função [__str__] retorna uma sequência de caracteres que contém as propriedades do objeto;
14.2.5.3. A implementação [Métier]
A implementação [Métier] (Metier.py) da interface [InterfaceMétier] será a seguinte:
# importações
from InterfaceDao import InterfaceDao
from InterfaceMétier import InterfaceMétier
from StatsForElève import StatsForElève
class Métier(InterfaceMétier):
# construtor
def __init__(self, dao: InterfaceDao):
# armazenamos o parâmetro
self.__dao = dao
# -----------
# interface
# -----------
# os indicadores relativos às notas de um aluno específico
def get_stats_for_élève(self, id_élève: int) -> StatsForElève:
# Estatísticas do aluno nº idEleve
# id_élève: número do aluno
# recupera-se suas notas com a camada [dao]
notes_élève = self.__dao.get_notes_for_élève_by_id(id_élève)
élève = notes_élève["élève"]
notes = notes_élève["notes"]
# interrompe-se se não houver notas
if len(notes) == 0:
# retorna o resultado
return StatsForElève().fromdict({"élève": élève, "notes": []})
# análise das notas do aluno
somme_pondérée = 0
somme_coeff = 0
max = -1
min = 21
for note in notes:
# valor da nota
valeur = note.valeur
# coeficiente da disciplina
coeff = note.matière.coefficient
# soma dos coeficientes
somme_coeff += coeff
# soma ponderada
somme_pondérée += valeur * coeff
# busca do mínimo
if valeur < min:
min = valeur
# busca do máximo
if valeur > max:
max = valeur
# cálculo dos indicadores ausentes
moyenne_pondérée = float(somme_pondérée) / somme_coeff
# o resultado é apresentado no formato de um tipo [StatsForElève]
return StatsForElève(). \
fromdict({"élève": élève, "notes": notes,
"moyenne_pondérée": moyenne_pondérée,
"min": min, "max": max})
Notas
- linha 7: a classe [Métier] deriva da classe [InterfaceMétier]. Costuma-se dizer que ela implementa a interface [InterfaceMétier];
- linhas 9-12: o construtor recebe como único parâmetro uma referência à camada [dao]. Na linha 10, observe-se que foi atribuído o tipo [InterfaceDao] ao parâmetro [dao]. Não se espera uma implementação específica, mas simplesmente uma implementação que respeite a interface [InterfaceDao]. Aqui, isso não importa, já que o Python não levará esse tipo em consideração, mas é um bom hábito trabalhar com interfaces em vez de implementações específicas. O código fica, assim, mais fácil de modificar;
- linhas 19-60: implementação do método [get_stats_for_élève];
- linha 19: o método recebe um único parâmetro, o número [idElève] do aluno cujas estatísticas queremos obter;
- linha 24: solicita-se à camada [dao] as notas do aluno. Essa solicitação gera uma exceção caso o aluno não exista. Essa exceção não é tratada (ausência de try/catch) e, portanto, é repassada para o código chamador;
- linha 25: chegamos aqui se não tiver ocorrido nenhuma exceção. [notes_élève] é, então, um dicionário com duas chaves [élève, note]:
- linha 25: recuperam-se as informações sobre o aluno (seu nome, sua turma, etc.);
- linha 26: recuperam-se as notas do aluno;
- linhas 28-31: verifica-se se o aluno tem notas. Se não tiver, não há estatísticas a serem calculadas;
- linha 31: retorna-se um objeto [StatsForElève] construído a partir de um dicionário com o método [BaseEntity.fromdict];
- linhas 33-54: utilizam-se as notas do aluno para calcular as estatísticas solicitadas. Os comentários do código devem ser suficientes para sua compreensão;
- linhas 56-60: retorna-se um objeto [StatsForElève] construído a partir de um dicionário com o método [BaseEntity.fromdict];
14.2.5.4. Teste da camada [métier]
Um script [UnitTest] da camada [métier] poderia ser o seguinte (TestMétier.py):
# importações
import unittest
class Testmétier(unittest.TestCase):
def setUp(self):
# configuração do aplicativo
import config
config.configure()
def test_statsForEleve11(self):
# importações
from Dao import Dao
from Métier import Métier
# testamos os indicadores do aluno 11
dao = Dao()
stats_for_élève = Métier(dao).get_stats_for_élève(11)
# exibição
print(f"\nstats={stats_for_élève}")
# verificações
self.assertEqual(stats_for_élève.min, 6)
self.assertEqual(stats_for_élève.max, 10)
self.assertAlmostEqual(stats_for_élève.moyenne_pondérée, 7.333, delta=1e-3)
if __name__ == '__main__':
unittest.main()
Notas
- linhas 6-9: a função [setUp] é usada aqui para configurar o Python Path do teste;
- linha 16: instanciamos a camada [dao];
- linha 17: instanciamos a camada [métier] e utilizamos seu método [get_stats_for_élève] para calcular as estatísticas do aluno nº 11;
- linha 19: exibe-se o resultado [StatsForElève] obtido. Como [StatsForElève] deriva de [BaseEntity], é a cadeia jSON de [StatsForElève] que é exibida aqui;
- linha 21: verifica-se a nota mínima do aluno;
- linha 22: verifica-se a nota máxima do aluno;
- linha 23: verifica-se se a média ponderada é igual a 7,333, com uma precisão de 10⁻³. Em geral, não é possível comparar números reais com exatidão, pois, internamente, eles costumam ter apenas uma representação aproximada;
Os resultados do teste são os seguintes:
Testing started at 18:17 ...
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe "C:\Program Files\JetBrains\PyCharm Community Edition 2020.1.2\plugins\python-ce\helpers\pycharm\_jb_unittest_runner.py" --path C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/TestMétier.py
Launching unittests with arguments python -m unittest C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/TestMétier.py in C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\troiscouches\v01\tests
Ran 1 test in 0.015s
OK
stats=Elève={"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, notes=[10 6], max=10, min=6, moyenne pondérée=7.33
Process finished with exit code 0
14.2.6. A camada [ui]

- em [1], a interface da camada [ui];
- em [2], a implementação dessa interface;
- em [3], o script principal do aplicativo;
14.2.6.1. Interface [InterfaceUi]
A interface da camada [UI] será a seguinte:
# importações
from abc import ABC, abstractmethod
# interface UI
class InterfaceUi(ABC):
# execução da camada UI
@abstractmethod
def run(self: object):
pass
Notas
- linhas 9-10: a camada [UI] terá apenas um método, [run];
14.2.6.2. A implementação [Console]
A camada [console] é implementada pelo seguinte script [Console.py]:
# importações de camadas
from InterfaceDao import InterfaceDao
from InterfaceMétier import InterfaceMétier
from InterfaceUi import InterfaceUi
# outras dependências
from MyException import MyException
class Console(InterfaceUi):
# construtor
def __init__(self: object, métier: InterfaceMétier):
# área de negócio: a camada [métier]
# os atributos são armazenados
self.métier = métier
# -----------
# interface
# -----------
def run(self):
# diálogo com o usuário
fini = False
while not fini:
# pergunta/resposta
réponse = input("Numéro de l'élève (>=1 et * pour arrêter) : ").strip()
# Concluído?
if réponse == "*":
break
# a entrada está correta?
ok = False
try:
id_élève = int(réponse, 10)
ok = id_élève >= 1
except ValueError as erreur:
pass
# dado correto?
if not ok:
print("Saisie incorrecte. Recommencez...")
continue
# cálculo das estatísticas para o aluno selecionado
try:
print(self.métier.get_stats_for_élève(id_élève))
except MyException as erreur:
print(f"L'erreur suivante s'est produite : {erreur}")
- linhas 3-5: importação de todas as interfaces;
- linha 11: a classe [Console] implementa a interface [InterfaceUi];
- linhas 12-17: o construtor da classe [Console] recebe como parâmetro uma referência à camada [métier]. Observe-se que atribuímos o tipo [InterfaceMétier] a esse parâmetro para lembrar que estamos trabalhando com interfaces, e não com implementações específicas;
- linha 24: implementação do método [run] da interface;
- linha 27: um loop que termina quando a condição da linha 31 é verificada;
- linha 29: entrada de um dado digitado no teclado. A função [input] recebe um parâmetro opcional: a mensagem a ser exibida na tela para solicitar a entrada. Essa mensagem é sempre recuperada como uma sequência de caracteres. A função [strip] remove os “espaços” que a precedem ou a seguem;
- linhas 34-39: verifica-se se a entrada, um número de aluno, é válida. Deve ser um número inteiro >=1. Vale lembrar que a entrada foi feita como uma sequência de caracteres;
- linha 36: tenta-se converter a entrada em um número inteiro na base 10. A função [int] lança uma exceção se isso não for possível;
- linha 37: só se chega aqui se não tiver ocorrido nenhuma exceção. Verifica-se se o número inteiro obtido é realmente >=1;
- linhas 38-39: trata-se da exceção. Se houve exceção, a variável [ok] da linha 34 permaneceu como [False];
- linhas 41-43: se a entrada estiver incorreta, exibe-se uma mensagem de erro e retorna-se ao início do ciclo (linha 43);
- linhas 45-48: calculam-se as estatísticas do aluno cujo número foi inserido;
- linha 46: utiliza-se o método [get_stats_for_élève] da camada [métier]. Esse método lança uma exceção caso o aluno não exista. A exceção é tratada nas linhas 47-48. Sabemos que as camadas [dao] e [métier] lançam a exceção [MyException];
14.3. O script principal [main]
O script principal [main] é o seguinte (main.py):
# configurando o aplicativo
import config
config = config.configure()
# o syspath está configurado — já é possível fazer as importações
from Console import Console
from Dao import Dao
from Métier import Métier
# ----------- camada [console]
try:
# instanciação da camada [dao]
dao = Dao()
# instanciação da camada [métier]
métier = Métier(dao)
# instanciação da camada [ui]
console = Console(métier)
# execução da camada [console]
console.run()
except BaseException as ex:
# exibe-se o erro
print(f"L'erreur suivante s'est produite : {ex}")
finally:
pass
- linhas 1-4: configura-se o Python Path do aplicativo;
- linhas 6-9: importam-se as classes e interfaces necessárias;
- linha 14: instanciação da camada [dao];
- linha 16: instanciação da camada [métier];
- linha 18: instanciação da camada [ui];
- linha 20: iniciamos a interação com o usuário;
- linhas 13-20: normalmente, nenhuma exceção é gerada nessas linhas. As exceções que são propagadas pelas camadas [dao] e [métier] são interceptadas pela camada [Console]. O gerenciamento de exceções é uma arte difícil quando não se conhece perfeitamente as camadas utilizadas (o que não é o caso aqui). Em caso de dúvidas, é possível adicionar código para interceptar qualquer tipo de exceção que possa ser lançada pelo código executado. É isso que é feito aqui, nas linhas 21-23. Interceptamos todas as exceções derivadas de [BaseException], ou seja, todas as exceções;
- linhas 24-25: a cláusula [finally] não tem função aqui. Ela está presente apenas para permitir que as linhas 21-23 sejam comentadas. De fato, no modo de depuração, não é recomendável interceptar as exceções. Nesse caso, é o interpretador Python que as intercepta e, então, fornece o número da linha em que a exceção ocorreu. Uma informação indispensável. Quando as linhas 21-23 são comentadas, a presença das linhas 24-25 permite ter um try/catch sintaticamente correto. Na ausência delas, o Python declara um erro;
Aqui está um exemplo de execução:
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/troiscouches/v01/main/main.py
Numéro de l'élève (>=1 et * pour arrêter) : 11
Elève={"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, notes=[10 6], max=10, min=6, moyenne pondérée=7.33
Numéro de l'élève (>=1 et * pour arrêter) : 1
L'erreur suivante s'est produite : MyException[10, L'élève d'identifiant 1 n'existe pas]
Numéro de l'élève (>=1 et * pour arrêter) : *
Process finished with exit code 0
14.4. Exemplo 2
Este novo exemplo de arquiteturas em camadas tem como objetivo mostrar a vantagem da programação por interfaces. Ela facilita a manutenção e o teste das aplicações. Utilizaremos novamente uma arquitetura de três camadas:

Cada camada será implementada de duas maneiras diferentes. Queremos mostrar que é possível alterar facilmente a implementação de uma camada com impacto mínimo nas demais.
14.4.1. A camada [dao]

A interface [InterfaceDao] é a seguinte:
# importações
from abc import ABC, abstractmethod
# interface DAO
class InterfaceDao(ABC):
# um único método
@abstractmethod
def do_something_in_dao_layer(self, x: int, y: int) -> int:
pass
- linhas 8-10: o método [do_something_in_dao_layer] é o único método da interface;
A classe [DaoImpl1] implementa a interface [InterfaceDao] da seguinte maneira:
from InterfaceDao import InterfaceDao
class DaoImpl1(InterfaceDao):
# implementação InterfaceDao
def do_something_in_dao_layer(self: InterfaceDao, x: int, y: int) -> int:
return x + y
A classe [DaoImpl2] implementa a interface [InterfaceDao] da seguinte maneira:
from InterfaceDao import InterfaceDao
class DaoImpl2(InterfaceDao):
# implementação InterfaceDao
def do_something_in_dao_layer(self: InterfaceDao, x: int, y: int) -> int:
return x - y
14.4.2. A camada [métier]

A interface [InterfaceMétier] é a seguinte:
# importações
from abc import ABC, abstractmethod
# interface de negócios
class InterfaceMétier(ABC):
# um único método
@abstractmethod
def do_something_in_métier_layer(self, x: int, y: int) -> int:
pass
- linhas 8-10: o método [do_something_in_métier_layer] é o único método da interface;
A classe [AbstractBaseMétier] implementa a interface [InterfaceMétier] da seguinte maneira:
# importações
from abc import ABC, abstractmethod
from InterfaceDao import InterfaceDao
from InterfaceMétier import InterfaceMétier
class AbstractBaseMétier(InterfaceMétier, ABC):
# propriedades
# __dao é uma referência na camada [dao]
@property
def dao(self) -> InterfaceDao:
return self.__dao
@dao.setter
def dao(self, dao: InterfaceDao):
self.__dao = dao
# implementação da interface [InterfaceMétier]
@abstractmethod
def do_something_in_métier_layer(self, x: int, y: int) -> int:
pass
- linha 8: a classe [AbstractBaseMétier] deriva de duas classes:
- [InterfaceMétier]: a classe [AbstractBaseMétier] implementa essa interface nas linhas 19 a 22. Na verdade, percebe-se que ela não implementou o método [do_something_in_métier_layer], que foi declarado abstrato (linha 20). Caberá às classes derivadas implementar o método;
- [ABC] para ter acesso às anotações [@abstractmethod];
- a ordem é importante: se a invertemos aqui, o Python gera um erro na execução;
Esta é a primeira vez que utilizamos a herança múltipla (herdar de várias classes). A classe [AbstractBaseMétier] herda, ao mesmo tempo, as propriedades das classes [InterfaceMétier] e [ABC].
- linhas 9-17: define-se a propriedade [dao], que será uma referência à camada [dao];
Uma interface destina-se a ser implementada. Quando diferentes implementações compartilham propriedades, é interessante colocá-las em uma classe pai para evitar duplicá-las. Esse é o caso, aqui, da propriedade [dao]. A classe pai geralmente é sempre abstrata, pois não sabe implementar todos os métodos da interface.
A classe [MétierImpl1] implementa a interface [InterfaceMétier] da seguinte maneira:
from AbstractBaseMétier import AbstractBaseMétier
class MétierImpl1(AbstractBaseMétier):
# implementação da interface [InterfaceMétier]
def do_something_in_métier_layer(self:AbstractBaseMétier, x: int, y: int) -> int:
x += 1
y += 1
return self.dao.do_something_in_dao_layer(x, y)
- linha 4: a classe [MétierImpl1] deriva da classe [AbstractbaseMétier]. Portanto, ela herda a propriedade [dao] dessa classe;
- linhas 6-9: implementação da interface [InterfaceMétier], que não foi implementada pela classe pai [AbstractbaseMétier];
- linha 9: utiliza-se a camada [dao];
A classe [MétierImpl2] implementa a interface [InterfaceMétier] de maneira análoga:
from AbstractBaseMétier import AbstractBaseMétier
class MétierImpl2(AbstractBaseMétier):
# implementação da interface [InterfaceMétier]
def do_something_in_métier_layer(self:AbstractBaseMétier, x: int, y: int) -> int:
x -= 1
y -= 1
return self.dao.do_something_in_dao_layer(x, y)
14.4.3. A camada [ui]

A interface [InterfaceUi] é a seguinte:
# importações
from abc import ABC, abstractmethod
# interface Ui
class InterfaceUi(ABC):
# um único método
@abstractmethod
def do_something_in_ui_layer(self, x: int, y: int) -> int:
pass
- linhas 8-10: o único método da interface;
A classe [AbstractBaseUi] implementa a interface [InterfaceUi] da seguinte maneira:
# importações
from abc import ABC, abstractmethod
from InterfaceMétier import InterfaceMétier
from InterfaceUi import InterfaceUi
class AbstractBaseUi(InterfaceUi, ABC):
# propriedades
# o negócio é uma referência na camada [métier]
@property
def métier(self) -> InterfaceMétier:
return self.__métier
@métier.setter
def métier(self, métier: InterfaceMétier):
self.__métier = métier
# implementação da interface [InterfaceUI]
@abstractmethod
def do_something_in_ui_layer(self: InterfaceUi, x: int, y: int) -> int:
pass
- a classe [AbstractBaseUi] é uma classe abstrata (linha 20). Ela deverá ser derivada para implementar a interface [InterfaceUi];
- linhas 9-17: a classe [AbstractBaseUi] possui uma referência à camada [métier];
A classe de implementação [UiImpl1] é a seguinte:
from AbstractBaseUi import AbstractBaseUi
class UiImpl1(AbstractBaseUi):
# implementação da interface [InterfaceUi]
def do_something_in_ui_layer(self: AbstractBaseUi, x: int, y: int) -> int:
x += 1
y += 1
return self.métier.do_something_in_métier_layer(x, y)
- linha 4: a classe [UiImpl1] deriva da classe [AbstractBaseUi] e, portanto, herda sua propriedade [métier]. Esta é utilizada na linha 9;
A classe de implementação [UiImpl2] é análoga:
from AbstractBaseUi import AbstractBaseUi
class UiImpl2(AbstractBaseUi):
# implementação da interface [InterfaceUi]
def do_something_in_ui_layer(self: AbstractBaseUi, x: int, y: int) -> int:
x -= 1
y -= 1
return self.métier.do_something_in_métier_layer(x, y)
- linha 4: a classe [UiImpl2] deriva da classe [AbstractBaseUi] e, portanto, herda sua propriedade [métier]. Essa propriedade é utilizada na linha 9;
14.4.4. Os arquivos de configuração

- os arquivos [config1, config2] configuram o aplicativo de duas maneiras diferentes;
- o arquivo [main] é o script principal do aplicativo;
O arquivo [config1] é o seguinte:
def configure():
# etapa 1 ------
# caminho absoluto da pasta deste script
import os
script_dir = os.path.dirname(os.path.abspath(__file__))
# dependências
absolute_dependencies = [
# pastas locais do Python Path
f"{script_dir}/../dao",
f"{script_dir}/../ui",
f"{script_dir}/../métier",
]
# configurando o syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# etapa 2 ------
# configuração das camadas do aplicativo
from DaoImpl1 import DaoImpl1
from MétierImpl1 import MétierImpl1
from UiImpl1 import UiImpl1
# instanciação das camadas
# DAO
dao = DaoImpl1()
# logica de negócio
métier = MétierImpl1()
métier.dao = dao
# interface do usuário
ui = UiImpl1()
ui.métier = métier
# colocamos as instâncias das camadas na configuração
# aqui, apenas a camada ui é necessária
config = {"ui": ui}
# geramos a configuração
return config
- linhas 2-16: configuração do Python Path do aplicativo;
- linhas 18-31: instanciação das camadas [dao, métier, ui]. Para implementar suas interfaces, escolhe-se sempre a primeira implementação criada;
- linhas 33-35: inserem-se as referências das camadas na configuração. Aqui, o script principal precisa apenas da camada [ui];
O arquivo [config2] é semelhante e implementa cada interface com a segunda implementação disponível:
def configure():
# etapa 1 ---
# caminho absoluto da pasta deste script
import os
script_dir = os.path.dirname(os.path.abspath(__file__))
# dependências
absolute_dependencies = [
# pastas locais do Python Path
f"{script_dir}/../dao",
f"{script_dir}/../ui",
f"{script_dir}/../métier",
]
# configurando o syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# etapa 2 ------
# configuração das camadas do aplicativo
from DaoImpl2 import DaoImpl2
from MétierImpl2 import MétierImpl2
from UiImpl2 import UiImpl2
# instanciação das camadas
# DAO
dao = DaoImpl2()
# logica de negócio
métier = MétierImpl2()
métier.dao = dao
# interface do usuário
ui = UiImpl2()
ui.métier = métier
# colocamos as instâncias das camadas na configuração
# aqui, apenas a camada ui é necessária
config = {"ui": ui}
# geramos a configuração
return config
14.4.5. O script principal [main]

O script principal é o seguinte:
# importações
import importlib
import sys
# main ---------
# são necessários dois argumentos
nb_args = len(sys.argv)
if nb_args != 2 or (sys.argv[1] != "config1" and sys.argv[1] != "config2"):
print(f"Syntaxe : {sys.argv[0]} config1 ou config2")
sys.exit()
# configuração do aplicativo
module = importlib.import_module(sys.argv[1])
config = module.configure()
# execução da camada [ui]
print(config["ui"].do_something_in_ui_layer(10, 20))
Este script recebe um parâmetro:
- [config1] para usar a configuração nº 1;
- [config2] para utilizar a configuração nº 2;
O Python armazena os parâmetros em uma lista [sys.argv]:
- sys.argv[0] é o nome do script, neste caso, [main]. Esse parâmetro está sempre presente;
-
sys.argv[1] é o primeiro parâmetro passado ao script, sys.argv[2] o segundo, …
-
linha 8: obtém-se o número de parâmetros;
- linhas 9-11: verifica-se se há de fato um parâmetro e se seu valor é [config1] ou [config2]. Caso contrário, é exibida uma mensagem de erro (linha 10) e o programa é encerrado (linha 11);
Uma vez conhecida a configuração desejada, precisamos executá-la. Por exemplo, se a configuração 1 foi escolhida, precisamos executar o código:
O problema aqui é que a configuração a ser utilizada está em uma variável, a variável [len[liste1]]. Para importar um módulo cujo nome está em uma variável, precisamos usar o pacote [importlib] (linha 2).
- linha 14: importamos o módulo cujo nome está em [sys.argv[1];
- linha 15: feito isso, executamos a função [configure] desse módulo. Recuperamos um dicionário [config], que é a configuração do aplicativo;
- linha 18: sabemos que uma referência da camada [ui] está em config[‘ui’]. Ela é utilizada para chamar o método [do_something_in_ui_layer]. Sabemos que esse método chamará um método da camada [métier], que, por sua vez, chamará um método da camada [dao];
Por exemplo, a função [do_something_in_ui_layer] é a seguinte:
class UiImpl1(AbstractBaseUi):
# implementação da interface [InterfaceUi]
def do_something_in_ui_layer(self: AbstractBaseUi, x: int, y: int) -> int:
x += 1
y += 1
return self.métier.do_something_in_métier_layer(x, y)
- a linha 6 acima utiliza a propriedade [métier] da classe [UiImpl1], linha 1. No entanto, na configuração [config1], foi escrito:
# área de negócios
métier = MétierImpl1()
métier.dao = dao
# interface do usuário
ui = UiImpl1()
ui.métier = métier
- linha 6: a propriedade [métier] de [UIImpl1] é uma referência à classe [MétierImpl1] (linha 2). Portanto, será executado o método [do_something_in_ui_layer] da classe [MétierImpl1];
Na classe [MétierUiImpl1], está escrito:
class MétierImpl1(AbstractBaseMétier):
# implementação da interface [InterfaceMétier]
def do_something_in_métier_layer(self: AbstractBaseMétier, x: int, y: int) -> int:
x += 1
y += 1
return self.dao.do_something_in_dao_layer(x, y)
- na linha 6, o método chamado pela camada [ui], por sua vez, chamará um método da propriedade [dao] da classe [MétierImpl1];
No entanto, na configuração [config1], está escrito:
# DAO
dao = DaoImpl1()
# domínio de negócios
métier = MétierImpl1()
métier.dao = dao
- linha 5: a propriedade [MétierImpl1.dao] é do tipo [DaoImpl1] (linha 2);
O que queremos mostrar aqui é que o script [main] não precisa se preocupar com as camadas [métier] e [dao]. Ele precisa se preocupar apenas com a camada [ui], já que as ligações entre essa camada e as outras foram estabelecidas por configuração.

Para passar o parâmetro [config1] ou [config2] ao script [main], deve-se proceder da seguinte maneira:

- no [1-2], cria-se o que se denomina uma configuração de execução;
- no [3], atribui-se um nome a essa configuração para poder localizá-la;
- em [4], seleciona-se o script a ser executado. Se tiver sido seguido o procedimento [1-2], o script correto já terá sido selecionado;
- em [5], insere-se aqui os parâmetros a serem transmitidos ao script. Passa-se aqui a string [config1] para solicitar ao script que utilize a configuração nº 1;
- em [6], confirma-se a configuração de execução;

- em [1-2], solicita-se a exibição dos contextos de execução existentes;
- no [3], seleciona-se o contexto de execução existente e o duplica-se como [4];

- no [5], o nome atribuído à nova configuração. Será essa configuração que executará o script [main] [6], passando-lhe o parâmetro [config2] [7];
As configurações de execução estão disponíveis no canto superior direito da janela PyCharm:

Basta selecionar [2] ou [3] e, em seguida, clicar em [4] para executar o script [main] comum dos parâmetros [config1] ou [config2].
Com [config1], a execução de [main] produz os seguintes resultados:
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/troiscouches/v02/main/main.py config1
34
Process finished with exit code 0
Com [config2], a execução de [main] produz os seguintes resultados:
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/troiscouches/v02/main/main.py config2
-10
Process finished with exit code 0
Solicita-se ao leitor que verifique esses resultados.













