Практика · Тема 16 · Модулі й пакети¶
Лекція: lecture.html · Тест: quiz.html · Домашнє завдання: homework.md
Досі весь код жив в одному місці. Зараз ми розкладемо його по файлах — і подивимось, як ці файли знаходять одне одного.
Що зробимо:
- створимо власний модуль прямо із зошита й імпортуємо його;
- спробуємо всі три форми імпорту й побачимо, які імена з'являються;
- на власні очі переконаємось, чим небезпечна
from модуль import *; - простежимо, як Python шукає модуль по
sys.path, і отримаємоModuleNotFoundError; - переконаємось, що тіло модуля виконується один раз;
- побачимо
__name__в обох ролях — при запуску й при імпорті; - зберемо справжній пакет із
__init__.pyі підпакетом; - перевіримо власну реалізацію квадратного кореня проти
math.sqrt; - пройдемось стандартною бібліотекою.
Важливо: усі файли створюються в тимчасовій теці, яку операційна система прибере сама. Твій проєкт залишиться чистим, а зошит — самодостатнім: його можна запустити де завгодно.
1 · Тимчасова майстерня¶
Щоб імпортувати власний файл, Python має його побачити — тобто тека з файлом мусить
бути у списку sys.path. Зробимо тимчасову теку й додамо її на початок списку.
import sys
import tempfile
from pathlib import Path
# окрема тека для всіх файлів цієї практики — щоб не смітити в проєкті
majsternya = Path(tempfile.mkdtemp(prefix="tema16_"))
# додаємо на ПОЧАТОК: саме так Python поводиться з текою запущеного скрипта
sys.path.insert(0, str(majsternya))
print("майстерня:", majsternya)
print("перший рядок sys.path:", sys.path[0])
print("тека існує:", majsternya.is_dir())
майстерня: /tmp/tema16_d_d2ahl5 перший рядок sys.path: /tmp/tema16_d_d2ahl5 тека існує: True
2 · Перший модуль¶
Модуль — це просто файл .py. Напишемо файл znyzhky.py з однією константою і однією
функцією. Файл пишемо методом write_text: він створює файл і кладе туди рядок.
kod_znyzhky = '''
"""Розрахунок цін зі знижкою — модуль для навчальної крамниці."""
STANDARTNA = 0.10
def cina_zi_znyzhkoyu(cina, vidsotok):
"""Ціна, зменшена на заданий відсоток (0.10 = десять відсотків)."""
# округлюємо до копійок, бо це ціна, а не абстрактне число
return round(cina * (1 - vidsotok), 2)
'''
fajl_znyzhky = majsternya / "znyzhky.py"
fajl_znyzhky.write_text(kod_znyzhky, encoding="utf-8")
print("створено:", fajl_znyzhky.name)
print("---- вміст файлу ----")
print(fajl_znyzhky.read_text(encoding="utf-8"))
створено: znyzhky.py
---- вміст файлу ----
"""Розрахунок цін зі знижкою — модуль для навчальної крамниці."""
STANDARTNA = 0.10
def cina_zi_znyzhkoyu(cina, vidsotok):
"""Ціна, зменшена на заданий відсоток (0.10 = десять відсотків)."""
# округлюємо до копійок, бо це ціна, а не абстрактне число
return round(cina * (1 - vidsotok), 2)
Тепер найцікавіше: файл лежить на диску, а Python про нього ще не знає. Один рядок — і знає.
import znyzhky
print("модуль:", znyzhky)
print("завантажено з файлу:", znyzhky.__file__)
print("ціна 250 зі стандартною знижкою:", znyzhky.cina_zi_znyzhkoyu(250, znyzhky.STANDARTNA))
# перевіряємо, що функція справді рахує те, що ми задумали
assert znyzhky.cina_zi_znyzhkoyu(250, 0.10) == 225.0, "знижка порахувалась не так!"
print("✅ модуль працює")
модуль: <module 'znyzhky' from '/tmp/tema16_d_d2ahl5/znyzhky.py'> завантажено з файлу: /tmp/tema16_d_d2ahl5/znyzhky.py ціна 250 зі стандартною знижкою: 225.0 ✅ модуль працює
3 · Три форми імпорту¶
Форми відрізняються рівно одним: які імена з'являються у твоєму просторі імен.
Перевіримо це не на слово, а вимірюванням: подивимось, чи існує ім'я у словнику globals().
# globals() повертає словник імен цієї клітинки (точніше — цього зошита як модуля)
def imya_isnuye(imya):
"""Чи є таке ім'я в поточному просторі імен?"""
return imya in globals()
from znyzhky import cina_zi_znyzhkoyu # форма 2: беремо одне ім'я
import znyzhky as zn # форма 3: той самий модуль під псевдонімом
print("ім'я znyzhky :", imya_isnuye("znyzhky"))
print("ім'я cina_zi_znyzhkoyu :", imya_isnuye("cina_zi_znyzhkoyu"))
print("ім'я zn :", imya_isnuye("zn"))
print()
print("усі три ведуть до того самого об'єкта:", znyzhky is zn)
print("функція теж та сама :", znyzhky.cina_zi_znyzhkoyu is cina_zi_znyzhkoyu)
assert znyzhky is zn, "псевдонім мав указувати на той самий модуль"
print("✅ псевдонім — це друге ім'я, а не друга копія")
ім'я znyzhky : True ім'я cina_zi_znyzhkoyu : True ім'я zn : True усі три ведуть до того самого об'єкта: True функція теж та сама : True ✅ псевдонім — це друге ім'я, а не друга копія
4 · Чому import * — погана звичка¶
Створимо маленький модуль, у якому навмисно є ім'я kurs. Уяви, що в нас уже є своя змінна
з такою назвою. Подивимось, що з нею станеться після зірочки.
kod_valyuta = '''
"""Курси валют — модуль-пастка для демонстрації."""
kurs = 41.5 # тут kurs означає курс долара
NAZVA = "долар"
'''
(majsternya / "valyuta.py").write_text(kod_valyuta, encoding="utf-8")
kurs = "курс молодого бійця" # НАША змінна: зовсім про інше
print("до імпорту kurs =", repr(kurs))
from valyuta import * # ← ось вона, зірочка
print("після імпорту kurs =", repr(kurs))
print()
print("наше значення зникло без жодного повідомлення про помилку")
assert kurs == 41.5, "зірочка мала затерти наше значення"
print("✅ саме це й показує демонстрація: тихе затирання")
до імпорту kurs = 'курс молодого бійця' після імпорту kurs = 41.5 наше значення зникло без жодного повідомлення про помилку ✅ саме це й показує демонстрація: тихе затирання
Порівняй із безпечною формою. Повернемо своє значення й імпортуємо те саме через import:
kurs = "курс молодого бійця" # відновлюємо своє значення
import valyuta # модуль цілком, під власним іменем
print("наше kurs =", repr(kurs))
print("чуже valyuta.kurs =", valyuta.kurs)
assert kurs == "курс молодого бійця", "наше значення мало залишитись на місці"
print("✅ обидва значення живі, і видно, кому яке належить")
наше kurs = 'курс молодого бійця' чуже valyuta.kurs = 41.5 ✅ обидва значення живі, і видно, кому яке належить
5 · Як Python шукає модуль¶
Маршрут: спершу кеш sys.modules, потім вбудовані модулі, потім теки зі списку sys.path
згори вниз. Подивимось на цей список і перевіримо кілька імен без жодної помилки —
для цього є importlib.util.find_spec: він повертає опис знайденого модуля або None.
import importlib.util
print("перші три теки з sys.path:")
for teka in sys.path[:3]:
print(" ", teka if teka else "'' (тека запуску)")
print()
for imya in ["znyzhky", "json", "sys", "takogo_modulya_nemaje"]:
opys = importlib.util.find_spec(imya)
if opys is None:
print(f"{imya:<24} → не знайдено ніде")
elif opys.origin in (None, "built-in"):
print(f"{imya:<24} → вбудований в інтерпретатор")
else:
print(f"{imya:<24} → {opys.origin}")
assert importlib.util.find_spec("takogo_modulya_nemaje") is None
print()
print("✅ find_spec чесно каже «немає», не піднімаючи помилки")
перші три теки з sys.path:
/tmp/tema16_d_d2ahl5
/usr/lib64/python312.zip
/usr/lib64/python3.12
znyzhky → /tmp/tema16_d_d2ahl5/znyzhky.py
json → /usr/lib64/python3.12/json/__init__.py
sys → вбудований в інтерпретатор
takogo_modulya_nemaje → не знайдено ніде
✅ find_spec чесно каже «немає», не піднімаючи помилки
А тепер подивимось, як виглядає справжня помилка. Клітинка нижче навмисно падає —
це і є та сама ModuleNotFoundError, яку ти бачитимеш найчастіше.
import takogo_modulya_nemaje
--------------------------------------------------------------------------- ModuleNotFoundError Traceback (most recent call last) Cell In[8], line 1 ----> 1 import takogo_modulya_nemaje ModuleNotFoundError: No module named 'takogo_modulya_nemaje'
Читай останній рядок: No module named 'takogo_modulya_nemaje'. Це не поломка Python,
а звіт: маршрут пройдено до кінця, збігів немає. Три типові причини — одрук в імені,
пакет не встановлений, або файл лежить у теці, якої немає в sys.path.
6 · Модуль виконується один раз¶
Імпорт — це виконання файлу. Напишемо модуль, який друкує рядок при завантаженні, та імпортуємо його чотири рази поспіль. Рахуємо, скільки разів побачимо друк.
kod_lichylnyk = '''
print(" [тіло модуля lichylnyk виконується]")
ZAPUSKIV = 1
'''
(majsternya / "lichylnyk.py").write_text(kod_lichylnyk, encoding="utf-8")
print("імпорт 1:")
import lichylnyk
print("імпорт 2:")
import lichylnyk
print("імпорт 3:")
from lichylnyk import ZAPUSKIV
print("імпорт 4:")
import lichylnyk as l4
print()
print("рядок вище надрукувався рівно один раз — решта взята з кешу sys.modules")
імпорт 1: [тіло модуля lichylnyk виконується] імпорт 2: імпорт 3: імпорт 4: рядок вище надрукувався рівно один раз — решта взята з кешу sys.modules
Переконаємось, що модуль справді лежить у кеші й що всі імена вказують на один об'єкт.
print("'lichylnyk' у sys.modules:", "lichylnyk" in sys.modules)
print("той самий об'єкт:", lichylnyk is sys.modules["lichylnyk"] is l4)
assert "lichylnyk" in sys.modules, "після імпорту модуль має бути в кеші"
assert lichylnyk is l4, "псевдонім і звичайне ім'я — це один модуль"
print("✅ кеш працює: один файл — один об'єкт на весь запуск програми")
'lichylnyk' у sys.modules: True той самий об'єкт: True ✅ кеш працює: один файл — один об'єкт на весь запуск програми
7 · __name__ у двох ролях¶
Той самий файл поводиться по-різному залежно від того, запустили його чи імпортували.
Напишемо zvit.py із захисною умовою і перевіримо обидва сценарії.
kod_zvit = '''
"""Розрахунок суми з ПДВ."""
STAVKA_PDV = 0.20
def z_pdv(suma):
"""Сума разом із податком, округлена до копійок."""
return round(suma * (1 + STAVKA_PDV), 2)
print("рядок на верхньому рівні: виконується ЗАВЖДИ, __name__ =", __name__)
if __name__ == "__main__":
# цей блок працює лише при прямому запуску файлу
print("демонстрація: 1000 з ПДВ =", z_pdv(1000))
'''
(majsternya / "zvit.py").write_text(kod_zvit, encoding="utf-8")
print("файл zvit.py створено, рядків:", len(kod_zvit.strip().split(chr(10))))
файл zvit.py створено, рядків: 15
Сценарій 1 — прямий запуск. Запускаємо файл окремим процесом тим самим інтерпретатором,
у якому працює зошит. sys.executable — це шлях до нього.
import subprocess
zapusk = subprocess.run([sys.executable, str(majsternya / "zvit.py")],
capture_output=True, text=True)
print("---- що надрукував python3 zvit.py ----")
print(zapusk.stdout)
assert "__main__" in zapusk.stdout, "при прямому запуску __name__ мав бути __main__"
assert "демонстрація" in zapusk.stdout, "захисний блок мав виконатись"
print("✅ запустили напряму → __name__ == '__main__', блок спрацював")
---- що надрукував python3 zvit.py ---- рядок на верхньому рівні: виконується ЗАВЖДИ, __name__ = __main__ демонстрація: 1000 з ПДВ = 1200.0 ✅ запустили напряму → __name__ == '__main__', блок спрацював
Сценарій 2 — імпорт. Той самий файл, жодного символу не змінено.
import zvit
print()
print("ім'я модуля:", zvit.__name__)
print("функція доступна:", zvit.z_pdv(500))
print("а от рядка «демонстрація» ми не побачили — умова була хибна")
assert zvit.__name__ == "zvit", "при імпорті __name__ дорівнює імені модуля"
assert zvit.z_pdv(1000) == 1200.0, "функція має рахувати ПДВ 20%"
print("✅ імпортували → __name__ == 'zvit', захисний блок промовчав")
рядок на верхньому рівні: виконується ЗАВЖДИ, __name__ = zvit ім'я модуля: zvit функція доступна: 600.0 а от рядка «демонстрація» ми не побачили — умова була хибна ✅ імпортували → __name__ == 'zvit', захисний блок промовчав
8 · Збираємо пакет¶
Пакет — це тека з модулями та файлом __init__.py. Зберемо крамницю: пакет krama
з двома модулями й підпакетом zvity всередині.
teka_krama = majsternya / "krama"
teka_zvity = teka_krama / "zvity"
teka_zvity.mkdir(parents=True) # parents=True створює обидві теки одразу
# __init__.py пакета: виконується першим при import krama
(teka_krama / "__init__.py").write_text(
'''
"""Навчальна крамниця."""
OPYS = "крамниця кави та чаю"
print(" [виконується krama/__init__.py]")
''', encoding="utf-8")
(teka_krama / "tovary.py").write_text(
'''
CINY = {"кава": 250, "чай": 120, "какао": 180}
def cina(nazva):
"""Ціна товару або 0, якщо такого немає."""
return CINY.get(nazva, 0)
''', encoding="utf-8")
(teka_zvity / "__init__.py").write_text(
'''
FORMAT = "текстовий"
print(" [виконується krama/zvity/__init__.py]")
''', encoding="utf-8")
(teka_zvity / "pdf.py").write_text(
'''
def zberehty(ryadky):
"""Імітація збереження звіту: повертає кількість рядків."""
return len(ryadky)
''', encoding="utf-8")
print("дерево пакета:")
for shlyah in sorted(teka_krama.rglob("*.py")):
print(" ", shlyah.relative_to(majsternya))
дерево пакета:
krama/__init__.py
krama/tovary.py
krama/zvity/__init__.py
krama/zvity/pdf.py
Тепер імпортуємо. Стеж за друком: __init__.py кожного рівня виконується тоді, коли до
цього рівня доходить черга — і теж лише один раз.
print("import krama:")
import krama
print(" OPYS =", krama.OPYS)
print()
print("import krama.tovary:")
from krama import tovary
print(" ціна кави:", tovary.cina("кава"))
print()
print("import krama.zvity.pdf:")
from krama.zvity import pdf
print(" рядків у звіті:", pdf.zberehty(["кава 250", "чай 120"]))
assert tovary.cina("кава") == 250, "ціна кави мала бути 250"
assert pdf.zberehty(["a", "b", "c"]) == 3, "звіт мав порахувати три рядки"
print()
print("✅ пакет зібрано й повністю працює")
import krama: [виконується krama/__init__.py] OPYS = крамниця кави та чаю import krama.tovary: ціна кави: 250 import krama.zvity.pdf: [виконується krama/zvity/__init__.py] рядків у звіті: 2 ✅ пакет зібрано й повністю працює
Зверни увагу на повні імена модулів: вони рахуються від кореня проєкту, а не від теки, у якій ти пишеш. Це і є абсолютний імпорт.
print("krama →", krama.__name__)
print("krama.tovary →", tovary.__name__)
print("krama.zvity.pdf →", pdf.__name__)
print()
print("у sys.modules з'явилися записи на кожен рівень:")
for imya in sorted(k for k in sys.modules if k.startswith("krama")):
print(" ", imya)
assert pdf.__name__ == "krama.zvity.pdf", "повне ім'я має містити всі рівні"
print()
print("✅ кожна крапка — це один рівень теки")
krama → krama
krama.tovary → krama.tovary
krama.zvity.pdf → krama.zvity.pdf
у sys.modules з'явилися записи на кожен рівень:
krama
krama.tovary
krama.zvity
krama.zvity.pdf
✅ кожна крапка — це один рівень теки
9 · Наша реалізація проти бібліотечної¶
Найкорисніше, що дає практика: побачити, що всередині бібліотеки немає магії. Напишемо
власний модуль із квадратним коренем за методом Ньютона й порівняємо з math.sqrt.
kod_matematyka = '''
"""Наша власна маленька математика."""
def koren(chyslo, krokiv=40):
"""Квадратний корінь методом Ньютона.
Ідея: беремо будь-яке припущення й багато разів усереднюємо його
з часткою chyslo/pryp. Значення швидко сходиться до кореня.
"""
if chyslo == 0:
return 0.0
pryp = chyslo / 2 # перше припущення — половина числа
for _ in range(krokiv):
pryp = (pryp + chyslo / pryp) / 2
return pryp
'''
(majsternya / "moya_matematyka.py").write_text(kod_matematyka, encoding="utf-8")
import math
import moya_matematyka
print(f"{'число':>10} | {'наш корінь':>20} | {'math.sqrt':>20}")
print("-" * 58)
for chyslo in [2, 9, 16, 123.45, 1e6]:
nash = moya_matematyka.koren(chyslo)
bibliotechnyj = math.sqrt(chyslo)
print(f"{chyslo:>10} | {nash:>20.15f} | {bibliotechnyj:>20.15f}")
число | наш корінь | math.sqrt
----------------------------------------------------------
2 | 1.414213562373095 | 1.414213562373095
9 | 3.000000000000000 | 3.000000000000000
16 | 4.000000000000000 | 4.000000000000000
123.45 | 11.110805551354051 | 11.110805551354051
1000000.0 | 1000.000000000000000 | 1000.000000000000000
# перевіряємо збіг на всьому наборі одразу, з допуском на похибку float
rozbizhnosti = []
for chyslo in [2, 9, 16, 123.45, 1e6, 0.25, 7]:
rozbizhnosti.append(abs(moya_matematyka.koren(chyslo) - math.sqrt(chyslo)))
najbilsha = max(rozbizhnosti)
print("найбільша розбіжність:", najbilsha)
assert najbilsha < 1e-9, "наш корінь розійшовся з бібліотечним!"
print("✅ збігається — усередині math.sqrt теж просто алгоритм")
найбільша розбіжність: 2.220446049250313e-16 ✅ збігається — усередині math.sqrt теж просто алгоритм
10 · Батарейки в комплекті¶
Шість модулів стандартної бібліотеки, які знадобляться найшвидше. Нічого встановлювати не треба — вони приїхали разом з інтерпретатором.
import random
import json
import collections
from datetime import date
# зерно робить «випадковість» відтворюваною — той самий результат при кожному запуску
random.seed(42)
print("math · корінь із 2 :", round(math.sqrt(2), 6))
print("random · кидок кубика :", random.randint(1, 6))
print("datetime · днів у 2026 році :", (date(2027, 1, 1) - date(2026, 1, 1)).days)
print("pathlib · склеєний шлях :", Path("dani") / "zvit.csv")
print("json · об'єкт у текст :", json.dumps({"mova": "uk"}))
print("collections · лічильник :", collections.Counter(["кава", "чай", "кава"]))
math · корінь із 2 :
1.414214
random · кидок кубика : 6
datetime · днів у 2026 році : 365
pathlib · склеєний шлях : dani/zvit.csv
json · об'єкт у текст : {"mova": "uk"}
collections · лічильник : Counter({'кава': 2, 'чай': 1})
# перевіряємо кілька тверджень про ці модулі — щоб не вірити на слово
assert json.loads(json.dumps({"mova": "uk"})) == {"mova": "uk"}, "json туди й назад має збігтись"
assert (Path("dani") / "zvit.csv").suffix == ".csv", "pathlib має розпізнати розширення"
assert collections.Counter("кака")["к"] == 2, "лічильник мав знайти дві букви к"
random.seed(42)
persha_seriya = [random.randint(1, 6) for _ in range(5)]
random.seed(42)
druha_seriya = [random.randint(1, 6) for _ in range(5)]
print("з тим самим зерном:", persha_seriya, "і", druha_seriya)
assert persha_seriya == druha_seriya, "однакове зерно має давати однакову послідовність"
print("✅ усі твердження про стандартну бібліотеку підтвердились")
з тим самим зерном: [6, 1, 1, 6, 3] і [6, 1, 1, 6, 3] ✅ усі твердження про стандартну бібліотеку підтвердились
11 · Прибираємо за собою¶
Тимчасова тека більше не потрібна. Видаляємо її — модулі, які вже завантажені в пам'ять,
від цього нікуди не подінуться, бо вони живуть у sys.modules.
import shutil
fajliv_bulo = len(list(majsternya.rglob("*.py")))
shutil.rmtree(majsternya)
sys.path.remove(str(majsternya))
print("видалено файлів .py:", fajliv_bulo)
print("тека існує:", majsternya.exists())
print("а модуль у пам'яті — так, живий:", znyzhky.cina_zi_znyzhkoyu(100, 0.5))
assert not majsternya.exists(), "тимчасова тека мала зникнути"
print("✅ проєкт лишився чистим")
видалено файлів .py: 9 тека існує: False а модуль у пам'яті — так, живий: 50.0 ✅ проєкт лишився чистим
Завдання¶
🟢 Рівень 1 — База¶
Створи в тимчасовій теці модуль perevodyach.py з двома функціями: v_hryvni(usd, kurs)
і v_dolary(uah, kurs). Імпортуй його трьома різними формами й перевір
assert-ом, що переведення туди й назад повертає початкове число.
Зроблено, якщо: усі три форми імпорту працюють, а assert проходить.
🟡 Рівень 2 — Плюс¶
Додай до perevodyach.py блок if __name__ == "__main__": з демонстрацією.
Запусти файл через subprocess і переконайся, що демонстрація друкується.
Потім імпортуй його й переконайся, що при імпорті демонстрації немає.
Зроблено, якщо: обидва assert-и (на наявність і на відсутність рядка) проходять.
🔴 Рівень 3 — Виклик¶
Збери пакет finansy з підпакетом finansy/kursy і модулем finansy/kursy/nbu.py.
У finansy/__init__.py зроби фасад: перетягни туди функцію з глибини так, щоб
працювало from finansy import v_hryvni. Потім навмисно створи циклічний імпорт
між двома модулями пакета, отримай AttributeError — і виправ його, винісши спільну
константу в третій модуль.
Зроблено, якщо: фасад працює, циклічний імпорт відтворено (traceback збережено
в клітинці з тегом raises-exception) і після виправлення пакет імпортується без помилок.