14. Багатошарова архітектура та програмування за допомогою інтерфейсів
14.1. Вступ
Ми пропонуємо написати додаток, що дозволяє відображати оцінки учнів середньої школи. Цей додаток може мати багаторівневу архітектуру:

- шар [ui] (User Interface) — це шар, що взаємодіє з користувачем програми;
- Рівень [métier] реалізує правила управління додатком, такі як розрахунок заробітної плати або рахунку-фактури. Цей рівень використовує дані, отримані від користувача через рівень [présentation], а також дані з SGBD через рівень [dao];
- шар [dao] (об’єкти доступу до даних) керує доступом до даних з SGBD (система управління базами даних).
Саме цю архітектуру використовували в |курсі з Python 2|. Також можна запропонувати такий варіант:

Відмінності від попередньої шарової структури такі:
- головний скрипт, названий вище [main], організовує створення екземплярів шарів;
- шари [ui, métier, dao] більше не обов’язково взаємодіють між собою. Якщо це необхідно, скрипт [main] надає їм посилання на шари, які їм потрібні;
Код тут організовано за напрямками компетенції з «диригентом»:
- координатором є головний скрипт [main];
- шари [ui], [dao] та [métier] є центрами компетенції;
Таку організацію можна назвати оркестровою.
14.2. Приклад 1
Ми проілюструємо багаторівневу архітектуру на прикладі простого консольного додатка:
- бази даних не буде;
- шар [dao] керуватиме сутностями Elève, Classe, Matière, Note, що дозволяють керувати оцінками учнів;
- шар [métier] дозволить розраховувати показники за оцінками конкретного учня;
- шар [ui] буде консольним додатком, що відображатиме результати учнів;
Проект PyCharm додатка виглядає наступним чином:
![]() |
Примітка: папки, виділені синім кольором, входять до складу [Sources Root] проекту PyCharm.
14.2.1. Об’єкти програми
Ентітетами називатимемо класи, єдиною роллю яких є інкапсуляція даних. Для цього можна було б використовувати словники. Перевага класу полягає в тому, що він дозволяє перевіряти правильність даних, що зберігаються в об’єкті, та надає метод, який повертає ідентифікатор об’єкта у вигляді символьного рядка.
![]() |
14.2.1.1. Ентітет [Classe]
Ентітет [Classe] (Classe.py) представляє клас коледжу:
# імпортує
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils
class Classe(BaseEntity):
# атрибути, виключені зі стану класу
excluded_keys = []
# властивості класу
@staticmethod
def get_allowed_keys() -> list:
# id: ідентифікатор класу
# назва: назва класу
return BaseEntity.get_allowed_keys() + ["nom"]
# геттер
@property
def nom(self: object) -> str:
return self.__nom
# сетери
@nom.setter
def nom(self: object, nom: str):
# ім'я має бути непорожнім рядком
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")
Примітки
- рядок 7: суть [Classe] походить від суті [BaseEntity], розглянутої в розділі |Клас BaseEntity|;
- рядки 11–16: клас визначається номером id та nom (рядок 16). Властивість [id] надається класом [BaseEntity], а ім’я — класом [Classe];
- рядки 18–30: методи getter/setter атрибута [nom];
14.2.1.2. Об’єкт [Matière]
Клас [Matière] (matière.py) має такий вигляд:
# імпорти
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils
class Matière(BaseEntity):
# атрибути, виключені зі стану класу
excluded_keys = []
# властивості класу
@staticmethod
def get_allowed_keys() -> list:
# id: ідентифікатор предмета
# назва: назва предмета
# коефіцієнт: коефіцієнт дисципліни
return BaseEntity.get_allowed_keys() + ["nom", "coefficient"]
# геттер
@property
def nom(self: object) -> str:
return self.__nom
@property
def coefficient(self: object) -> float:
return self.__coefficient
# сетери
@nom.setter
def nom(self: object, nom: str):
# назва має бути непорожнім рядком
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):
# коефіцієнт повинен бути дійсним числом >=0
erreur = False
if isinstance(coefficient, (int, float)):
if coefficient >= 0:
self.__coefficient = coefficient
else:
erreur = True
else:
erreur = True
# помилка?
if erreur:
raise MyException(22, f"Le coefficient de la matière {self.nom} doit être un réel >=0")
Примітки
- рядок 7: клас [Classe] походить від класу [BaseEntity];
- рядки 11–17: предмет визначається за його номером [id], назвою [nom] та коефіцієнтом [coefficient];
- рядки 19–50: методи getter/setter атрибутів класу;
14.2.1.3. Об’єкт [Elève]
Клас [Elève] (élève.py) має такий вигляд:
# імпорти
from BaseEntity import BaseEntity
from Classe import Classe
from MyException import MyException
from Utils import Utils
class Elève(BaseEntity):
# атрибути, виключені зі стану класу
excluded_keys = []
# властивості класу
@staticmethod
def get_allowed_keys() -> list:
# id: ідентифікатор учня
# прізвище: прізвище учня
# ім'я: ім'я учня
# клас: клас учня
return BaseEntity.get_allowed_keys() + ["nom", "prénom", "classe"]
# гетери
@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
# сетери
@nom.setter
def nom(self: object, nom: str) -> str:
# прізвище має бути непорожнім рядком
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:
# ім'я має бути непорожнім рядком
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:
# очікується тип «Клас»
if isinstance(value, Classe):
self.__classe = value
# або тип dict
elif isinstance(value,dict):
self.__classe=Classe().fromdict(value)
# або типу 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}")
Примітки
- рядок 9: клас [Elève] походить від класу [BaseEntity];
- рядки 13–20: учень характеризується своїм номером [id], прізвищем [nom], ім’ям [prénom] та класом [classe]. Останній параметр є посиланням на об’єкт [Classe];
- рядки 22–65: методи getter/setter атрибутів класу;
14.2.1.4. Об’єкт [Note]
Клас [Note] (note.py) має такий вигляд:
# імпорти
from BaseEntity import BaseEntity
from Elève import Elève
from Matière import Matière
from MyException import MyException
class Note(BaseEntity):
# атрибути, виключені зі стану класу
excluded_keys = []
# властивості класу
@staticmethod
def get_allowed_keys() -> list:
# id: ідентифікатор оцінки
# значення: сама оцінка
# учень: учень (типу «Учень»), якого стосується оцінка
# предмет: предмет (типу «Предмет»), до якого відноситься оцінка
# отже, об’єкт «Оцінка» — це оцінка учня з певного предмета
return BaseEntity.get_allowed_keys() + ["valeur", "élève", "matière"]
# гетери
@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
# геттери
@valeur.setter
def valeur(self: object, valeur: float):
# оцінка має бути дійсним числом у діапазоні від 0 до 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:
# очікується тип «Учень»
if isinstance(value, Elève):
self.__élève = value
# або тип dict
elif isinstance(value, dict):
self.__élève = Elève().fromdict(value)
# або типу 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:
# очікується тип «Предмет»
if isinstance(value, Matière):
self.__matière = value
# або тип dict
elif isinstance(value, dict):
self.__matière = Matière().fromdict(value)
# або типу 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}")
Примітки
- рядок 8: клас [Note] походить від класу [BaseEntity];
- рядки 12–20: об’єкт [Note] характеризується своїм номером [id], значенням оцінки [valeur], посиланням [élève] на учня, який отримав цю оцінку, та посиланням на предмет [matière], що є предметом оцінки;
- рядки 22–75: методи getter/setter атрибутів класу;
14.2.2. Налаштування додатка
![]() |
Файл [config.py] налаштовує середовище основного скрипта [main] (1), а також середовище тестів (2). Усі ці скрипти містять інструкцію [import config] на початку коду. Нагадаємо, що папка, яка містить скрипт, що є об’єктом команди [python script], автоматично стає частиною Python-папки Path.Si, отже, оскільки [config] знаходиться в тій самій папці, що й скрипти з інструкцією [import config], його буде знайдено. Файли [1] та [2] тут є ідентичними. Однак це не завжди так.
Файл [config.sys] має такий вигляд:
def configure():
import os
# абсолютний шлях до папки, в якій знаходиться цей скрипт
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"
# абсолютні залежності
absolute_dependencies=[
# локальні папки, що містять класи та інтерфейси
f"{root_dir}/02/entities",
f"{script_dir}/../entities",
f"{script_dir}/../interfaces",
f"{script_dir}/../services",
]
# оновлення syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# виконується конфігурація
return {}
- рядки 11–14: папки, які мають входити до Python Path (sys.path);
- папка [f"{root_dir}/02/entities"] надає доступ до класів [BaseEntity] та [MyException];
- папка [f"{script_dir}/../entities"] надає доступ до класів [Elève], [Classe], [Matière], [Note];
- папка [f"{script_dir}/../interfaces",] надає доступ до інтерфейсів додатка;
- папка [f"{script_dir}/../services"] надає доступ до класів, що реалізують інтерфейси;
14.2.3. Тестування сутностей
![]() |
Тут ми будемо писати тести, які виконуються інструментом під назвою [unittest]. PyCharm постачається з кількома тестовими фреймворками. Вибір одного з них здійснюється у налаштуваннях PyCharm:

