27. (Л) Модулі та пакети у Python. Імпорт, структура проєкту¶
Зміст лекції¶
- Навіщо ділити програму на файли
- Модуль — це файл
.py - Форми інструкції
import - Модуль — це обʼєкт
- Що відбувається під час імпорту
__name__іif __name__ == "__main__":from ... importстворює нове імʼя, а не посилання на змінну модуля- Де Python шукає модулі:
sys.path - Затінення стандартних модулів
- Стандартна бібліотека
- Пакети: каталог з
__init__.py - Абсолютний і відносний імпорт
- Запуск модуля з пакета:
python3 -m - Циклічні імпорти
- Порядок і стиль імпортів
- Структура проєкту
- Приклад: проєкт «журнал оцінок»
- Типові помилки
Навіщо ділити програму на файли¶
Досі кожна наша програма жила в одному файлі. Поки програма займає 50–100 рядків, так і має бути. Але програми ростуть: у домашніх завданнях лекцій 21–26 вже були функції для читання файлу, для обчислень, для виводу звіту, для налаштування логування. Коли все це лежить в одному файлі на 500 рядків, зʼявляються проблеми:
- Важко знайти потрібне. Щоб виправити формат звіту, доводиться гортати повз читання файлів і обчислення.
- Неможливо використати повторно. Функцію
average()з одного завдання хочеться взяти в інше. Копіювання означає, що помилку доведеться виправляти в кількох місцях. - Важко працювати вдвох. Двоє людей, які одночасно редагують один великий файл, постійно отримують конфлікти в git (лекція 12).
- Усе бачить усе. Будь-яка функція може випадково використати чи змінити будь-яку глобальну змінну.
Розвʼязок — розбити програму на модулі: окремі файли, кожен з яких відповідає за одну частину роботи. Модулі, повʼязані за змістом, обʼєднують у пакети.
Ви вже користувалися чужими модулями: import sys, import logging, from pathlib import Path. Сьогодні розберемо, як це працює, і навчимося писати власні.
Модуль — це файл .py¶
Модуль (module) — це будь-який файл з кодом на Python. Імʼя модуля — це імʼя файлу без .py: файл grades.py — це модуль grades.
Створіть у порожньому каталозі два файли.
# File: grades.py
PASS_SCORE = 60
def average(scores: list[int]) -> float:
return sum(scores) / len(scores)
def is_passed(score: float) -> bool:
return score >= PASS_SCORE
# File: main.py
import grades
scores = [78, 92, 55]
avg = grades.average(scores)
print(f"average: {avg:.1f}")
print("passed:", grades.is_passed(avg))
print("pass score:", grades.PASS_SCORE)
Запустіть main.py:
Інструкція import grades знаходить файл grades.py у тому ж каталозі, де лежить main.py, виконує його і дає доступ до всього, що в ньому визначено, через крапку: grades.average, grades.PASS_SCORE.
Тепер grades.py можна підключити до будь-якої іншої програми — функції написано один раз.
Імʼя файлу має бути правильним ідентифікатором
Імʼя модуля використовується в коді, тому воно підпорядковується тим самим правилам, що й імена змінних (лекція 04): латинські літери, цифри, _, не починається з цифри.
| Файл | Чи можна імпортувати |
|---|---|
grades.py, student_report.py |
так |
student-report.py |
ні — import student-report означає «student мінус report» |
01_grades.py |
ні — імʼя починається з цифри |
Grades.py |
так, але PEP 8 радить імена модулів малими літерами |
Скрипти, які лише запускають, можна називати як завгодно. Але файли, які імпортуватимуть, — тільки lowercase_with_underscores.
Форми інструкції import¶
import module¶
Імпортується модуль цілком, до вмісту звертаються через grades.. Це рекомендована форма за замовчуванням: з коду одразу видно, звідки взялася функція. grades.average(...) читається однозначно, а просто average(...) змушує шукати, де її визначено.
import module as alias¶
Модулю дається коротше імʼя (псевдонім). Використовують для довгих назв або коли є загальноприйнята домовленість, наприклад import numpy as np. Не варто вигадувати власні загадкові скорочення.
from module import name¶
Імпортуються конкретні імена — їх можна використовувати без префікса. Зручно, коли імʼя часто використовується і його походження очевидне: from pathlib import Path, from datetime import datetime.
from module import name as alias¶
Допомагає, коли імена з різних модулів збігаються.
from module import *¶
# File: shapes.py
__all__ = ["circle_area"]
PI = 3.14159
def circle_area(radius: float) -> float:
return PI * radius**2
def _check(radius: float) -> None:
if radius < 0:
raise ValueError("radius must be non-negative")
12.56636
Traceback (most recent call last):
File "/home/student/project/main.py", line 5, in <module>
print(PI)
^^
NameError: name 'PI' is not defined
* імпортує всі «публічні» імена модуля. Що саме вважається публічним:
- якщо в модулі є список
__all__— тільки імена з нього (томуPIне імпортувався); - якщо
__all__немає — усі імена, що не починаються з_.
Не використовуйте import * у своєму коді
Після from shapes import * неможливо зрозуміти, звідки взялося імʼя, редактор і mypy гірше підказують, а нове імʼя в модулі може непомітно перезаписати вашу змінну з такою самою назвою. import * іноді доречний в інтерактивному сеансі python3, але не в програмах.
Підкреслення на початку імені
Імʼя _check означає «внутрішня деталь модуля, ззовні не використовуйте». Python не забороняє shapes._check(...), це домовленість між програмістами. Але import * такі імена пропускає, а редактор зазвичай не пропонує їх у підказках.
Порівняння¶
| Форма | Як викликати | Коли використовувати |
|---|---|---|
import grades |
grades.average(...) |
за замовчуванням |
import grades as gr |
gr.average(...) |
довга назва або загальноприйнятий псевдонім |
from grades import average |
average(...) |
імʼя часто використовується і його походження очевидне |
from grades import average as avg_of |
avg_of(...) |
конфлікт імен |
from grades import * |
average(...) |
не використовувати |
Модуль — це обʼєкт¶
Після import grades змінна grades посилається на обʼєкт модуля — такий самий обʼєкт, як список чи функція. У нього є атрибути: усе, що визначено у файлі, плюс кілька службових.
# File: main.py (grades.py from the previous section is next to it)
import grades
print(type(grades))
print(grades.__name__)
print(grades.__file__)
print([name for name in dir(grades) if not name.startswith("__")])
| Атрибут | Що містить |
|---|---|
__name__ |
імʼя модуля |
__file__ |
повний шлях до файлу модуля |
__doc__ |
рядок документації модуля (перший рядок-літерал у файлі) |
dir(module) |
список усіх імен у модулі |
Кожен модуль має власний простір імен (namespace). Змінна PASS_SCORE у grades.py і змінна PASS_SCORE у main.py — це дві різні змінні, які не заважають одна одній. Саме тому модулі розвʼязують проблему «усе бачить усе»: те, що визначено в модулі, видно ззовні лише через grades..
Документуйте модуль
Першим рядком файлу пишіть рядок документації — він потрапить у __doc__ і в help(grades):
Що відбувається під час імпорту¶
# File: main.py
import sys
print("main: start")
import config
import config
from config import APP_NAME
print("main:", APP_NAME)
print("config" in sys.modules)
Три імпорти — одне повідомлення config: loading. Тому модуль може безпечно імпортуватися з десяти різних файлів: усі вони отримають той самий обʼєкт.
Байткод у __pycache__
Після першого імпорту поруч із вашими модулями зʼявиться каталог __pycache__ з файлами grades.cpython-313.pyc — це скомпільований байткод (лекція 01). Він пришвидшує наступні запуски і створюється тільки для імпортованих модулів, а не для файлу, який ви запускаєте. Каталог має бути в .gitignore (лекція 12).
Зміни в модулі не підхоплюються «на льоту»
Якщо ви в інтерактивному python3 зробили import grades, а потім змінили grades.py, повторний import grades нічого не змінить — модуль уже в sys.modules. Перезапустіть інтерпретатор.
__name__ і if __name__ == "__main__":¶
Кожен модуль має змінну __name__. Її значення залежить від того, як використовується файл:
- файл запустили командою
python3 grades.py—__name__ == "__main__"; - файл імпортували (
import grades) —__name__ == "grades".
Навіщо це потрібно¶
Типова ситуація: ви написали grades.py і внизу додали кілька рядків, щоб перевірити функції.
# File: grades.py
PASS_SCORE = 60
def average(scores: list[int]) -> float:
return sum(scores) / len(scores)
print("self-check:", average([78, 92, 55]))
Тепер кожна програма, яка робить import grades, першим рядком виведе self-check: 75.0. Перевірка, яка мала виконуватися лише під час розробки, просочилася в чужий вивід. Ще гірше, якщо там був input() — імпорт «зависне» в очікуванні введення.
Розвʼязок — виконувати такий код лише тоді, коли файл запущено напряму:
# File: grades.py
PASS_SCORE = 60
def average(scores: list[int]) -> float:
return sum(scores) / len(scores)
def main() -> None:
print("self-check:", average([78, 92, 55]))
if __name__ == "__main__":
main()
python3 grades.py— виводитьself-check: 75.0;import grades— нічого не виводить, лише визначаєPASS_SCORE,averageіmain.
Шаблон для кожної програми
Відтепер оформлюйте кожну програму, яку запускають, так:
Уся логіка — у функціях, на верхньому рівні — лише імпорти, константи, визначення і цей блок. Такий файл можна і запустити, і безпечно імпортувати (наприклад, щоб протестувати його функції).from ... import створює нове імʼя, а не посилання на змінну модуля¶
Згадаймо лекцію 20: змінна — це імʼя, привʼязане до обʼєкта. from grades import PASS_SCORE створює в поточному модулі нове імʼя PASS_SCORE, яке посилається на той самий обʼєкт 60. Але це два різні імені в різних просторах імен.
# File: grades.py
PASS_SCORE = 60
def average(scores: list[int]) -> float:
return sum(scores) / len(scores)
def is_passed(score: float) -> bool:
return score >= PASS_SCORE
# File: main.py
import grades
from grades import PASS_SCORE, is_passed
PASS_SCORE = 50
print("local PASS_SCORE:", PASS_SCORE)
print("grades.PASS_SCORE:", grades.PASS_SCORE)
print("is_passed(55):", is_passed(55))
grades.PASS_SCORE = 50
print("is_passed(55):", is_passed(55))
PASS_SCORE = 50уmain.pyзмінило лише локальне імʼя. Функціяis_passedбереPASS_SCOREзі свого модуля, де досі60.grades.PASS_SCORE = 50змінило змінну в самому модулі, іis_passedце побачила.
Не змінюйте змінні чужих модулів
Те, що grades.PASS_SCORE = 50 працює, не означає, що так варто робити: будь-який модуль може непомітно змінити поведінку іншого, і помилку буде дуже важко знайти. Якщо значення має налаштовуватися — передайте його параметром функції: is_passed(score, pass_score=50). Константи модуля (імена ВЕЛИКИМИ літерами) — тільки для читання.
Де Python шукає модулі: sys.path¶
Коли модуля ще немає в sys.modules, Python шукає його:
- серед вбудованих модулів, скомпільованих усередину інтерпретатора (
sys,builtinsта деякі інші); - у каталогах зі списку
sys.path— по черзі, до першого збігу.
'/home/student/project'
'/usr/lib/python313.zip'
'/usr/lib/python3.13'
'/usr/lib/python3.13/lib-dynload'
'/usr/local/lib/python3.13/dist-packages'
'/usr/lib/python3/dist-packages'
Елемент sys.path |
Що там |
|---|---|
| перший елемент | каталог запущеного скрипта (для python3 -m ... — поточний каталог) |
вміст змінної оточення PYTHONPATH |
додаткові каталоги, якщо змінну задано |
/usr/lib/python3.13 |
стандартна бібліотека |
.../site-packages або .../dist-packages |
сторонні пакети, встановлені через pip або apt |
Вивід на вашому компʼютері відрізнятиметься.
Звідси важливі наслідки:
- Модуль знаходиться відносно запущеного скрипта, а не відносно поточного каталогу терміналу.
python3 project/main.pyзнайдеproject/grades.py. - Якщо модуль не знайдено в жодному каталозі —
ModuleNotFoundError:
Traceback (most recent call last):
File "<string>", line 1, in <module>
import nosuchmodule
ModuleNotFoundError: No module named 'nosuchmodule'
- Якщо модуль знайдено, але в ньому немає потрібного імені —
ImportError:
Traceback (most recent call last):
File "<string>", line 1, in <module>
from math import squareroot
ImportError: cannot import name 'squareroot' from 'math' (unknown location)
Не змінюйте sys.path вручну
В інтернеті часто радять sys.path.append(".."), щоб «побачити» модуль з іншого каталогу. Це крихкий прийом: він залежить від того, звідки запускають програму, і ламається на чужому компʼютері. Правильний розвʼязок — нормальна структура проєкту і запуск через python3 -m (розділи нижче).
Затінення стандартних модулів¶
Каталог скрипта стоїть у sys.path першим, раніше за стандартну бібліотеку. Тож якщо назвати свій файл так само, як стандартний модуль, Python імпортує ваш файл.
my random.py
Traceback (most recent call last):
File "/home/student/project/main.py", line 3, in <module>
print(random.randint(1, 6))
^^^^^^^^^^^^^^
AttributeError: module 'random' has no attribute 'randint' (consider renaming '/home/student/project/random.py' since it has the same name as the standard library module named 'random' and prevents importing that standard library module)
Python 3.13 вже підказує причину, у старіших версіях буде лише AttributeError: module 'random' has no attribute 'randint'. Найчастіші «жертви»: random.py, math.py, string.py, logging.py, json.py, csv.py, test.py, statistics.py — студенти люблять називати файли за темою заняття.
Як виправити
- Перейменуйте свій файл (
random_practice.py). - Видаліть каталог
__pycache__поруч із ним, щоб не лишилося старого байткоду. - Перевірити, який файл насправді імпортується, можна так:
print(random.__file__).
Стандартна бібліотека¶
Разом з Python встановлюються сотні модулів — стандартна бібліотека. Перш ніж писати щось своє чи встановлювати сторонній пакет, перевірте, чи немає готового розвʼязку в ній.
| Модуль | Для чого | Приклад |
|---|---|---|
math |
математичні функції | math.sqrt(2), math.pi |
random |
випадкові числа | random.randint(1, 6), random.choice(names) |
statistics |
середнє, медіана, відхилення | statistics.median(scores) |
datetime |
дати і час | datetime.now(), date(2026, 10, 6) |
time |
пауза, вимірювання часу | time.sleep(1), time.perf_counter() |
pathlib |
шляхи до файлів (лекція 21) | Path("data") / "grades.txt" |
os |
взаємодія з ОС, змінні оточення | os.environ.get("HOME") |
sys |
аргументи, вихід, інтерпретатор | sys.argv, sys.exit(1) |
json, csv |
читання і запис форматів даних | json.load(file) |
collections |
Counter, defaultdict, deque (лекція 25) |
Counter(words) |
itertools |
комбінації і перестановки, зручні цикли | itertools.combinations(names, 2) |
re |
регулярні вирази | re.fullmatch(r"[A-Z]{2}-\d{2}", group) |
argparse |
розбір аргументів командного рядка | parser.add_argument("--debug") |
logging |
логування (лекція 26) | logging.getLogger(__name__) |
typing |
типи для анотацій (лекція 20) | from typing import Final |
# Program: a few standard library modules together
import random
import statistics
from datetime import date
scores = [random.randint(50, 100) for _ in range(5)]
print("today:", date.today().isoformat())
print("scores:", scores)
print("mean:", statistics.mean(scores))
print("median:", statistics.median(scores))
Повний перелік — у документації стандартної бібліотеки.
Пакети: каталог з __init__.py¶
Коли модулів стає багато, їх групують у пакети (packages). Пакет — це каталог з модулями та файлом __init__.py. Пакети можуть містити інші пакети (підпакети).
school — це пакет, school.grades і school.students — модулі в ньому. Імʼя модуля з пакета пишеться через крапку, як шлях: school.grades відповідає файлу school/grades.py.
Усі форми імпорту працюють і з пакетами:
import school.grades # use: school.grades.average(...)
from school import grades # use: grades.average(...)
from school.grades import average # use: average(...)
Створімо пакет і перевіримо:
Що таке __init__.py¶
- Його наявність позначає каталог як звичайний пакет.
- Він виконується під час першого імпорту пакета (або будь-якого модуля з нього) — так само, як звичайний модуль.
- Усе, що в ньому визначено, стає атрибутами пакета:
import schoolдає доступ до імен зschool/__init__.py.
Найчастіше __init__.py залишають порожнім або з рядком документації. Іноді в ньому «піднімають» найважливіші імена нагору, щоб користувачам пакета не треба було знати, в якому модулі вони лежать:
# File: school/__init__.py
"""School: utilities for working with student grades."""
from .grades import average
__all__ = ["average"]
Тепер працює і коротша форма: from school import average.
А без __init__.py?
З Python 3.3 каталог без __init__.py теж можна імпортувати — це так званий namespace package, механізм для складних випадків, коли один пакет розкидано по кількох каталогах. Для власних проєктів завжди створюйте __init__.py, навіть порожній: так поведінка передбачувана, і інструменти (mypy, редактор, тести) працюють без сюрпризів.
Не тримайте важкий код у __init__.py
__init__.py виконується при будь-якому імпорті з пакета. Читання файлів, мережеві запити чи print() у ньому сповільнюють і засмічують кожен імпорт.
Абсолютний і відносний імпорт¶
Усередині пакета модулі часто імпортують один одного. Є два способи вказати, що саме імпортувати.
Абсолютний імпорт — повний шлях від кореня проєкту:
Відносний імпорт — шлях відносно поточного пакета, починається з крапки:
# File: school/students.py
from .grades import average # module grades from the same package
from . import grades # the same package itself
| Запис | Означає |
|---|---|
from .grades import average |
модуль grades з того ж пакета |
from . import grades |
модуль grades з того ж пакета (як обʼєкт) |
from ..config import APP_NAME |
модуль config з батьківського пакета |
Обидва способи правильні. PEP 8 рекомендує абсолютний як очевидніший, а відносний доречний для імпортів усередині одного пакета: якщо пакет перейменують, відносні імпорти не доведеться змінювати.
Форма import .grades не існує
Відносний імпорт можливий тільки у формі from . import ... або from .module import ....
Головна пастка відносних імпортів¶
Відносний імпорт працює лише в модулі, який є частиною пакета. Файл, запущений напряму, пакета «не знає»:
Traceback (most recent call last):
File "/home/student/project/school/students.py", line 1, in <module>
from .grades import average
ImportError: attempted relative import with no known parent package
Під час запуску python3 school/students.py Python бачить просто файл, його __name__ — "__main__", і він не входить у жоден пакет. Тому «крапка» ні на що не вказує. Розвʼязок — наступний розділ.
Запуск модуля з пакета: python3 -m¶
Ключ -m запускає модуль за його іменем, а не файл за шляхом:
Що змінюється порівняно з python3 school/students.py:
python3 school/students.py |
python3 -m school.students |
|
|---|---|---|
| Що вказуємо | шлях до файлу | імʼя модуля через крапки |
sys.path[0] |
каталог school/ |
поточний каталог (project/) |
| Модуль знає свій пакет | ні | так |
| Відносні імпорти | ImportError |
працюють |
Абсолютні from school... |
ModuleNotFoundError |
працюють |
Отже, python3 -m треба запускати з кореневого каталогу проєкту — того, де лежить каталог пакета.
Ви вже бачили -m: python3 -m venv env, python3 -m pip install ... — це запуск модулів venv і pip зі стандартної бібліотеки та site-packages.
__main__.py — пакет, який можна запустити¶
Якщо в пакеті є файл __main__.py, пакет можна запустити за іменем:
# File: school/__main__.py
from .grades import average
def main() -> None:
print("average:", average([78, 92, 55]))
if __name__ == "__main__":
main()
Так зазвичай оформлюють точку входу програми, що складається з пакета: python3 -m gradebook.
Циклічні імпорти¶
Циклічний імпорт виникає, коли модуль A імпортує модуль B, а B — модуль A (напряму або через ланцюжок).
# File: students.py
from grades import average
STUDENTS = {"Shevchenko": [78, 92, 55]}
def report() -> None:
for name, scores in STUDENTS.items():
print(name, average(scores))
# File: grades.py
from students import STUDENTS
def average(scores: list[int]) -> float:
return sum(scores) / len(scores)
def best_student() -> str:
return max(STUDENTS, key=lambda name: average(STUDENTS[name]))
Traceback (most recent call last):
File "/home/student/project/main.py", line 1, in <module>
import students
File "/home/student/project/students.py", line 1, in <module>
from grades import average
File "/home/student/project/grades.py", line 1, in <module>
from students import STUDENTS
ImportError: cannot import name 'STUDENTS' from 'students' (consider renaming '/home/student/project/students.py' if it has the same name as a library you intended to import)
Що сталося (згадайте схему імпорту):
main.pyімпортуєstudents. Python створює порожній модульstudents, кладе його вsys.modulesі починає виконуватиstudents.py.- Перший рядок
students.pyімпортуєgrades. Починається виконанняgrades.py. - Перший рядок
grades.pyімпортуєSTUDENTSзstudents. Модульstudentsуже є вsys.modules, але він виконаний лише до першого рядка —STUDENTSще не визначено.
У старіших версіях Python повідомлення прямо називає причину: cannot import name 'STUDENTS' from partially initialized module 'students' (most likely due to a circular import). У Python 3.13 підказка про перейменування тут вводить в оману — дивіться на traceback: ланцюжок students → grades → students і є ознакою циклу.
Порядок і стиль імпортів¶
PEP 8 і загальна практика встановлюють такі правила:
- Усі імпорти — на початку файлу, після рядка документації модуля.
- Імпорти групують у три блоки, розділені порожнім рядком:
- стандартна бібліотека;
- сторонні пакети (встановлені окремо, про них — у наступних лекціях);
- власні модулі проєкту.
- Усередині блоку — за алфавітом.
- Один модуль — один рядок
import:import osіimport sysокремо, а неimport os, sys. Дляfrom ... import a, bкілька імен в одному рядку — нормально. - Жодного
import *.
"""Entry point of the gradebook application."""
import logging
import sys
from pathlib import Path
from gradebook.grades import average
from gradebook.storage import load_scores
Автоматичне сортування
Редактор VS Code з розширенням Python вміє впорядковувати імпорти командою Organize Imports (Shift+Alt+O). Інструменти isort або ruff роблять це з командного рядка.
Структура проєкту¶
Немає єдиного «правильного» способу організувати проєкт, але є загальноприйняті домовленості. Для невеликого застосунку, який ви пишете в курсі, рекомендуємо таку структуру:
gradebook-project/ <- project root, git repository, run commands from here
├── .gitignore
├── README.md <- what the program does and how to run it
├── data/ <- input files
│ └── grades.txt
└── gradebook/ <- the package: all the code
├── __init__.py
├── __main__.py <- entry point: python3 -m gradebook
├── grades.py <- calculations
├── storage.py <- reading and writing files
└── report.py <- building output
Принципи, на яких вона побудована:
- Один модуль — одна відповідальність.
grades.pyлише обчислює,storage.pyлише читає файли,report.pyлише формує текст. Щоб змінити формат звіту, відкриваємо тількиreport.py. - Обчислення без вводу/виводу. Функції в
grades.pyотримують дані параметрами і повертають результат — жоднихprint(),input(),open(). Такі функції легко перевіряти і використовувати повторно. - Тонка точка входу.
__main__.pyлише налаштовує логування, викликає інші модулі і повертає код завершення. Логіки в ньому мінімум. - Залежності йдуть в один бік:
__main__→report,storage→grades. Нижчі модулі нічого не знають про вищі — тож модулі не імпортують один одного по колу. - Код — у пакеті, дані — окремо. Шлях до даних обчислюється від розташування коду (
Path(__file__)), а не від поточного каталогу терміналу. - Команди запускаються з кореня проєкту:
python3 -m gradebook.
.gitignore для такого проєкту (див. лекцію 12):
Більші проєкти: pyproject.toml і src-layout
Бібліотеки, які публікують на PyPI, зазвичай мають файл pyproject.toml (назва, версія, залежності, налаштування інструментів) і кладуть пакет у підкаталог src/ (src/gradebook/). Крім того, поруч із пакетом зазвичай є каталог tests/ з автоматичними тестами. Для навчальних проєктів у цьому курсі достатньо структури вище.
Приклад: проєкт «журнал оцінок»¶
Зберемо все разом: прочитаємо оцінки студентів з файлу, обчислимо середні бали і виведемо звіт, з логуванням з лекції 26. Створіть структуру з попереднього розділу.
# File: data/grades.txt
Shevchenko;78,92,55
Kovalenko;95,88,100
Bondarenko;40,52,61
Melnyk;90,abc,70
Tkachenko
Останні два рядки зіпсовано навмисно.
# File: gradebook/__init__.py
"""Gradebook: read student scores from a file and print a report."""
from .grades import average, is_passed
__all__ = ["average", "is_passed"]
# File: gradebook/grades.py
"""Calculations with scores. No input/output here."""
PASS_SCORE = 60
def average(scores: list[int]) -> float:
return sum(scores) / len(scores)
def is_passed(score: float) -> bool:
return score >= PASS_SCORE
def letter(score: float) -> str:
if score >= 90:
return "A"
if score >= 75:
return "B"
if score >= PASS_SCORE:
return "C"
return "F"
# File: gradebook/storage.py
"""Reading scores from a text file."""
import logging
from pathlib import Path
logger = logging.getLogger(__name__)
def load_scores(path: Path) -> dict[str, list[int]]:
result: dict[str, list[int]] = {}
with path.open(encoding="utf-8") as file:
for number, line in enumerate(file, start=1):
line = line.strip()
if not line:
continue
try:
name, raw_scores = line.split(";")
scores = [int(item) for item in raw_scores.split(",")]
except ValueError:
logger.warning("line %d skipped: %r", number, line)
continue
result[name] = scores
logger.info("loaded %d students from %s", len(result), path.name)
return result
# File: gradebook/report.py
"""Building a text report."""
from .grades import average, is_passed, letter
def build_report(data: dict[str, list[int]]) -> list[str]:
lines = []
for name, scores in sorted(data.items()):
avg = average(scores)
status = "passed" if is_passed(avg) else "failed"
lines.append(f"{name:<12} {avg:6.1f} {letter(avg)} {status}")
return lines
# File: gradebook/__main__.py
"""Entry point: python3 -m gradebook"""
import logging
import sys
from pathlib import Path
from .report import build_report
from .storage import load_scores
DATA_FILE = Path(__file__).parent.parent / "data" / "grades.txt"
logger = logging.getLogger(__name__)
def main() -> int:
logging.basicConfig(
level=logging.INFO,
format="%(levelname)-7s %(name)s: %(message)s",
)
try:
data = load_scores(DATA_FILE)
except FileNotFoundError:
logger.error("data file not found: %s", DATA_FILE)
return 1
for line in build_report(data):
print(line)
return 0
if __name__ == "__main__":
sys.exit(main())
Запуск з кореня проєкту:
WARNING gradebook.storage: line 4 skipped: 'Melnyk;90,abc,70'
WARNING gradebook.storage: line 5 skipped: 'Tkachenko'
INFO gradebook.storage: loaded 3 students from grades.txt
Bondarenko 51.0 F failed
Kovalenko 94.3 A passed
Shevchenko 75.0 B passed
Зверніть увагу:
%(name)sу лозі — це__name__модуля. Одразу видно, що попередження прийшли зgradebook.storage, — саме для цього в лекції 26 ми писалиgetLogger(__name__).Path(__file__).parent.parent—__file__у__main__.pyдорівнює.../gradebook-project/gradebook/__main__.py; перший.parent— каталог пакета, другий — корінь проєкту. Програма знайде дані незалежно від того, з якого каталогу запущено термінал, якщо пакет видно вsys.path.sys.exit(main())— код завершення0або1(лекція 23) повертається операційній системі.- Пакет можна використовувати як бібліотеку:
from gradebook import averageпрацює завдяки__init__.py.
Що буде, якщо запускати неправильно:
| Команда | Результат |
|---|---|
python3 -m gradebook з кореня проєкту |
працює |
python3 -m gradebook з іншого каталогу |
No module named gradebook — пакета немає в sys.path |
python3 gradebook/__main__.py |
ImportError: attempted relative import with no known parent package |
Типові помилки¶
| Помилка | Чому погано | Як правильно |
|---|---|---|
Файл названо random.py, math.py, logging.py… |
затінює стандартний модуль: AttributeError у неочікуваному місці |
унікальні імена; перевірка через module.__file__ |
| Імʼя модуля з дефісом або з цифри на початку | такий файл неможливо імпортувати | lowercase_with_underscores |
print(), input() або тестовий код на верхньому рівні модуля |
виконується при кожному імпорті | код — у функціях; запуск — під if __name__ == "__main__": |
from module import * |
незрозуміло, звідки імʼя; тихе перезаписування змінних | import module або явний перелік імен |
Змінено PASS_SCORE після from grades import PASS_SCORE і очікується зміна в модулі |
змінено лише локальне імʼя | передавати значення параметром |
| Змінні чужого модуля змінюються ззовні | неочевидні побічні ефекти | константи — тільки для читання |
sys.path.append("..") |
працює лише з певного каталогу | правильна структура + python3 -m |
| Відносний імпорт у файлі, який запускають напряму | ImportError: ... no known parent package |
python3 -m package.module з кореня проєкту |
python3 -m з неправильного каталогу |
ModuleNotFoundError |
запускати з каталогу, де лежить пакет |
| Модулі імпортують один одного | ImportError через частково ініціалізований модуль |
залежності між модулями мають іти в один бік |
| Шлях до даних відносно поточного каталогу терміналу | програма працює лише з одного місця | Path(__file__).parent / ... |
Зміни в модулі не видно в інтерактивному python3 |
модуль уже в sys.modules |
перезапустити інтерпретатор |
__pycache__/ у git |
зайві файли, привʼязані до версії Python | .gitignore |
Підсумок¶
- Модуль — файл
.py; його імʼя — імʼя файлу без розширення. Модуль має власний простір імен. - Форми імпорту:
import m,import m as x,from m import name,from m import name as x. За замовчуванням —import m;import *не використовуємо. - Під час першого імпорту код модуля виконується повністю, один раз; результат зберігається в
sys.modules. __name__дорівнює"__main__"для запущеного файлу та імені модуля для імпортованого. Код запуску — підif __name__ == "__main__":.from m import nameстворює нове імʼя в поточному модулі; присвоєння йому не змінюєm.name.- Модулі шукаються у
sys.path: каталог скрипта, стандартна бібліотека, site-packages. Свій файл з іменем стандартного модуля його затінює. - Пакет — каталог з
__init__.py; модулі в ньому —package.module.__init__.pyвиконується при імпорті пакета. - Абсолютний імпорт —
from package.module import name; відносний —from .module import name, працює лише всередині пакета. python3 -m package.moduleзапускає модуль як частину пакета;__main__.pyдозволяє запускатиpython3 -m package.- Циклічний імпорт (модулі імпортують один одного) призводить до
ImportError: один із модулів виконаний лише частково. - Імпорти — на початку файлу, групами: стандартна бібліотека, сторонні, власні.
- Структура проєкту: пакет з модулями за відповідальностями, тонка точка входу, дані окремо від коду, запуск з кореня проєкту.
Корисні посилання¶
- Tutorial: Modules — офіційний посібник
- The import system — як працює імпорт
__main__— модуль верхнього рівня і__main__.pysys.path— шлях пошуку модулів- The Python Standard Library — перелік модулів
- PEP 8: Imports — стиль імпортів
Домашнє завдання¶
Мета — навчитися розбивати програму на модулі й пакети, правильно імпортувати і запускати їх. Усі дані — ваші власні, латиницею. Для кожного завдання покажіть дерево файлів (find . -name "*.py" | sort або tree), вміст файлів і вивід запуску.
-
Створіть модуль
about_me.pyз константамиFIRST_NAME,LAST_NAME,GROUP,BIRTH_YEAR(ваші дані) і функціямиfull_name() -> strтаage(current_year: int) -> int. Уmain.pyвикористайте модуль трьома способами:import about_me,import about_me as me,from about_me import full_name, age. Виведітьdir(about_me)без службових імен іabout_me.__file__. -
Додайте в
about_me.pyрядокprint("about_me loaded")на верхньому рівні. Імпортуйте модуль уmain.pyтричі різними формами і покажіть, скільки разів зʼявилося повідомлення. Поясніть результат черезsys.modules. Потім приберітьprint, оформіть самоперевірку модуля черезmain()іif __name__ == "__main__":і покажіть вивідpython3 about_me.pyтаpython3 main.py. -
Створіть у каталозі файл
random.py(абоstatistics.py) з однією функцією. Напишіть поручmain.py, який використовує відповідний стандартний модуль, наприклад, обирає випадкове число від 1 до кількості літер у вашому прізвищі. Покажіть помилку, поясніть її, виведіть__file__імпортованого модуля, виправте і покажіть правильний вивід. -
Продемонструйте різницю між
import grades+grades.X = ...іfrom grades import X+X = .... У модуліlimits.pyзадайтеMAX_ABSENCESрівним номеру вашого дня народження і функціюis_allowed(absences: int) -> bool. Покажіть вивід, поясніть, чому результати різні, і перепишіть функцію так, щоб межа передавалася параметром. -
Створіть пакет
myschoolз модулямиgrades.py(обчислення) іschedule.py(ваш розклад на тиждень у вигляді словника та функція, що повертає пари на заданий день). Уschedule.pyвикористайте відносний імпорт зgrades.py. Покажіть: (а) запускpython3 myschool/schedule.pyі помилку; (б) запускpython3 -m myschool.scheduleз кореня проєкту; (в) запускpython3 -m myschool.scheduleз каталогуmyschool/. Поясніть кожен результат черезsys.path[0]. -
Додайте до пакета
myschoolфайл__main__.py, щобpython3 -m myschoolвиводив ваше імʼя, групу і розклад на сьогоднішній день тижня (datetime.date.today().weekday()). У__init__.py«підніміть» дві найважливіші функції і задайте__all__. Покажіть, щоfrom myschool import <function>працює з окремого скрипта в корені проєкту. -
Створіть два модулі, які імпортують один одного (наприклад,
students.pyз вашими даними та даними 3–4 одногрупників іstats.pyз обчисленнями). Отримайте помилку циклічного імпорту, збережіть traceback і поясніть по кроках, що сталося. Намалюйте схему імпортів між модулями (можна mermaid) і позначте на ній, де виникає цикл. -
Розбийте програму з домашнього завдання лекції 26 (або 23), яка читала файл і логувала пропущені рядки, на пакет за структурою з розділу «Структура проєкту»: окремі модулі для обчислень, читання файлу і виводу; точка входу —
__main__.py; дані — у каталозіdata/; логер у кожному модулі —getLogger(__name__). Покажіть дерево проєкту, вивідpython3 -m <your_package>(в лозі мають бути видні імена різних модулів),.gitignoreіREADME.mdз інструкцією запуску. Закомітьте проєкт у git і покажітьgit status— у ньому не має бути__pycache__. -
Знайдіть у своєму коді з попередніх лабораторних щонайменше три функції, які ви копіювали з однієї програми в іншу. Винесіть їх в окремий модуль (або пакет)
utilsі перепишіть дві старі програми так, щоб вони імпортували функції звідти. Покажітьgit diffдо і після та поясніть, які проблеми з розділу «Навіщо ділити програму на файли» це вирішує.
Знайшли помилку чи бажаєте додати інформацію, щоб покращити курс? Створіть issue на GitHub