14. Arquitetura em camadas e programação por interfaces
14.1. Introdução
Propomos escrever uma aplicação que permita a visualização das notas dos alunos de uma escola secundária. Esta aplicação pode ter uma arquitetura multicamadas:

- a camada [ui] (Interface do Utilizador) é a camada em contacto com o utilizador da aplicação;
- A camada [métier] implementa as regras de gestão da aplicação, tais como o cálculo de um salário ou de uma fatura. Esta camada utiliza dados provenientes do utilizador através da camada [présentation] e da camada SGBD através da camada [dao];
- a camada [dao] (Data Access Objects) gere o acesso aos dados da camada SGBD (Sistema de Gestão de Bases de Dados).
Esta é 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 denominado [main], acima referido, organiza a instanciação das camadas;
- as camadas [ui, métier, dao] já não comunicam necessariamente entre si. Se for necessário que o façam, o script [main] fornece-lhes as referências das camadas de que necessitam;
O código está aqui organizado em centros de competências com um «maestro»:
- o «maestro» é o script principal [main];
- as camadas [ui], [dao] e [métier] são os centros de competências;
Poderíamos chamar a esta organização uma organização orquestral.
14.2. Exemplo 1
Vamos ilustrar a arquitetura em camadas com uma aplicação de consola simples:
- não haverá base de dados;
- a camada [dao] irá gerir as entidades Elève, Classe, Matière e Note, permitindo a gestão das notas dos alunos;
- a camada [métier] permitirá calcular indicadores com base nas notas de um aluno específico;
- a camada [ui] será uma aplicação de consola que apresentará os resultados dos alunos;
O projeto PyCharm da aplicação é o seguinte:
![]() |
Nota: as pastas a azul fazem parte do [Sources Root] do projeto PyCharm.
14.2.1. As entidades da aplicação
Chamaremos de entidades as classes cuja única função é encapsular dados. Para tal, poderíamos utilizar dicionários. O interesse da classe reside em permitir testar a validade dos dados armazenados no objeto e em fornecer um método que devolva a identidade do objeto sob a forma de uma cadeia de caracteres.
![]() |
14.2.1.1. A entidade [Classe]
A entidade [Classe] (Classe.py) representa uma turma do colégio:
# 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 cadeia 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 cadeia 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 pelo seu n.º [id], pelo seu nome [nom] e pelo 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
# apelido: apelido 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 apelido deve ser uma cadeia 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 próprio deve ser uma cadeia 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:
# é esperado 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], o seu apelido [nom], o seu nome próprio [prénom] e a 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) a quem a nota diz respeito
# disciplina: disciplina (do tipo Disciplina) a que a nota se refere
# o objeto «Nota» corresponde, portanto, à nota de um aluno numa 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:
# é esperado 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 pelo seu n.º [id], pelo valor da nota [valeur], por uma referência [élève] ao aluno que obteve essa nota e por uma referência à disciplina [matière] objeto da nota;
- linhas 22-75: getters/setters dos atributos da classe;
14.2.2. Configuração da aplicação
![]() |
O ficheiro [config.py] configura o ambiente do script principal [main] (1), bem como o dos testes (2). Todos estes scripts têm uma instrução [import config] no início do código. Recorde-se que a pasta que contém o script alvo do comando [python script] faz automaticamente parte do Python Path.Si; portanto, como o [config] se encontra na mesma pasta que os scripts com a instrução [import config], será encontrado. Os ficheiros [1] e [2] são, neste caso, idênticos. No entanto, isso pode não ser o caso.
O ficheiro [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=[
# as pastas locais que contêm 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)
# faz-se 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 da aplicação;
- a pasta [f"{script_dir}/../services"] dá acesso às classes que implementam as interfaces;
14.2.3. Testes das entidades
![]() |
Vamos aqui escrever testes executados por uma ferramenta chamada [unittest]. O PyCharm inclui vários frameworks de teste. A escolha de um deles é 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
# configuramos a aplicação
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
# criação de uma nota a partir de uma cadeia de caracteres jSON
note = Note().fromjson(
'{"id": 8, "valor": 12, "aluno": {"id": 42, "apelido": "apelido4", "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
# cálculo 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 irá fornecer os diferentes métodos de teste;
- linhas 3-6: configura-se a aplicação para que as classes necessárias aos testes sejam encontradas;
- linha 9: uma classe de teste [unittest] deve herdar da 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, pretende-se 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 é efetivamente 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] é efetivamente do tipo [Elève];
- linha 23: verifica-se se [note.élève.classe] é efetivamente do tipo [Classe];
- linha 24: verifica-se se [note.matière] é efetivamente do tipo [Matière];
- linhas 33-42: faz-se o mesmo com o método [BaseEntity.fromdict];
Existem várias formas de executar os testes:
![]() |
- em [1-2], executa-se [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 deve-se à organização do código do [TestBaseEntity]:
import unittest
# configuração da aplicação
import config
config = config.configure()
class TestBaseEntity(unittest.TestCase):
O que está a causar problemas ao 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 forma:
import unittest
class TestBaseEntity(unittest.TestCase):
def setUp(self):
# configuração da aplicação
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]. Esta função tem um papel específico: é executada antes de cada função de teste (test_note1, test_note2);
Feito isto, 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 forma:
def test_note1(self):
# erro intencional — 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 saber a causa do erro clicando no teste falhado [2]:
![]() |
- em [7-8], a causa do erro;
Outra forma de executar uma classe de testes é fazê-lo num 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 (eliminou-se o erro 1==2);
Por fim, uma terceira forma de executar a classe de testes [TestBaseEntity], também num terminal, é a seguinte. Concluímos a classe de testes com as seguintes linhas 6-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 atribuído ao script que é executado. Quando o script é aquele iniciado pelo comando [python script.py], a variável [__name__] assume o valor [__main__] (2 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 através do framework [UnitTest]. Eis 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):
# configura-se a aplicação
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 próprio 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 utilizar o tipo [MyException] para ser bem-sucedido
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 se atribui o valor «x», na linha 16, como identificador do aluno, espera-se que ocorra uma exceção. Dever-se-ia, portanto, passar para as linhas 20-22;
- linha 21: exibição da mensagem de erro;
- linha 22: recupera-se 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 ocorreu efetivamente um erro;
- que o código de erro é 1;
- este processo é repetido com as funções das linhas 24-213;
- linhas 215-222: verifica-se se uma ação lança uma exceção de um determinado tipo;
- linha 220: indica-se que o teste foi bem-sucedido se for lançada 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
Neste caso, 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 disponibiliza a interface:
![]() |
- O código chamador [1] não conhece a implementação do código chamado [3]. Só sabe como o chamar. É a interface [2] que lhe indica isso. Esta define um certo número de métodos/funções a utilizar para interagir com o código chamado. Esta interface é também designada por API (Interface de Programação de Aplicações);
A camada [dao] disponibilizará a seguinte interface:
- [get_classes] apresenta a lista das turmas do ensino básico;
- [get_matières] apresenta a lista das disciplinas lecionadas no colégio;
- [get_élèves] apresenta a lista de alunos do ensino básico;
- [get_notes] apresenta a lista das notas de todos os alunos;
- [get_notes_for_élève_by_id] apresenta as notas de um aluno específico;
- [get_élève_by_id] apresenta um aluno identificado pelo seu número;
O código chamador utilizará apenas estes métodos. Não precisa de saber como são implementados. Os dados podem, assim, provir de diferentes fontes (armazenados em memória, de uma base de dados, de ficheiros de texto…) sem que isso tenha impacto no código chamador. A isto chama-se programação por interfaces.
O Python 3 possui um conceito semelhante 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
# procurar 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 chama-se [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 num método abstrato: o 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. Apenas podem ser instanciadas as classes derivadas de [InterfaceDao] que tenham implementado todos os métodos de [InterfaceDao]. Assim, se criarmos duas classes, [Dao1] e [Dao2], derivadas da classe [InterfaceDao], ambas implementarão os métodos abstratos de [InterfaceDao]. Poder-se-ia dizer, assim, que elas implementam a interface [InterfaceDao];
- as linguagens que implementam tanto interfaces como classes abstratas atribuem à interface um papel diferente do da classe abstrata. Uma interface não tem atributos e não pode ser instanciada. Uma classe pode implementar uma interface definindo todos os seus métodos;
14.2.4.2. Implementação [Dao]
A classe [Dao] (dao.py) implementa a interface [InterfaceDao] da seguinte forma:
# 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
# construem-se listas fixas
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 tem parâmetros. Ele define de forma estática 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 vermos a mensagem de erro emitida pelo Python;
Um programa de teste poderia ser o seguinte: [tests-dao.py]:
# configura-se a aplicação
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)
Nota: o script [tests-dao.py] não é um teste [unittest], uma vez que 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 quaisquer pressupostos 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 instância da classe [Dao] (linha 3 acima). O interpretador Python 3 indica que não consegue instanciar a classe, uma vez que 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 sugere-nos que definamos os métodos da mesma:
![]() |
- em [1], clique com o botão direito do rato no código;
- em [2-3], selecione [Generate / Implement Methods] para implementar os métodos em falta da classe [Dao];
- em [4], selecione os métodos a implementar, neste caso todos;
Feito isto, a classe [Dao] é completada pela PyCharm da seguinte forma:
# -----------
# 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 forma:
# -----------
# 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:
# procurar o 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()))
# apresenta o 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-19 não apresentam dificuldades;
- linhas 29-36: o método que devolve 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 filtrar, neste caso a lista de alunos;
- linha 31: o critério de filtragem da lista é implementado com a ajuda de uma função [f(e :Elève)->bool]. Esta é aplicada a cada um dos elementos da lista a filtrar. Se o elemento satisfizer o critério de filtragem, é mantido na lista filtrada; caso contrário, é excluído. Aqui, pode-se:
- indicar o nome da função f e implementá-la noutro 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()], em que [e] representa um elemento da lista filtrada, ou seja, um aluno. Foi isso que se fez 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.º [id] não for igual ao procurado. Uma função deste tipo pode ser substituída por uma função denominada lambda: [lambda e: e.id == élève_id]:
- e: representa o parâmetro da função f, neste caso, um aluno. Pode-se utilizar o nome que se quiser;
- e.id==élève_id é o critério de filtragem: um aluno [e] só é selecionado se o seu n.º [id] corresponder ao que está a ser procurado;
- linha 31: a função [filter] devolve a lista filtrada num 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çada uma exceção;
- linha 36: se chegarmos aqui, é porque não houve nenhuma exceção. Sabemos, então, que recuperámos uma lista com um único elemento (não há dois alunos com o mesmo n.º [id]). Devolvemos, portanto, o primeiro elemento da lista;
- linhas 21-27: o método [get_notes_for_élève_by_id] deve devolver as notas do aluno cujo n.º [id] lhe é passado;
- linhas 22-23: começa-se por procurar o aluno com o n.º [élève_id] utilizando o método [get_élève_by_id] que acabámos de comentar. Pode ocorrer uma exceção se o aluno procurado não existir. Como não existe um try/catch em torno da instrução da linha 23, a exceção será propagada para o código chamador. É isso que se pretende;
- linhas 24-25: assim que o aluno for recuperado, recuperam-se todas as suas notas. Faz-se isso novamente com um filtro:
- o filtro é [filter(critère, self_getnotes()]. A lista a filtrar é, portanto, a lista de todas as notas de todos os alunos do colégio;
- o critério de filtragem é expresso através de uma função [lambda]: lambda n: n.élève.id == élève_id. O parâmetro n é um elemento da lista a filtrar, ou seja, uma nota. O tipo [Note] possui uma propriedade [élève] que representa o aluno a quem pertence a nota. É necessário, portanto, 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 a aplicação
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 das 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}")
Obtenemos então 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 apresentar uma nota (para os outros objetos, o procedimento é semelhante), temos também:
- o aluno a quem a nota pertence;
- a disciplina a que a nota se refere;
É a função [BaseEntity.asdict] que produz este resultado (ver parágrafo «ligação»).
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] irá 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ócio
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] apresenta as notas do aluno n.º idElève, bem como informações sobre as mesmas: média ponderada, nota mais baixa, nota mais alta. Estas informações estão encapsuladas num 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: as suas notas
# moyennePondérée: a sua média ponderada pelos coeficientes das disciplinas
# min: a sua nota mínima
# máx.: a 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;
- as suas notas [notes];
- a sua média ponderada [moyenne_pondérée];
- a sua nota mínima [min];
- a sua nota máxima [max];
- não se definem getters/setters para estes atributos. Parte-se do princípio de que é a camada [métier] que cria objetos deste tipo e que esta não cria objetos inválidos;
- linhas 23-33: a função [__str__] devolve uma cadeia de caracteres que reflete 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):
# guardamos 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 para o aluno com o n.º idEleve
# id_élève: n.º do aluno
# recuperam-se as 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"]
# paramos 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
# procura do mínimo
if valeur < min:
min = valeur
# procura do máximo
if valeur > max:
max = valeur
# cálculo dos indicadores em falta
moyenne_pondérée = float(somme_pondérée) / somme_coeff
# o resultado é apresentado na forma 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]. É habitual 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, note-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]. Neste caso, isso não tem importância, uma vez que o Python não vai ter em conta este tipo, mas é uma boa prática trabalhar com interfaces em vez de com implementações específicas. O código torna-se assim mais facilmente modificável;
- 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.º [idElève] do aluno cujas estatísticas se pretendem obter;
- linha 24: solicita-se à camada [dao] as notas do aluno. Este pedido resulta numa exceção se o aluno não existir. Esta exceção não é tratada (ausência de try/catch) e, por isso, é reenviada para o código chamador;
- linha 25: chega-se 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 (o seu nome, a 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 calcular;
- linha 31: devolve-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 a sua compreensão;
- linhas 56-60: devolve-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 da aplicação
import config
config.configure()
def test_statsForEleve11(self):
# importações
from Dao import Dao
from Métier import Métier
# testam-se os indicadores do aluno 11
dao = Dao()
stats_for_élève = Métier(dao).get_stats_for_élève(11)
# visualizaçã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] é aqui utilizada para configurar o Python Path do teste;
- linha 16: instanciamos a camada [dao];
- linha 17: instanciamos a camada [métier] e utilizamos o seu método [get_stats_for_élève] para calcular as estatísticas do aluno n.º 11;
- linha 19: apresenta-se o resultado [StatsForElève] obtido. Como [StatsForElève] deriva de [BaseEntity], é a cadeia jSON de [StatsForElève] que é aqui apresentada;
- 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, na maioria das vezes, estes têm 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 fralda [ui]

- em [1], a interface da camada [ui];
- em [2], a implementação dessa interface;
- em [3], o script principal da aplicação;
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 das 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 memorizados
self.métier = métier
# -----------
# interface
# -----------
def run(self):
# diálogo com o utilizador
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
# dados corretos?
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]. Note-se que foi atribuído o tipo [InterfaceMétier] a este parâmetro para recordar que se está a trabalhar com interfaces e não com implementações específicas;
- linha 24: implementação do método [run] da interface;
- linha 27: um ciclo que termina quando a condição da linha 31 é verificada;
- linha 29: introdução de um valor digitado no teclado. A função [input] recebe um parâmetro opcional: a mensagem a apresentar no ecrã para solicitar a introdução de dados. Esta é sempre recuperada como uma cadeia de caracteres. A função [strip] remove os «espaços» que a precedem ou seguem;
- linhas 34-39: verifica-se se a entrada, um número de aluno, é válida. Tem de ser um número inteiro >=1. Recorde-se que a entrada foi feita como uma cadeia de caracteres;
- linha 36: tenta-se converter o valor introduzido num número inteiro na base 10. A função [int] lança uma exceção se tal 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 é, de facto, >=1;
- linhas 38-39: trata-se da exceção. Se tiver ocorrido uma exceção, a variável [ok] da linha 34 permaneceu em [False];
- linhas 41-43: se a introdução de dados tiver sido incorreta, exibe-se uma mensagem de erro e volta-se ao início do ciclo (linha 43);
- linhas 45-48: calculam-se as estatísticas do aluno cujo número foi introduzido;
- linha 46: utiliza-se o método [get_stats_for_élève] da camada [métier]. Este método lança uma exceção se o aluno não existir. A exceção é tratada nas linhas 47-48. Sabe-se 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):
# configurar a aplicação
import config
config = config.configure()
# o syspath está configurado — já é possível efetuar 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:
# é apresentado o erro
print(f"L'erreur suivante s'est produite : {ex}")
finally:
pass
- linhas 1-4: configura-se o Python Path da aplicação;
- linhas 6-9: importam-se as classes e interfaces necessárias;
- linha 14: instanciamento da camada [dao];
- linha 16: instanciação da camada [métier];
- linha 18: instanciação da camada [ui];
- linha 20: inicia-se o diálogo com o utilizador;
- linhas 13-20: normalmente, nenhuma exceção é lançada nestas linhas. As exceções que provêm das camadas [dao] e [métier] são interceptadas pela camada [Console]. A gestão 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úvida, pode-se adicionar código para interceptar qualquer tipo de exceção que possa ser lançada pelo código executado. É isso que se faz aqui, nas linhas 21-23. Interceptam-se todas as exceções derivadas de [BaseException], ou seja, todas as exceções;
- linhas 24-25: a cláusula [finally] não tem qualquer efeito aqui. Está presente apenas para permitir comentar as linhas 21-23. Com efeito, no modo de depuração, não é aconselhável interceptar as exceções. Neste caso, é o interpretador Python que as intercepta e, ao fazê-lo, indica o número da linha onde 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 sua ausência, o Python declara um erro;
Eis 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 visa demonstrar a vantagem da programação por interfaces. Esta facilita a manutenção e o teste das aplicações. Utilizaremos novamente uma arquitetura de três camadas:

Cada camada será implementada de duas formas diferentes. Pretendemos demonstrar que é possível alterar facilmente a implementação de uma camada com um impacto mínimo nas restantes.
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 forma:
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 forma:
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ócio
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 forma:
# 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 esta interface nas linhas 19-22. Na verdade, verifica-se que não implementou o método [do_something_in_métier_layer], que declarou como 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 reporta um erro na execução;
É a primeira vez que se utiliza a herança múltipla (herdar de várias classes). A classe [AbstractBaseMétier] herda, simultaneamente, 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 partilham propriedades, é interessante colocá-las numa classe pai, a fim de evitar a sua duplicação. É o caso, aqui, da propriedade [dao]. A classe pai é, geralmente, sempre abstrata, porque não sabe implementar todos os métodos da interface.
A classe [MétierImpl1] implementa a interface [InterfaceMétier] da seguinte forma:
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]. Por conseguinte, 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 forma 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 forma:
# importações
from abc import ABC, abstractmethod
from InterfaceMétier import InterfaceMétier
from InterfaceUi import InterfaceUi
class AbstractBaseUi(InterfaceUi, ABC):
# propriedades
# o domínio de 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). Terá de 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, por conseguinte, herda a 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, por conseguinte, herda a sua propriedade [métier]. Esta é utilizada na linha 9;
14.4.4. Os ficheiros de configuração