- У [4] доступні кілька фреймворків для тестування:

14.2.3.1. Клас тестів [TestBaseEntity]
Скрипт тесту [TestBaseEntity] матиме такий вигляд:
import unittest
# налаштовуємо додаток
import config
config = config.configure()
class TestBaseEntity (unittest.TestCase):
def test_note1(self):
# імпорт
from Note import Note
from Elève import Elève
from Classe import Classe
from Matière import Matière
# створення оцінки на основі рядка jSON
note = Note().fromjson(
'{"id": 8, "значення": 12, "учень": {"id": 42, "прізвище": "прізвище4", "ім'я": "ім'я4", "клас": {"id": 2, "назва": "клас2"}}, "предмет": {"id": 2, "назва": "предмет2", "коефіцієнт": 2}}')
# перевірки
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):
# імпорт
from Note import Note
from Elève import Elève
from Classe import Classe
from Matière import Matière
# формування оцінки на основі словника
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}})
# перевірки
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()
Примітки
- рядок 1: імпортується модуль [unittest], який надасть різні методи тестування;
- рядки 3–6: налаштовуємо додаток так, щоб він міг знайти класи, необхідні для тестування;
- рядок 9: тестовий клас [unittest] повинен успадковувати клас [unittest.TestCase];
- рядки 11, 27: імена тестових функцій повинні починатися з [test], інакше вони не будуть розпізнані;
- рядки 13–16: імпортуються необхідні класи;
- у цьому тестовому класі ми хочемо перевірити поведінку методів [BaseEntity.fromdict] (рядок 34) та [BaseEntity.fromjson] (рядок 18). Клас [Note] має властивості, які є посиланнями на інші класи. Ми хочемо перевірити, чи обидва попередні методи створюють коректні об’єкти [Note];
- рядок 18: створюємо об’єкт [Note] на основі об’єкта jSON;
- рядок 21: перевіряємо, чи створений об’єкт дійсно має тип [Note]. Метод [assertIsInstance] є методом класу [unittest.TestCase], батьківського класу класу [TestBaseEntity];
- рядок 22: перевіряється, чи [note.élève] дійсно має тип [Elève];
- рядок 23: перевіряється, чи [note.élève.classe] дійсно належить до типу [Classe];
- рядок 24: перевіряємо, чи [note.matière] дійсно належить до типу [Matière];
- рядки 33–42: те саме робимо з методом [BaseEntity.fromdict];
Існує кілька способів виконання тестів:
![]() |
- у [1-2] виконується [TestBaseEntity] із використанням фреймворку [UnitTest];
- у [3-5] тести завершуються з помилкою. [UnitTests] повідомляє, що не знайдено жодного тесту для виконання;
Невдача тестів пов’язана з організацією коду [TestBaseEntity]:
import unittest
# налаштування програми
import config
config = config.configure()
class TestBaseEntity(unittest.TestCase):
Що заважає фреймворку [UnitTest], так це наявність виконуваного коду (рядки 3–6) перед визначенням тестового класу (рядок 9).
Тож переорганізуємо код наступним чином:
import unittest
class TestBaseEntity(unittest.TestCase):
def setUp(self):
# налаштування програми
import config
config.configure()
def test_note1(self):
…
def test_note2(self):
…
if __name__ == '__main__':
unittest.main()
- рядки 6–10: визначається функція [setUp]. Ця функція виконує особливу роль: вона виконується перед кожною тестовою функцією (test_note1, test_note2);
Після цього виконання класу [TestBaseEntity] дає такі результати:
![]() |
Цього разу обидва методи тестування були виконані, і тести пройшли успішно.
Давайте подивимося, що відбувається, коли тест завершується невдало. Змінимо код класу [test_note1] наступним чином:
def test_note1(self):
# навмисна помилка — перевіряємо, чи 1==2
self.assertEqual(1,2)
# імпорт
from Note import Note
…
- рядок 2: перевіряється, чи 1==2;
Результати виконання будуть такими:
![]() |
Дізнатися причину помилки можна, натиснувши на тест, що завершився невдало, [2]:
![]() |
- у [7-8] вказано причину помилки;
Інший спосіб запустити набір тестів — виконати його в терміналі:
![]() |
(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
У рядку 6 вказано, що обидва тести пройшли успішно (помилка 1==2 була усунена);
Нарешті, третій спосіб виконання класу тестів [TestBaseEntity], також у терміналі, є таким. Клас тестів завершується наступними рядками 6–7;
…
self.assertIsInstance(note.élève.classe, Classe)
self.assertIsInstance(note.matière, Matière)
if __name__ == '__main__':
unittest.main()
- рядок 6: змінна [__name__] — це ім’я скрипта, що виконується. Коли скрипт запускається командою [python script.py], значення змінної [__name__] дорівнює [__main__] (2 символи з підкресленням перед та після ідентифікатора). Отже, рядок 7 виконується лише тоді, коли скрипт [TestBaseEntity] запускається командою [python TestBaseEntity.py]. Інструкція [unittest.main()] запускає виконання скрипта за допомогою фреймворку [UnitTest]. Ось приклад:
(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. Клас тестів [TestEntités]
Клас тестів [TestEntités] має такий вигляд:
import unittest
class TestEntités(unittest.TestCase):
def setUp(self):
# налагодження програми
import config
config.configure()
def test_code1a(self):
# імпорт
from Elève import Elève
from MyException import MyException
# код помилки
code = None
try:
# недійсний ідентифікатор
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
# перевірка
self.assertEqual(code, 1)
def test_code41(self):
# імпорт
from Elève import Elève
from MyException import MyException
# код помилки
code = None
try:
# неправильне ім'я
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
# перевірка
self.assertEqual(code, 41)
def test_code42(self):
# імпорт
from Elève import Elève
from MyException import MyException
# код помилки
code = None
try:
# неправильне ім'я
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
# перевірка
self.assertEqual(code, 42)
def test_code43(self):
# імпорт
from Elève import Elève
from MyException import MyException
# код помилки
code = None
try:
# неправильний клас
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
# перевірка
self.assertEqual(code, 43)
def test_code1b(self):
# імпорт
from Classe import Classe
from MyException import MyException
# код помилки
code = None
try:
# неправильний ідентифікатор
Classe().fromdict({"id": "x", "nom": "y"})
except MyException as ex:
print(f"\ncode erreur={ex.code}, message={ex}")
code = ex.code
# перевірка
self.assertEqual(code, 1)
def test_code11(self):
# імпорт
from Classe import Classe
from MyException import MyException
# код помилки
code = None
try:
# неправильне ім'я
Classe().fromdict({"id": 1, "nom": ""})
except MyException as ex:
code = ex.code
# перевірка
self.assertEqual(code, 11)
def test_code1c(self):
# імпорт
from Matière import Matière
from MyException import MyException
# код помилки
code = None
try:
# неправильний ідентифікатор
Matière().fromdict({"id": "x", "nom": "y", "coefficient": "t"})
except MyException as ex:
print(f"\ncode erreur={ex.code}, message={ex}")
code = ex.code
# перевірка
self.assertEqual(code, 1)
def test_code21(self):
# імпорт
from Matière import Matière
from MyException import MyException
# код помилки
code = None
try:
# неправильне ім'я
Matière().fromdict({"id": "1", "nom": "", "coefficient": "t"})
except MyException as ex:
print(f"\ncode erreur={ex.code}, message={ex}")
code = ex.code
# перевірка
self.assertEqual(code, 21)
def test_code22(self):
# імпорт
from Matière import Matière
from MyException import MyException
# код помилки
code = None
try:
# неправильний коефіцієнт
Matière().fromdict({"id": 1, "nom": "y", "coefficient": "t"})
except MyException as ex:
print(f"\ncode erreur={ex.code}, message={ex}")
code = ex.code
# перевірка
self.assertEqual(code, 22)
def test_code1d(self):
# імпорт
from Note import Note
from MyException import MyException
# код помилки
code = None
try:
# неправильний ідентифікатор
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
# перевірка
self.assertEqual(code, 1)
def test_code31(self):
# імпорт
from Note import Note
from MyException import MyException
# код помилки
code = None
try:
# неправильне значення
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
# перевірка
self.assertEqual(code, 31)
def test_code32(self):
# імпорт
from Note import Note
from MyException import MyException
# код помилки
code = None
try:
# неправильний учень
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
# перевірка
self.assertEqual(code, 32)
def test_code33(self):
# імпорт
from Elève import Elève
from Note import Note
from Classe import Classe
from MyException import MyException
# код помилки
code = None
try:
# неправильний предмет
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
# перевірка
self.assertEqual(code, 33)
def test_exception(self):
# імпорт
from Elève import Elève
# для успішного виконання тесту потрібно запустити тип [MyException]
from MyException import MyException
with self.assertRaises(MyException):
# тест
Elève().fromdict({"id": "x", "nom": "y", "prénom": "z", "classe": "t"})
if __name__ == '__main__':
unittest.main()
- мета тестового скрипта — перевірити методи-сеттери класів: переконатися, що атрибутам різних об’єктів неможливо присвоїти некоректні значення;
- рядки 11–24: перевіряється, чи неможливо передати недійсний ідентифікатор учня. Оскільки в рядку 16 як ідентифікатор учня передається значення «x», очікується виникнення винятку. Отже, виконання має перейти до рядків 20–22;
- рядок 21: виведення повідомлення про помилку;
- рядок 22: отримуємо код помилки (див. параграф |Об’єкт MyException|);
- рядок 24: перевіряємо (assert), що код помилки дорівнює 1. Тут перевіряємо дві речі:
- що помилка дійсно сталася;
- що код помилки дорівнює 1;
- цей процес повторюється з функціями у рядках 24–213;
- рядки 215–222: перевіряється, чи дія викликає виняток певного типу;
- рядок 220: вказується, що тест пройшов успішно, якщо він викликає виняток типу [MyException];
Результати
Виконуємо тестовий скрипт:
![]() |
Отримано такі результати:
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
У цьому випадку всі тести пройшли успішно
14.2.4. Рівень [dao]

Рівень [dao] реалізує інтерфейс [InterfaceDao] [1]. Цей інтерфейс реалізовано класом [Dao] (2). Скрипт [tests_dao] (3) перевіряє методи шару [dao].
14.2.4.1. Інтерфейс [InterfaceDao]
Інтерфейс — це угода між кодом, що викликає, та кодом, що викликається. Саме код, що викликається, надає інтерфейс:
![]() |
- Викликаючий код [1] не знає реалізації викликаного коду [3]. Він знає лише, як його викликати. Про це йому повідомляє інтерфейс [2]. Цей інтерфейс визначає певну кількість методів/функцій, які слід використовувати для взаємодії з викликаним кодом. Цей інтерфейс також називають API (Application Programming Interface);
Рівень [dao] надаватиме такий інтерфейс:
- [get_classes] повертає список класів середньої школи;
- [get_matières] повертає список предметів, що викладаються в гімназії;
- [get_élèves] видає список учнів середньої школи;
- [get_notes] видає список оцінок усіх учнів;
- [get_notes_for_élève_by_id] виводить оцінки конкретного учня;
- [get_élève_by_id] повертає учня за його номером;
Код, що викликає ці методи, використовуватиме лише їх. Йому не потрібно знати, як вони реалізовані. Дані можуть надходити з різних джерел (вбудовані, з бази даних, з текстових файлів тощо) без впливу на код, що їх викликає. Це називається програмуванням за допомогою інтерфейсів.
У Python 3 є поняття, близьке до поняття інтерфейсу: абстрактний клас. Ми будемо ним користуватися. Ми об’єднаємо інтерфейси цього прикладу в папці [interfaces].
Ми визначаємо абстрактний клас [InterfaceDao] (InterfaceDao.py) для рівня [dao]:
# імпорт
from abc import ABC, abstractmethod
# інтерфейс Dao
from Elève import Elève
class InterfaceDao(ABC):
# список класів
@abstractmethod
def get_classes(self: object) -> list:
pass
# список учнів
@abstractmethod
def get_élèves(self: object) -> list:
pass
# список предметів
@abstractmethod
def get_matières(self: object) -> list:
pass
# список оцінок
@abstractmethod
def get_notes(self: object) -> list:
pass
# список оцінок учня
@abstractmethod
def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
pass
# пошук учня за його ідентифікатором
@abstractmethod
def get_élève_by_id(self, élève_id: int) -> Elève:
pass
Примітки:
- рядок 2: ABC = абстрактний базовий клас. З модуля [abc] імпортується клас ABC, а також декоратор [abstractmethod], який використовується у рядках 10, 15, 20, 25, 30 та 35;
- рядок 8: абстрактний клас називається [InterfaceDao] і походить від класу [ABC];
- методи абстрактного класу декоровані декоратором [@abstractmethod], який перетворює декорований таким чином метод на абстрактний: його код не визначено. Проте туди вставляється код: інструкція [pass], яка нічого не робить;
- абстрактний клас [InterfaceDao] не може бути інстанційований. Інстанціювати можна лише класи, похідні від [InterfaceDao], які реалізували всі методи класу [InterfaceDao]. Отже, якщо створити два класи [Dao1] та [Dao2], що походять від класу [InterfaceDao], то обидва вони реалізують абстрактні методи класу [InterfaceDao]. Таким чином, можна сказати, що вони реалізують інтерфейс [InterfaceDao];
- мови програмування, що реалізують як інтерфейси, так і абстрактні класи, надають інтерфейсу іншу роль, ніж абстрактному класу. Інтерфейс не має атрибутів і не може бути інстанційований. Клас може реалізувати інтерфейс, визначивши всі його методи;
14.2.4.2. Реалізація [Dao]
Клас [Dao] (dao.py) реалізує інтерфейс [InterfaceDao] наступним чином:
# імпорт сутностей та інтерфейсів
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
# шар [dao] реалізує інтерфейс InterfaceDao
class Dao(InterfaceDao):
# конструктор
# створюються жорстко задані списки
def __init__(self):
# створюються екземпляри класів
classe1 = Classe().fromdict({"id": 1, "nom": "classe1"})
classe2 = Classe().fromdict({"id": 2, "nom": "classe2"})
self.classes = [classe1, classe2]
# предмети
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]
# учні
é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]
# оцінки учнів з різних предметів
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]
# -----------
# інтерфейс IDao
# -----------
Примітки:
- рядки 1–7: імпортуються сутності та інтерфейс [InterfaceDao];
- рядок 11: клас [Dao] походить від абстрактного класу [InterfaceDao]. Скажемо, що він реалізує інтерфейс [InterfaceDao];
- рядок 14: конструктор не має параметрів. Він жорстко створює чотири списки:
- рядки 15–18: список класів;
- рядки 19–22: список предметів;
- рядки 23–28: список учнів;
- рядки 29–38: список оцінок;
- рядки 40–44: реалізація методів інтерфейсу [Interface Dao]. Тут ми їх не визначаємо, щоб побачити повідомлення про помилку, яке видає Python;
Приклад тестової програми може виглядати так: [tests-dao.py]:
# налаштування програми
import config
config = config.configure()
# створення екземпляра шару [dao]
from Dao import Dao
daoImpl = Dao()
# список класів
for classe in daoImpl.get_classes():
print(classe)
# список предметів
for matière in daoImpl.get_matières():
print(matière)
# список класів
for élève in daoImpl.get_élèves():
print(élève)
# список оцінок
for note in daoImpl.get_notes():
print(note)
Примітка: скрипт [tests-dao.py] не є тестом [unittest], оскільки він не містить методів, імена яких починаються з [test_].
Коментарі не потребують додаткових пояснень. У рядках 11–25 використовується інтерфейс шару [dao]. Тут немає жодних припущень щодо фактичної реалізації шару. У рядку 9 створюється екземпляр шару [dao].
Результати виконання цього скрипта такі:
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
Бачимо, що помилка виникає одразу після створення екземпляра класу [Dao] (рядок 3 вище). Інтерпретатор Python 3 повідомляє, що не може створити екземпляр класу, оскільки ми не визначили абстрактні методи [get_classes, get_matières, get_notes, get_notes_for_élève_by_id, get_élève_by_id, get_élèves].
Pycharm також підтримує поняття абстрактного класу і пропонує нам визначити його методи:
![]() |
- у [1] — клацніть правою кнопкою миші на коді;
- у [2-3] виберіть [Generate / Implement Methods], щоб реалізувати відсутні методи класу [Dao];
- у [4] виберіть методи, які потрібно реалізувати, у даному випадку всі;
Після цього клас [Dao] доповнюється класом PyCharm наступним чином:
# -----------
# інтерфейс 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
Ми доповнюємо клас [Dao] наступним чином:
# -----------
# інтерфейс IDao
# -----------
# список класів
def get_classes(self) -> list:
return self.classes
# список предметів
def get_matières(self) -> list:
return self.matières
# список учнів
def get_élèves(self) -> list:
return self.élèves
# список оцінок
def get_notes(self) -> list:
return self.notes
def get_notes_for_élève_by_id(self, élève_id: int) -> dict:
# пошук учня
élève = self.get_élève_by_id(élève_id)
# отримання його оцінок
notes = list(filter(lambda n: n.élève.id == élève_id, self.get_notes()))
# повернення результату
return {"élève": élève, "notes": notes}
def get_élève_by_id(self, élève_id: int) -> Elève:
# фільтрування учнів
élèves = list(filter(lambda e: e.id == élève_id, self.get_élèves()))
# знайдено?
if not élèves:
raise MyException(10, f"L'élève d'identifiant {élève_id} n'existe pas")
# результат
return élèves[0]
- рядки 5–19 не викликають труднощів;
- рядки 29–36: метод, який повертає учня, номер якого передається. Якщо такого учня не існує, генерується виняток;
- рядок 31: функція [filter] дозволяє відфільтрувати список:
- перший параметр — критерій фільтрації;
- другий параметр — це список, який потрібно відфільтрувати, у даному випадку список учнів;
- рядок 31: критерій фільтрації списку реалізовано за допомогою функції [f(e :Elève)->bool]. Ця функція застосовується до кожного елемента списку, що фільтрується. Якщо елемент відповідає критерію фільтрації, він залишається у відфільтрованому списку, інакше — виключається з нього. Тут можна або:
- задати ім’я функції f і реалізувати її в іншому місці. Тоді виклик функції [filter] перетворюється на [filter(f,self.get_élèves()];
- надати визначення функції f. Виклик функції [filter] тоді перетворюється на [filter(f(e :Elève){…},self.get_élèves()], де [e] представляє елемент відфільтрованого списку, тобто учня. Саме це й було зроблено в даному випадку. Визначення функції f у цьому випадку буде таким: [f(e :Elève){return e.id==élève_id)] — учень відбирається лише тоді, коли номер [id] не дорівнює шуканому. Таку функцію можна замінити так званою лямбда-функцією: [lambda e: e.id == élève_id]:
- e: позначає параметр функції f, у даному випадку — учня. Можна використовувати будь-яке ім’я на власний вибір;
- e.id==élève_id — це критерій фільтрації: учень [e] відбирається лише в тому випадку, якщо його номер [id] відповідає шуканому;
- рядок 31: функція [filter] повертає відфільтрований список у типі, який не є типом [list], але який можна перетворити на тип [list]. Саме це ми робимо тут за допомогою виразу [list(liste filtrée)];
- рядки 33–34: якщо відфільтрований список порожній, це означає, що шуканого учня не існує. У такому разі генерується виняток;
- рядок 36: якщо ми дійшли до цього місця, значить, винятку не сталося. Тоді ми знаємо, що отримали список з 1 елементом (немає двох учнів з однаковим номером [id]). Тому повертаємо перший елемент списку;
- рядки 21–27: метод [get_notes_for_élève_by_id] повинен повернути оцінки учня, номер якого передано йому як [id];
- рядки 22–23: спочатку виконується пошук учня з номером [élève_id] за допомогою методу [get_élève_by_id], який щойно було закомментований. Якщо шуканий учень не існує, може виникнути виняток. Оскільки навколо оператора в рядку 23 немає блоку try/catch, виняток буде передано до коду, що викликає. Це саме те, що потрібно;
- рядки 24–25: після того, як учень знайдений, отримуємо всі його оцінки. Це робимо знову за допомогою фільтра:
- фільтр має ім’я [filter(critère, self_getnotes()]. Отже, список, що підлягає фільтруванню, — це список усіх оцінок усіх учнів коледжу;
- критерій фільтрації виражається за допомогою функції [lambda]: lambda n: n.élève.id == élève_id. Параметр n є елементом списку, що фільтрується, тобто оцінкою. Тип [Note] має властивість [élève], яка позначає учня, якому належить оцінка. Отже, [n.élève.id], що позначає номер цього учня, має дорівнювати номеру шуканого учня;
Потім ми виконуємо скрипт [tests-dao.py].
# налаштування програми
import config
config = config.configure()
# створення екземпляра шару [dao]
from Dao import Dao
daoImpl = Dao()
# список класів
for classe in daoImpl.get_classes():
print(classe)
# перелік предметів
for matière in daoImpl.get_matières():
print(matière)
# список класів
for élève in daoImpl.get_élèves():
print(élève)
# список оцінок
for note in daoImpl.get_notes():
print(note)
# конкретний учень
print(daoImpl.get_élève_by_id(11))
# список його оцінок
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}")
У результаті отримуємо такі результати:
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
Можна помітити, що під час відображення оцінки (для інших об’єктів ситуація аналогічна) також відображається:
- ім’я учня, якому належить запис;
- предмет, до якого відноситься оцінка;
Цей результат генерує функція [BaseEntity.asdict] (див. параграф «посилання»).
14.2.5. Шар [métier]
![]() | ![]() |
- [InterfaceMétier] є інтерфейсом шару [métier];
- [Métier] — це клас реалізації шару [métier];
- [Testmétier] — це тестовий клас [UnitTest] для класу [Métier];
14.2.5.1. Інтерфейс [InterfaceMétier]
Рівень [métier] реалізує наступний інтерфейс [InterfaceMétier] (InterfaceMétier.py):
# імпортні дані
from abc import ABC, abstractmethod
from StatsForElève import StatsForElève
# бізнес-інтерфейс
class InterfaceMétier(ABC):
# розрахунок статистичних даних для учня
@abstractmethod
def get_stats_for_élève(self, idElève: int) -> StatsForElève:
pass
- [get_stats_for_élève] повертає оцінки учня № idElève, а також інформацію про них: зважений середній бал, найнижчу оцінку, найвищу оцінку. Ця інформація інкапсульована в об’єкт типу [StatsForElève];
14.2.5.2. Суть [StatsForElève]
Тип [StatsForElève] (StatsForElève.py), що інкапсулює статистичні дані (оцінки, мінімальне, максимальне значення, зважений середній бал) про учня, має такий вигляд:
# імпорт
from BaseEntity import BaseEntity
# статистика конкретного учня
class StatsForElève(BaseEntity):
# атрибути, виключені зі звіту про клас
excluded_keys = []
# властивості класу
@staticmethod
def get_allowed_keys() -> list:
# id: ідентифікатор оцінки
# учень: відповідний учень
# оцінки: його оцінки
# moyennePondérée: його середній бал, зважений за коефіцієнтами предметів
# min: його найнижча оцінка
# max: його максимальна оцінка
return BaseEntity.get_allowed_keys() + ["élève", "notes", "moyenne_pondérée", "min", "max"]
# toString
def __str__(self) -> str:
# випадок, коли учень не має оцінок
if len(self.notes) == 0:
return f"Elève={self.élève}, notes=[]"
# випадок учня з оцінками
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}"
Примітки:
- рядок 8: клас [StatsForElève] походить від класу [BaseEntity];
- рядки 13–22: властивості класу;
- ідентифікатор [id], що походить від [BaseEntity];
- учень [élève], статистичні дані якого інкапсульовано;
- його оцінки [notes];
- його зважений середній бал [moyenne_pondérée];
- його мінімальна оцінка [min];
- максимальне значення — [max];
- для цих атрибутів не визначаються методи getter/setter. Виходимо з того, що саме рівень [métier] створює об’єкти цього типу і що він не створює недійсних об’єктів;
- рядки 23–33: функція [__str__] повертає рядок, що містить властивості об’єкта;
14.2.5.3. Реалізація [Métier]
Реалізація [Métier] (Metier.py) інтерфейсу [InterfaceMétier] буде такою:
# імпортні дані
from InterfaceDao import InterfaceDao
from InterfaceMétier import InterfaceMétier
from StatsForElève import StatsForElève
class Métier(InterfaceMétier):
# конструктор
def __init__(self, dao: InterfaceDao):
# зберігаємо параметр
self.__dao = dao
# -----------
# інтерфейс
# -----------
# показники щодо оцінок конкретного учня
def get_stats_for_élève(self, id_élève: int) -> StatsForElève:
# Статистика для учня № idEleve
# id_élève: номер учня
# оцінки цього учня можна отримати за допомогою шару [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"]
# процес зупиняється, якщо оцінок немає
if len(notes) == 0:
# повертаємо результат
return StatsForElève().fromdict({"élève": élève, "notes": []})
# обробка оцінок учня
somme_pondérée = 0
somme_coeff = 0
max = -1
min = 21
for note in notes:
# значення оцінки
valeur = note.valeur
# коефіцієнт предмета
coeff = note.matière.coefficient
# сума коефіцієнтів
somme_coeff += coeff
# зважена сума
somme_pondérée += valeur * coeff
# пошук мінімального значення
if valeur < min:
min = valeur
# пошук максимального значення
if valeur > max:
max = valeur
# обчислення відсутніх показників
moyenne_pondérée = float(somme_pondérée) / somme_coeff
# результат видається у вигляді типу [StatsForElève]
return StatsForElève(). \
fromdict({"élève": élève, "notes": notes,
"moyenne_pondérée": moyenne_pondérée,
"min": min, "max": max})
Примітки
- рядок 7: клас [Métier] походить від класу [InterfaceMétier]. Зазвичай кажуть, що він реалізує інтерфейс [InterfaceMétier];
- рядки 9–12: конструктор отримує як єдиний параметр посилання на шар [dao]. У рядку 10 слід зауважити, що параметру [dao] присвоєно тип [InterfaceDao]. Ми не очікуємо конкретної реалізації, а лише реалізації, що відповідає інтерфейсу [InterfaceDao]. У даному випадку це не має значення, оскільки Python не враховуватиме цей тип, але це хороша практика — працювати з інтерфейсами, а не з конкретними реалізаціями. Тоді код легше модифікувати;
- рядки 19–60: реалізація методу [get_stats_for_élève];
- рядок 19: метод отримує єдиний параметр — номер [idElève] учня, статистику якого потрібно отримати;
- рядок 24: до шару [dao] надсилається запит на отримання оцінок учня. Цей запит призводить до винятку, якщо учень не існує. Цей виняток не обробляється (відсутність try / catch) і тому передається до коду, що викликає;
- рядок 25: сюди потрапляємо, якщо винятку не сталося. [notes_élève] є тоді словником із двома ключами [élève, note]:
- рядок 25: отримуємо інформацію про учня (його ім’я, клас тощо);
- рядок 26: отримуємо його оцінки;
- рядки 28–31: перевіряємо, чи є в учня оцінки. Якщо їх немає, то обчислювати статистику немає чого;
- рядок 31: повертається об’єкт [StatsForElève], побудований на основі словника за допомогою методу [BaseEntity.fromdict];
- рядки 33–54: використовуються оцінки учня для обчислення необхідних статистичних даних. Коментарів у коді має вистачити для його розуміння;
- рядки 56–60: повертається об’єкт [StatsForElève], побудований на основі словника за допомогою методу [BaseEntity.fromdict];
14.2.5.4. Тестування шару [métier]
Скрипт [UnitTest] шару [métier] може виглядати наступним чином (TestMétier.py):
# імпорт
import unittest
class Testmétier(unittest.TestCase):
def setUp(self):
# конфігурація додатка
import config
config.configure()
def test_statsForEleve11(self):
# імпортуємо
from Dao import Dao
from Métier import Métier
# тестування показників учня 11
dao = Dao()
stats_for_élève = Métier(dao).get_stats_for_élève(11)
# відображення
print(f"\nstats={stats_for_élève}")
# перевірки
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()
Примітки
- рядки 6–9: функція [setUp] використовується тут для налаштування Python Path тесту;
- рядок 16: створюється екземпляр шару [dao];
- рядок 17: створюється екземпляр шару [métier] і використовується його метод [get_stats_for_élève] для обчислення статистичних даних учня № 11;
- рядок 19: виводиться отриманий результат [StatsForElève]. Оскільки [StatsForElève] походить від [BaseEntity], тут виводиться ланцюжок jSON від [StatsForElève];
- рядок 21: перевіряється мінімальна оцінка учня;
- рядок 22: перевіряється його максимальна оцінка;
- рядок 23: перевіряється, чи зважний середній бал дорівнює 7,333 з точністю до 10⁻³. Як правило, неможливо точно порівнювати дійсні числа, оскільки внутрішньо вони найчастіше мають лише наближене представлення;
Результати перевірки такі:
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. Шар [ui]

- у [1] — інтерфейс шару [ui];
- у [2] — реалізація цього інтерфейсу;
- у [3] — головний скрипт додатка;
14.2.6.1. Інтерфейс [InterfaceUi]
Інтерфейс рівня [UI] матиме такий вигляд:
# імпорт
from abc import ABC, abstractmethod
# інтерфейс UI
class InterfaceUi(ABC):
# виконання шару UI
@abstractmethod
def run(self: object):
pass
Примітки
- рядки 9–10: рівень [UI] матиме лише один метод, [run];
14.2.6.2. Реалізація [Console]
Рівень [console] реалізовано за допомогою наступного скрипта [Console.py]:
# імпорт рівнів
from InterfaceDao import InterfaceDao
from InterfaceMétier import InterfaceMétier
from InterfaceUi import InterfaceUi
# інші залежності
from MyException import MyException
class Console(InterfaceUi):
# конструктор
def __init__(self: object, métier: InterfaceMétier):
# бізнес-логіка: шар [métier]
# зберігання атрибутів
self.métier = métier
# -----------
# інтерфейс
# -----------
def run(self):
# діалог з користувачем
fini = False
while not fini:
# питання / відповідь
réponse = input("Numéro de l'élève (>=1 et * pour arrêter) : ").strip()
# закінчено?
if réponse == "*":
break
# чи введено дані правильно?
ok = False
try:
id_élève = int(réponse, 10)
ok = id_élève >= 1
except ValueError as erreur:
pass
# дані правильні?
if not ok:
print("Saisie incorrecte. Recommencez...")
continue
# розрахунок статистичних даних для обраного учня
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}")
- рядки 3–5: імпорт усіх інтерфейсів;
- рядок 11: клас [Console] реалізує інтерфейс [InterfaceUi];
- рядки 12–17: конструктор класу [Console] отримує як параметр посилання на шар [métier]. Зауважимо, що цьому параметру присвоєно тип [InterfaceMétier], щоб нагадати, що ми працюємо з інтерфейсами, а не з конкретними реалізаціями;
- рядок 24: реалізація методу [run] інтерфейсу;
- рядок 27: цикл, який зупиняється, коли виконується умова з рядка 31;
- рядок 29: введення даних, набраних з клавіатури. Функція [input] приймає необов’язковий параметр: повідомлення, яке потрібно вивести на екран для запиту введення даних. Ці дані завжди отримуються у вигляді символьного рядка. Функція [strip] видаляє з нього «пробіли», що йдуть перед або після нього;
- рядки 34–39: перевіряється, чи введене значення — номер учня — є правильним. Це має бути ціле число >=1. Нагадаємо, що введене значення було отримано у вигляді символьного рядка;
- рядок 36: робиться спроба перетворити введене значення на ціле число в десятковій системі числення. Функція [int] генерує виняток, якщо це неможливо;
- рядок 37: до цього рядка доходимо лише в тому випадку, якщо виняток не виник. Перевіряємо, чи отримане ціле число дійсно >=1;
- рядки 38–39: обробляється виняток. Якщо стався виняток, змінна [ok] з рядка 34 залишилася на рівні [False];
- рядки 41–43: якщо введення було неправильним, виводиться повідомлення про помилку і виконується повторний цикл (рядок 43);
- рядки 45–48: обчислюються статистичні дані про учня, номер якого було введено;
- рядок 46: використовується метод [get_stats_for_élève] з шару [métier]. Цей метод генерує виняток, якщо учень не існує. Цей виняток обробляється в рядках 47–48. Відомо, що шари [dao] та [métier] генерують виняток [MyException];
14.3. Головний скрипт [main]
Головний скрипт [main] має такий вигляд (main.py):
# налаштування програми
import config
config = config.configure()
# syspath налаштовано — можна виконувати імпорт
from Console import Console
from Dao import Dao
from Métier import Métier
# ----------- рівень [console]
try:
# інстанціювання шару [dao]
dao = Dao()
# інстанціювання шару [métier]
métier = Métier(dao)
# інстанціювання шару [ui]
console = Console(métier)
# виконання шару [console]
console.run()
except BaseException as ex:
# виводиться повідомлення про помилку
print(f"L'erreur suivante s'est produite : {ex}")
finally:
pass
- рядки 1–4: налаштовується Python Path додатка;
- рядки 6–9: імпортуються необхідні класи та інтерфейси;
- рядок 14: створення екземпляра шару [dao];
- рядок 16: створення екземпляра шару [métier];
- рядок 18: створення екземпляра шару [ui];
- рядок 20: запускається діалог з користувачем;
- рядки 13–20: зазвичай з цих рядків не генеруються винятки. Ті винятки, що надходять із шарів [dao] та [métier], зупиняються шаром [Console]. Обробка винятків — це складне мистецтво, коли ви не досконало знаєте використовувані шари (у даному випадку це не так). У разі сумнівів можна додати код, щоб перехопити будь-який тип винятку, який може бути згенерований виконуваним кодом. Саме це й зроблено тут, у рядках 21–23. Перехоплюються всі винятки, що походять від [BaseException], тобто всі винятки;
- рядки 24–25: оператор [finally] тут нічого не робить. Він присутній лише для того, щоб можна було закомментувати рядки 21–23. Адже в режимі налагодження немає сенсу перехоплювати винятки. У цьому випадку їх зупиняє інтерпретатор Python, який потім видає номер рядка, де сталося виключення. Це необхідна інформація. Коли рядки 21–23 закомментовані, наявність рядків 24–25 дозволяє отримати синтаксично правильний блок try/catch. За їх відсутності Python видає помилку;
Ось приклад виконання:
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. Приклад 2
Цей новий приклад багаторівневої архітектури має на меті продемонструвати переваги програмування за допомогою інтерфейсів. Воно полегшує обслуговування та тестування додатків. Ми знову використаємо трирівневу архітектуру:

Кожен рівень буде реалізовано двома різними способами. Ми хочемо показати, що реалізацію одного рівня можна легко змінити, мінімально впливаючи на інші.
14.4.1. Рівень [dao]

Інтерфейс [InterfaceDao] виглядає так:
# імпорт
from abc import ABC, abstractmethod
# інтерфейс DAO
class InterfaceDao(ABC):
# єдиний метод
@abstractmethod
def do_something_in_dao_layer(self, x: int, y: int) -> int:
pass
- рядки 8–10: метод [do_something_in_dao_layer] є єдиним методом інтерфейсу;
Клас [DaoImpl1] реалізує інтерфейс [InterfaceDao] наступним чином:
from InterfaceDao import InterfaceDao
class DaoImpl1(InterfaceDao):
# реалізація InterfaceDao
def do_something_in_dao_layer(self: InterfaceDao, x: int, y: int) -> int:
return x + y
Клас [DaoImpl2] реалізує інтерфейс [InterfaceDao] наступним чином:
from InterfaceDao import InterfaceDao
class DaoImpl2(InterfaceDao):
# реалізація InterfaceDao
def do_something_in_dao_layer(self: InterfaceDao, x: int, y: int) -> int:
return x - y
14.4.2. Рівень [métier]

Інтерфейс [InterfaceMétier] має такий вигляд:
# імпорт
from abc import ABC, abstractmethod
# бізнес-інтерфейс
class InterfaceMétier(ABC):
# єдиний метод
@abstractmethod
def do_something_in_métier_layer(self, x: int, y: int) -> int:
pass
- рядки 8–10: метод [do_something_in_métier_layer] є єдиним методом інтерфейсу;
Клас [AbstractBaseMétier] реалізує інтерфейс [InterfaceMétier] наступним чином:
# імпорт
from abc import ABC, abstractmethod
from InterfaceDao import InterfaceDao
from InterfaceMétier import InterfaceMétier
class AbstractBaseMétier(InterfaceMétier, ABC):
# властивості
# __dao є посиланням на рівень [dao]
@property
def dao(self) -> InterfaceDao:
return self.__dao
@dao.setter
def dao(self, dao: InterfaceDao):
self.__dao = dao
# реалізація інтерфейсу [InterfaceMétier]
@abstractmethod
def do_something_in_métier_layer(self, x: int, y: int) -> int:
pass
- рядок 8: клас [AbstractBaseMétier] є похідним від двох класів:
- [InterfaceMétier]: клас [AbstractBaseMétier] реалізує цей інтерфейс у рядках 19–22. Насправді бачимо, що він не реалізував метод [do_something_in_métier_layer], який оголосив абстрактним (рядок 20). Реалізувати цей метод повинні похідні класи;
- [ABC], щоб отримати доступ до анотацій [@abstractmethod];
- порядок має значення: якщо його тут змінити на протилежний, Python видасть помилку під час виконання;
Це перший випадок використання множинної спадковості (спадкування від декількох класів). Клас [AbstractBaseMétier] одночасно успадковує властивості класів [InterfaceMétier] та [ABC].
- рядки 9–17: ми визначаємо властивість [dao], яка буде посиланням на шар [dao];
Інтерфейс призначений для реалізації. Коли різні реалізації мають спільні властивості, доцільно розмістити їх у батьківському класі, щоб уникнути їх дублювання. Саме так відбувається у випадку з властивістю [dao]. Батьківський клас, як правило, завжди є абстрактним, оскільки він не реалізує всіх методів інтерфейсу.
Клас [MétierImpl1] реалізує інтерфейс [InterfaceMétier] наступним чином:
from AbstractBaseMétier import AbstractBaseMétier
class MétierImpl1(AbstractBaseMétier):
# реалізація інтерфейсу [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)
- рядок 4: клас [MétierImpl1] походить від класу [AbstractbaseMétier]. Отже, він успадковує властивість [dao] цього класу;
- рядки 6–9: реалізація інтерфейсу [InterfaceMétier], який не реалізував батьківський клас [AbstractbaseMétier];
- рядок 9: використовується шар [dao];
Клас [MétierImpl2] реалізує інтерфейс [InterfaceMétier] аналогічним чином:
from AbstractBaseMétier import AbstractBaseMétier
class MétierImpl2(AbstractBaseMétier):
# реалізація інтерфейсу [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. Рівень [ui]