- os ficheiros [config1, config2] configuram a aplicação de duas formas diferentes;
- o ficheiro [main] é o script principal da aplicação;
O ficheiro [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",
]
# configuramos o syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# etapa 2 ------
# configuração das camadas da aplicação
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 utilizador
ui = UiImpl1()
ui.métier = métier
# colocamos as instâncias das camadas na configuração
# aqui só é necessária a camada ui
config = {"ui": ui}
# fazemos a configuração
return config
- linhas 2-16: configuração do Python Path da aplicação;
- linhas 18-31: instanciação das camadas [dao, métier, ui]. Para implementar as respetivas interfaces, opta-se sempre pela primeira implementação criada;
- linhas 33-35: inserem-se as referências das camadas na configuração. Aqui, o script principal necessita apenas da camada [ui];
O ficheiro [config2] é semelhante e implementa cada interface com a segunda implementação disponível:
def configure():
# passo 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",
]
# configuramos o syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# etapa 2 ------
# configuração das camadas da aplicação
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 utilizador
ui = UiImpl2()
ui.métier = métier
# colocamos as instâncias das camadas na configuração
# aqui só é necessária a camada ui
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 da aplicação
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 utilizar a configuração n.º 1;
- [config2] para utilizar a configuração n.º 2;
O Python guarda os parâmetros numa lista [sys.argv]:
- sys.argv[0] é o nome do script, neste caso [main]. Este 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 existe efetivamente um parâmetro e se o seu valor é [config1] ou [config2]. Se não for esse o caso, é exibida uma mensagem de erro (linha 10) e o programa é encerrado (linha 11);
Assim que a configuração pretendida for conhecida, é necessário executá-la. Por exemplo, se tiver sido escolhida a configuração 1, é necessário executar o código:
O problema aqui é que a configuração a utilizar está numa variável, a variável [len[liste1]]. Para importar um módulo cujo nome está numa variável, temos de utilizar o pacote [importlib] (linha 2).
- linha 14: importamos o módulo cujo nome está em [sys.argv[1];
- linha 15: feito isto, executamos a função [configure] desse módulo. Recuperamos um dicionário [config], que corresponde à configuração da aplicação;
- linha 18: sabemos que existe uma referência da camada [ui] em config[‘ui’]. Utilizamo-la para chamar o método [do_something_in_ui_layer]. Sabemos que este método irá chamar um método da camada [métier], que, por sua vez, irá 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:
# funções de negócio
métier = MétierImpl1()
métier.dao = dao
# interface do utilizador
ui = UiImpl1()
ui.métier = métier
- linha 6: a propriedade [métier] de [UIImpl1] é uma referência à classe [MétierImpl1] (linha 2). Assim, 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] irá, 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ócio
métier = MétierImpl1()
métier.dao = dao
- linha 5: a propriedade [MétierImpl1.dao] é do tipo [DaoImpl1] (linha 2);
O que se pretende demonstrar aqui é que o script [main] não precisa de se preocupar com as camadas [métier] e [dao]. Tem apenas de se preocupar com a camada [ui], uma vez que as ligações entre esta camada e as outras foram estabelecidas por configuração.

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

- no [1-2], cria-se o que se denomina uma configuração de execução;
- no [3], atribui-se um nome a esta configuração para poder localizá-la;
- em [4], seleciona-se o script a executar. Se tiver seguido o procedimento [1-2], o script correto já terá sido selecionado;
- Em [5], introduzem-se aqui os parâmetros a transmitir ao script. Aqui, passa-se a cadeia [config1] para solicitar ao script que utilize a configuração n.º 1;
- em [6], valida-se a configuração de execução;

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

- no [5], o nome atribuído à nova configuração. Será esta 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 o [config1], a execução do [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 o [config2], a execução do [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
Convidamos o leitor a verificar estes resultados.