Інтерфейс [InterfaceUi] має такий вигляд:
# імпорт
from abc import ABC, abstractmethod
# інтерфейс Ui
class InterfaceUi(ABC):
# єдиний метод
@abstractmethod
def do_something_in_ui_layer(self, x: int, y: int) -> int:
pass
- рядки 8–10: єдиний метод інтерфейсу;
Клас [AbstractBaseUi] реалізує інтерфейс [InterfaceUi] наступним чином:
# імпорт
from abc import ABC, abstractmethod
from InterfaceMétier import InterfaceMétier
from InterfaceUi import InterfaceUi
class AbstractBaseUi(InterfaceUi, ABC):
# властивості
# бізнес-логіка є посиланням на рівні [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
# реалізація інтерфейсу [InterfaceUI]
@abstractmethod
def do_something_in_ui_layer(self: InterfaceUi, x: int, y: int) -> int:
pass
- клас [AbstractBaseUi] є абстрактним класом (рядок 20). Він повинен бути похідним, щоб реалізувати інтерфейс [InterfaceUi];
- рядки 9–17: клас [AbstractBaseUi] має посилання на шар [métier];
Клас реалізації [UiImpl1] має такий вигляд:
from AbstractBaseUi import AbstractBaseUi
class UiImpl1(AbstractBaseUi):
# реалізація інтерфейсу [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)
- рядок 4: клас [UiImpl1] походить від класу [AbstractBaseUi] і, отже, успадковує його властивість [métier]. Ця властивість використовується в рядку 9;
Клас реалізації [UiImpl2] є аналогічним:
from AbstractBaseUi import AbstractBaseUi
class UiImpl2(AbstractBaseUi):
# реалізація інтерфейсу [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)
- рядок 4: клас [UiImpl2] походить від класу [AbstractBaseUi] і, отже, успадковує його властивість [métier]. Ця властивість використовується в рядку 9;
14.4.4. Конфігураційні файли

- файли [config1, config2] налаштовують додаток двома різними способами;
- файл [main] є головним скриптом додатка;
Файл [config1] має такий вигляд:
def configure():
# крок 1 ------
# абсолютний шлях до папки цього скрипта
import os
script_dir = os.path.dirname(os.path.abspath(__file__))
# залежності
absolute_dependencies = [
# локальні папки Python Path
f"{script_dir}/../dao",
f"{script_dir}/../ui",
f"{script_dir}/../métier",
]
# налаштування syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# крок 2 ------
# налаштування шарів додатка
from DaoImpl1 import DaoImpl1
from MétierImpl1 import MétierImpl1
from UiImpl1 import UiImpl1
# інстанціювання шарів
# DAO
dao = DaoImpl1()
# бізнес-логіка
métier = MétierImpl1()
métier.dao = dao
# UI
ui = UiImpl1()
ui.métier = métier
# інстанції шарів додаємо до конфігурації
# тут потрібен лише шар ui
config = {"ui": ui}
# зберігаємо конфігурацію
return config
- рядки 2–16: налаштування Python Path додатка;
- рядки 18–31: створення екземплярів шарів [dao, métier, ui]. Для реалізації їхніх інтерфейсів щоразу обирається перша створена реалізація;
- рядки 33–35: додаємо посилання на шари до конфігурації. У цьому випадку головному скрипту потрібен лише шар [ui];
Файл [config2] є аналогічним і реалізує кожен інтерфейс за допомогою другої доступної реалізації:
def configure():
# крок 1 ---
# абсолютний шлях до папки цього скрипта
import os
script_dir = os.path.dirname(os.path.abspath(__file__))
# залежності
absolute_dependencies = [
# локальні папки Python Path
f"{script_dir}/../dao",
f"{script_dir}/../ui",
f"{script_dir}/../métier",
]
# налаштування syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# крок 2 ------
# налаштування шарів додатка
from DaoImpl2 import DaoImpl2
from MétierImpl2 import MétierImpl2
from UiImpl2 import UiImpl2
# інстанціювання шарів
# DAO
dao = DaoImpl2()
# бізнес-логіка
métier = MétierImpl2()
métier.dao = dao
# UI
ui = UiImpl2()
ui.métier = métier
# інстанції шарів додаємо до конфігурації
# тут потрібен лише шар ui
config = {"ui": ui}
# створюємо конфігурацію
return config
14.4.5. Головний скрипт [main]

Головний скрипт має такий вигляд:
# імпортуємо
import importlib
import sys
# main ---------
# потрібні два аргументи
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()
# конфігурація додатка
module = importlib.import_module(sys.argv[1])
config = module.configure()
# виконання шару [ui]
print(config["ui"].do_something_in_ui_layer(10, 20))
Цей скрипт приймає один параметр:
- [config1] — для використання конфігурації № 1;
- [config2] — для використання конфігурації № 2;
Python записує параметри у список [sys.argv]:
- sys.argv[0] — це ім’я скрипта, у даному випадку [main]. Цей параметр завжди присутній;
-
sys.argv[1] — це перший параметр, що передається скрипту, sys.argv[2] — другий, …
-
рядок 8: отримуємо кількість параметрів;
- рядки 9–11: перевіряється, чи дійсно є параметр і чи його значення дорівнює [config1] або [config2]. Якщо це не так, виводиться повідомлення про помилку (рядок 10) і програма завершується (рядок 11);
Як тільки бажана конфігурація відома, нам потрібно виконати цю конфігурацію. Наприклад, якщо було обрано конфігурацію 1, нам потрібно виконати код:
Проблема тут полягає в тому, що конфігурація, яку потрібно використовувати, знаходиться у змінній [len[liste1]]. Щоб імпортувати модуль, ім’я якого знаходиться у змінній, нам потрібно використати пакет [importlib] (рядок 2).
- рядок 14: імпортуємо модуль, ім’я якого міститься у [sys.argv[1];
- рядок 15: після цього виконується функція [configure] цього модуля. Отримується словник [config], який є конфігурацією додатка;
- рядок 18: відомо, що посилання на рівень [ui] міститься в config[‘ui’]. Його використовують для виклику методу [do_something_in_ui_layer]. Відомо, що цей метод викличе метод шару [métier], який, у свою чергу, викличе метод шару [dao];
Наприклад, функція [do_something_in_ui_layer] має такий вигляд:
class UiImpl1(AbstractBaseUi):
# реалізація інтерфейсу [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)
- у рядку 6 вище використовується властивість [métier] класу [UiImpl1], рядок 1. Однак у конфігурації [config1] було написано:
# бізнес-логіка
métier = MétierImpl1()
métier.dao = dao
# інтерфейс користувача
ui = UiImpl1()
ui.métier = métier
- рядок 6: властивість [métier] класу [UIImpl1] є посиланням на клас [MétierImpl1] (рядок 2). Отже, буде виконано метод [do_something_in_ui_layer] класу [MétierImpl1];
У класі [MétierUiImpl1] зазначено:
class MétierImpl1(AbstractBaseMétier):
# реалізація інтерфейсу [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)
- у рядку 6 метод, викликаний шаром [ui], у свою чергу викликає метод властивості [dao] класу [MétierImpl1];
Однак у конфігурації [config1] записано:
# DAO
dao = DaoImpl1()
# бізнес-логіка
métier = MétierImpl1()
métier.dao = dao
- рядок 5: властивість [MétierImpl1.dao] має тип [DaoImpl1] (рядок 2);
Тут ми хочемо показати, що скрипт [main] не повинен враховувати шари [métier] та [dao]. Йому потрібно опікуватися лише шаром [ui], оскільки зв’язки між цим шаром та іншими вже встановлено під час налаштування.

Щоб передати параметр [config1] або [config2] скрипту [main], , слід виконати такі дії:

- у [1-2] створюється так звана конфігурація виконання;
- у [3] надаємо цій конфігурації ім’я, щоб можна було її знайти;
- у [4] вибираємо скрипт для виконання. Якщо ми дотримувалися процедури [1-2], потрібний скрипт уже вибрано;
- у полі «[5]» вказуються параметри, які потрібно передати скрипту. Тут вводиться рядок «[config1]», щоб скрипт використовував конфігурацію № 1;
- у [6] підтверджується конфігурація виконання;

- у [1-2] запитуємо перелік існуючих контекстів виконання;
- у [3] вибираємо існуючий контекст виконання та дублюємо його у [4];

- у [5] вказується ім’я нової конфігурації. Саме вона буде виконувати скрипт [main] [6], передаючи йому параметр [config2] [7];
Конфігурації виконання доступні у верхньому правому куті вікна PyCharm:

Достатньо вибрати [2] або [3], а потім натиснути [4], щоб виконати скрипт [main] зодним із параметрів [config1] або [config2].
З параметром [config1] виконання [main] дає такі результати:
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
З параметром [config2] виконання [main] дає такі результати:
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
Просимо читача перевірити ці результати.













