Урок 13. Модулі та стандартна бібліотека¶
В уроці 8 звіт кафе розклали на функції. Відтоді кафе підключило касу, і тепер вона віддає чеки не з готовими «пт» і «вечеря», а з міткою часу: 2024-07-19 18:30. Власниця хоче звіт за будь-який місяць, а адміністратор — запускати його сам, з термінала, без ноутбука.
Ноутбук з усім кодом уже на кілька екранів: генерація чеків, правила «яка година — який прийом їжі», функції звіту. Щоб знайти потрібну функцію, його доводиться гортати. Щоб скористатися звітом в іншій програмі, код доведеться копіювати. У цьому уроці ми розкладемо кафе на модулі — окремі файли, які імпортують один одного. А дати, підрахунки й календар візьмемо зі стандартної бібліотеки: вона вже встановлена разом з Python.
Що потрібно з попередніх уроків: NamedTuple Order (урок 5), словники й dict.get (урок 6), функції та звіт кафе (урок 8). Ти вже писав from bisect import bisect_left (урок 12) і from functools import wraps (урок 10), тож сьогодні розберемося, що саме відбувається в цьому рядку.
Після уроку ти зможеш:
- розкладати програму на модулі й імпортувати з них функції трьома способами;
- пояснювати, де Python шукає модуль і чому файл модуля виконується лише один раз;
- захищати код перевірок у модулі через
if __name__ == "__main__":; - розпізнавати затінення (shadowing), коли твій файл має ім'я стандартного модуля;
- працювати з датами через
datetimeіtimedelta, рахувати черезCounter, користуватисяcalendar,statisticsіrandom; - приймати аргументи з командного рядка через
sys.argv.
Задача розділу. Перетворити ноутбук кафе на проєкт з чотирьох модулів, який друкує звіт за місяць командою python main.py 2024 7. Повний код — у розділі «Практика», готовий проєкт — у теці cafe_report/.
Ноутбук заняття: Відкрити вправи в Colab Переглянути розв’язки
Пригадай¶
Дай відповідь подумки, нічого не запускаючи:
- В уроці 11 ми писали
from collections import deque, а в уроці 9 —import timeі потімtime.perf_counter(). Чому в першому випадку пишемо простоdeque(...), а в другому — з префіксомtime.? - Які поля має
Orderу звіті кафе з уроку 8? print,lenіsortedми ніколи не імпортували. Звідки вони беруться?
Відповіді
from collections import dequeбере з модуля одне ім'я і кладе його в нашу програму.import timeдає лише ім'я модуля, тому до функцій звертаємося через крапку.total_bill,tip,day,time,size.- Це вбудовані (built-in) функції: вони доступні завжди, без імпорту. Усе інше, навіть зі стандартної бібліотеки, треба імпортувати.
Модуль — це файл¶
Модуль — це звичайний файл .py. Нічого особливого в ньому немає: ті самі def, class і змінні, що й у ноутбуці. Ім'я модуля — ім'я файлу без .py.
Розкладемо код кафе за ролями, як у уроці 8 розкладали звіт на функції:
cafe_report/
├── rules.py ← правила: мітка часу → день тижня, година → прийом їжі
├── orders.py ← чеки: RawOrder з каси, Order для звіту, генерація даних
├── report.py ← звіт: функції з уроку 8
└── main.py ← точка входу: python main.py 2024 7
Так модулі створює ноутбук заняття, і так працюють усі приклади до розділу «Практика». У готовій теці cafe_report/ ті самі чотири модулі розкладено ще й по пакетах — теках з модулями. Як це змінює імпорти, розберемо в «Практиці».
Найпростіший модуль — правила кафе. У ньому немає жодного імпорту:
"""Правила кафе: як мітка часу перетворюється на день тижня і прийом їжі."""
DAYS = ("пн", "вт", "ср", "чт", "пт", "сб", "нд")
MONTHS = ("", "січень", "лютий", "березень", "квітень", "травень", "червень",
"липень", "серпень", "вересень", "жовтень", "листопад", "грудень")
def day_from_timestamp(ts):
"""Короткий день тижня: datetime(2024, 7, 19, ...) -> 'пт'."""
return DAYS[ts.weekday()]
def meal_type_from_hour(hour):
"""Прийом їжі за годиною замовлення."""
if 11 <= hour <= 15:
return "обід"
if 17 <= hour <= 23:
return "вечеря"
return "інше"
Інший файл, що лежить у тій самій теці, користується ним через import:
import rules
print(rules.meal_type_from_hour(19))
print(rules.meal_type_from_hour(16))
print(rules.DAYS[4])
rules.meal_type_from_hour читається як «функція meal_type_from_hour з модуля rules». Кожен модуль — окремий простір імен (namespace): у rules.py і в report.py можуть бути змінні з однаковими іменами, і вони не заважатимуть одна одній. Префікс завжди показує, звідки ім'я.
Модулі проєкту імпортують один одного і стандартну бібліотеку. Стрілка — «імпортує»:
flowchart TD
classDef step fill:#eceff1,stroke:#546e7a,stroke-width:1px,color:#1a1a1a;
classDef decision fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#1a1a1a;
classDef success fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#1a1a1a;
classDef error fill:#ffebee,stroke:#c62828,stroke-width:3px,color:#1a1a1a;
classDef warning fill:#fff8e1,stroke:#e65100,stroke-width:2px,color:#1a1a1a;
M["main.py<br>точка входу"] --> O["orders.py<br>чеки"]
M --> R["report.py<br>звіт"]
M --> U["rules.py<br>правила"]
O --> U
R --> U
M --> S1["sys, calendar"]
O --> S2["random, datetime, typing"]
R --> S3["collections"]
class M warning
class O,R,U step
class S1,S2,S3 success
Зелені блоки — стандартна бібліотека. rules.py нікого не імпортує, тому його найлегше перевіряти: даєш годину — отримуєш рядок.
Три способи імпорту¶
| Спосіб | Як викликати | Коли зручно |
|---|---|---|
import rules |
rules.meal_type_from_hour(19) |
імен багато, важливо бачити, звідки кожне |
from rules import meal_type_from_hour |
meal_type_from_hour(19) |
одне-два імені, які часто використовуються |
import statistics as st |
st.mean(bills) |
довга назва або домовленість (import pandas as pd) |
Є й четвертий спосіб, from math import *: він забирає з модуля всі імена. Так не пишуть, бо незрозуміло, звідки взялося ім'я, а чуже ім'я може непомітно замінити твоє або вбудоване:
Вбудована pow повертала ціле число, а після import * той самий виклик дає 8.0: math.pow мовчки замінила вбудовану функцію. PEP 8 радить уникати таких імпортів.
datetime — і модуль, і клас
У модулі datetime є клас з тим самим іменем. Після import datetime ім'я datetime означає модуль, і викликати його не можна:
Підказку «Did you mean» друкує Python 3.12 і новіші.
Або datetime.datetime(2024, 7, 19), або from datetime import datetime. У цьому уроці — другий варіант.
Порядок імпортів за PEP 8 — на початку файлу, трьома групами через порожній рядок: стандартна бібліотека, сторонні пакети (pip install), модулі твого проєкту. Так на початку orders.py:
import random
from datetime import datetime, timedelta
from typing import NamedTuple
from rules import day_from_timestamp, meal_type_from_hour
Що робить import¶
Коли Python бачить import rules, він виконує чотири кроки:
flowchart TD
classDef step fill:#eceff1,stroke:#546e7a,stroke-width:1px,color:#1a1a1a;
classDef decision fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#1a1a1a;
classDef success fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#1a1a1a;
classDef error fill:#ffebee,stroke:#c62828,stroke-width:3px,color:#1a1a1a;
classDef warning fill:#fff8e1,stroke:#e65100,stroke-width:2px,color:#1a1a1a;
A["import rules"] --> B{"rules уже<br>в sys.modules?"}
B -- так --> F["взяти готовий модуль<br>файл не виконується"]
B -- ні --> C["шукати rules.py<br>по теках sys.path"]
C --> D{"знайшли?"}
D -- ні --> E["ModuleNotFoundError"]
D -- так --> G["виконати файл<br>зверху вниз"]
G --> H["зберегти в sys.modules"]
H --> I["ім'я rules<br>з'являється в програмі"]
F --> I
class A,C,G,H step
class B,D decision
class E error
class F,I success
- Кеш. Якщо модуль уже імпортували, Python бере його зі словника
sys.modulesі нічого не виконує повторно. - Пошук. Інакше перебирає теки зі списку
sys.pathпо черзі, доки не знайдеrules.py. - Виконання. Виконує файл модуля зверху вниз, як звичайну програму:
defстворюють функції, присвоєння — змінні,printдрукує. - Збереження. Кладе готовий модуль у
sys.modules, а в програмі з'являється ім'яrules.
Кеш легко побачити:
import sys
import collections
print("collections" in sys.modules)
print(type(sys.modules["collections"]))
Де Python шукає: sys.path¶
sys.path — звичайний список тек. Порядок пошуку:
- тека скрипта, який запустили (
python main.py— тека, де лежитьmain.py); у ноутбуці — поточна тека; - теки зі змінної оточення
PYTHONPATH, якщо вона задана; - стандартна бібліотека;
site-packages— пакети, встановлені черезpip.
Перша ж тека, де знайшовся файл, перемагає. Тому main.py бачить rules.py, що лежить поруч, без жодних налаштувань. І тому ж з'являється пастка, про яку далі.
Виконання лише один раз¶
Файл модуля виконується при першому імпорті. Усе, що стоїть на верхньому рівні файлу, спрацює в цей момент — навіть якщо імпортуючий хотів лише одну функцію.
Коли report.py переносили з ноутбука, в кінець файлу за звичкою потрапила перевірка з уроку 8:
orders = [
Order(540.0, 50.0, "пт", "вечеря", 2),
Order(320.0, 30.0, "пт", "обід", 1),
Order(980.0, 120.0, "сб", "вечеря", 4),
]
print_report(orders)
Адміністратор запускає звіт за липень і бачить два звіти. Перший — з трьох тестових чеків:
пт — чеків: 2, виторг: 860.00, середній: 430.00
сб — чеків: 1, виторг: 980.00, середній: 980.00
Найкращий день: сб
За прийомом їжі: {'вечеря': 2, 'обід': 1}
Звіт кафе: липень 2024, днів: 31, чеків: 86
пн — чеків: 17, виторг: 14561.67, середній: 856.57
...
main.py виконав from report import print_report → Python виконав report.py зверху вниз → разом з функціями виконався і тестовий print_report(orders). Це побічний ефект імпорту. А от другий import report у тій самій програмі вже нічого не надрукує: модуль у кеші.
if __name__ == "__main__":¶
Тестовий звіт у report.py корисний: ним зручно перевірити модуль окремо, командою python report.py. Потрібно, щоб він спрацьовував лише тоді, коли файл запускають напряму, а не імпортують.
Для цього в кожного модуля є змінна __name__:
| Як використали файл | Значення __name__ всередині нього |
|---|---|
python report.py — запустили напряму |
"__main__" |
import report з іншого файлу |
"report" — ім'я модуля |
Отже, тестовий код ховаємо під умову:
if __name__ == "__main__":
from orders import Order
demo = [
Order(540.0, 50.0, "пт", "вечеря", 2),
Order(320.0, 30.0, "пт", "обід", 1),
Order(980.0, 120.0, "сб", "вечеря", 4),
]
print_report(demo)
Тепер python report.py друкує тестовий звіт, а python main.py 2024 7 — лише звіт за липень. Так само закінчується і main.py: main(sys.argv) стоїть під тією ж умовою, тож main.py можна імпортувати в тести, і звіт при цьому не запуститься.
У ноутбуці __name__ завжди "__main__": ноутбук — це програма, яку запускають напряму.
Модуль без побічних ефектів
На верхньому рівні модуля лишай тільки імпорти, константи (DAYS), def і class. Усе, що друкує, читає файли чи довго рахує, — у функції або під if __name__ == "__main__":. Тоді import нічого не робить, крім того, що дає імена.
Затінення: коли твій файл заважає стандартному¶
Адміністраторка кафе вирішила зберігати графік змін у тій самій теці і назвала файл логічно — calendar.py:
Наступний запуск звіту падає:
Traceback (most recent call last):
...
days_in_month = calendar.monthrange(year, month)[1]
^^^^^^^^^^^^^^^^^^^
AttributeError: module 'calendar' has no attribute 'monthrange'
main.py пише import calendar, маючи на увазі стандартний модуль. Але першою в sys.path стоїть тека скрипта, а там тепер лежить calendar.py з графіком. Python знайшов його і далі не шукав. Стандартний calendar затінено (shadowing).
Python 3.13 підказує прямо:
AttributeError: module 'calendar' has no attribute 'monthrange' (consider renaming '.../cafe_report/calendar.py' since it has the same name as the standard library module named 'calendar' and prevents importing that standard library module)
Старіші версії такої підказки не дають. Ліки однакові: перейменувати свій файл, наприклад на staff_schedule.py, і видалити теку __pycache__ поруч, якщо вона з'явилася.
Не називай файли іменами стандартних модулів
random.py, math.py, calendar.py, statistics.py, test.py, email.py — усе це імена модулів стандартної бібліотеки. Помилка при цьому з'являється не у твоєму файлі, а в чужому коді, який чекав на справжній модуль. Швидка перевірка: print(calendar.__file__) показує, який саме файл завантажено.
Стандартна бібліотека: батарейки в комплекті¶
Python постачається з сотнями готових модулів. Про це кажуть «batteries included» — батарейки в комплекті. Перш ніж писати щось самому чи шукати пакет для pip install, варто перевірити перелік модулів. Кафе знадобиться п'ять.
datetime: дата і час одним об'єктом¶
Каса віддає мітку часу як об'єкт datetime. З нього дістаємо все, що потрібно звіту:
from datetime import datetime, timedelta
ts = datetime(2024, 7, 19, 18, 30)
print(ts)
print(ts.year, ts.month, ts.day, ts.hour, ts.minute)
print(ts.weekday())
weekday() рахує дні з нуля: 0 — понеділок, 6 — неділя. Тому rules.DAYS[ts.weekday()] — це "пт".
Рядок ↔ дата. strftime (string from time) перетворює дату на рядок за шаблоном, strptime (string parse time) — навпаки:
print(ts.strftime("%d.%m.%Y %H:%M"))
print(ts.strftime("%Y-%m"))
parsed = datetime.strptime("15.07.2024", "%d.%m.%Y")
print(parsed)
Проміжки часу. timedelta — «скільки часу»: його додають до дати, а різниця двох дат теж дає timedelta. Дати порівнюються, як числа:
print(ts + timedelta(hours=2))
period = datetime(2025, 1, 1) - datetime(2023, 1, 1)
print(period.days)
print(datetime(2024, 7, 1) <= ts < datetime(2024, 8, 1))
731 день, бо 2024 рік високосний. Саме так orders.py генерує мітки часу: початок періоду плюс випадкова кількість днів, годин і хвилин.
Назви днів і місяців англійською
ts.strftime("%A") дасть Friday: назви залежать від локалі системи. Модуль locale вміє перемкнути її на українську, але лише якщо українська локаль встановлена на конкретному комп'ютері. У Colab чи на сервері її може не бути, і locale.setlocale впаде з locale.Error. Тому кафе тримає власні кортежі DAYS і MONTHS у rules.py: вони працюють однаково всюди.
random: відтворювані випадкові дані¶
Справжньої каси в нас немає, тому чеки генеруємо. Щоб звіт щоразу виходив однаковим, генератор створюємо з зерном (seed):
import random
rng = random.Random(42)
print(rng.randint(1, 6), rng.randint(1, 6), rng.randint(1, 6))
rng = random.Random(42)
print(rng.randint(1, 6), rng.randint(1, 6), rng.randint(1, 6))
Те саме зерно дає ту саму послідовність. Числа лише псевдовипадкові: їх обчислює формула. Для навчальних даних і тестів це саме те, що треба, для паролів — ні (для них є модуль secrets).
random.Random(seed) — окремий генератор, який належить лише функції generate_raw_orders. Виклик random.seed(42) змінив би спільний генератор для всієї програми, а це побічний ефект.
collections.Counter: підрахунок одним рядком¶
В уроці 6 чеки за днями рахували вручну: counts[day] = counts.get(day, 0) + 1. Counter робить те саме одним рядком:
from collections import Counter
times = ["вечеря", "обід", "вечеря", "інше", "вечеря", "обід"]
counts = Counter(times)
print(counts)
print(counts["вечеря"], counts["сніданок"])
print(counts.most_common(1))
Counter — це словник, тож усе, що вміє dict, вміє і він. Відсутній ключ дає 0, а не KeyError. most_common(k) повертає k найчастіших значень. У report.py функція count_by_day тепер займає один рядок: return Counter(order.day for order in orders).
calendar: скільки днів у місяці¶
Щоб порахувати «чеків на день», треба знати, скільки днів у місяці. Для лютого це залежить від року:
import calendar
first_weekday, days = calendar.monthrange(2024, 2)
print(days, first_weekday == 3)
print(calendar.monthrange(2023, 2)[1])
print(calendar.isleap(2024))
print(calendar.month_name[7], repr(calendar.month_name[0]))
monthrange повертає пару: день тижня першого числа (3 — четвер, нумерація як у weekday()) і кількість днів. month_name нумерує місяці з 1, тому під індексом 0 — порожній рядок. Кортеж MONTHS у rules.py влаштований так само: MONTHS[7] == "липень".
statistics: середнє і медіана¶
import statistics as st
bills = [320, 540, 760, 980, 4500]
print(st.mean(bills))
print(st.median(bills))
Один банкет на 4500 грн підняв середній чек до 1420, хоча чотири з п'яти чеків менші за тисячу. Медіана — середнє значення за порядком — такому викиду не піддається. Коли власниця питає «скільки зазвичай витрачає гість», чесніша відповідь — медіана.
Як знайти потрібний модуль¶
- Перелік модулів з описами: The Python Standard Library.
- Що є в модулі:
dir(module)іhelp(module.function)прямо в інтерпретаторі. - Який файл завантажено:
module.__file__.
import rules
print([name for name in dir(rules) if not name.startswith("_")])
print(rules.meal_type_from_hour.__doc__)
Рядок документації (docstring), який ти пишеш під def, — це саме те, що покаже help().
sys.argv: аргументи з командного рядка¶
Адміністратор запускає звіт так:
Усе, що написано після python, Python кладе в список sys.argv:
| Індекс | Значення | Тип |
|---|---|---|
sys.argv[0] |
"main.py" — ім'я скрипта |
str |
sys.argv[1] |
"2024" |
str |
sys.argv[2] |
"7" |
str |
Усі аргументи — рядки, навіть якщо схожі на числа. Тому main.py перетворює їх через int():
"""Звіт кафе за місяць: python main.py РІК МІСЯЦЬ"""
import calendar
import sys
from orders import generate_raw_orders, in_month, to_order
from report import print_report
from rules import MONTHS
def main(args):
if len(args) != 3:
print("Використання: python main.py РІК МІСЯЦЬ, наприклад: python main.py 2024 7")
return
year, month = int(args[1]), int(args[2])
raw_orders = generate_raw_orders(2000, seed=42)
orders = [to_order(raw) for raw in raw_orders if in_month(raw, year, month)]
days_in_month = calendar.monthrange(year, month)[1]
print(f"Звіт кафе: {MONTHS[month]} {year}, днів: {days_in_month}, чеків: {len(orders)}")
print_report(orders)
if __name__ == "__main__":
main(sys.argv)
main отримує список аргументів параметром, а не читає sys.argv сама. Так її легко перевірити в тестах: main(["main.py", "2024", "7"]).
А якщо ввести місяць словом?
Програма падає з трасуванням. Як перехоплювати такі помилки й відповідати людині зрозуміло — тема уроку 14. Для складніших програм зі стандартної бібліотеки є argparse: він сам друкує довідку --help і перевіряє типи аргументів.
Практика¶
Розібраний приклад: звіт кафе з чотирьох модулів¶
Готовий проєкт лежить у теці cafe_report/. Модулі ті самі, що й вище, але кожен лежить у своїй теці:
cafe_report/
├── main.py ← точка входу: python main.py 2024 7
├── rules/
│ ├── __init__.py ← порожній файл: тека rules — пакет
│ └── rules.py
├── orders/
│ ├── __init__.py
│ └── orders.py
├── reports/
│ ├── __init__.py
│ └── report.py
└── tests/
├── __init__.py
└── test_cafe_report.py ← перевірки проєкту
Тека з модулями й файлом __init__.py — це пакет. Модулів поки чотири, і пласкої теки вистачило б. Але кафе росте: у orders/ згодом ляже модуль для доставки, у reports/ — звіт для бухгалтера, і кожна роль матиме свою теку.
Коду модулів пакети не змінюють — лише рядки імпорту. Ім'я модуля тепер пишеться через крапку: спершу пакет, потім модуль.
| Пласка тека (ноутбук) | Тека з пакетами (cafe_report/) |
|---|---|
from rules import DAYS |
from rules.rules import DAYS — пакет rules, модуль rules |
from orders import Order |
from orders.orders import Order |
from report import print_report |
from reports.report import print_report |
python report.py |
python -m reports.report |
Запуск теж змінюється. python main.py працює як і раніше: main.py лежить у корені, тож перша тека в sys.path — cafe_report/, і пакети rules, orders, reports Python знаходить поруч. А от python reports/report.py впаде з ModuleNotFoundError: No module named 'rules': тоді першою текою стане reports/, де пакета rules немає. Модуль усередині пакета запускають через -m і ім'я модуля з крапкою: python -m reports.report. Детальніше про пакети — у розділі «Поглиблення: пакети».
rules/rules.py і main.py ти вже бачив вище: у main.py з cafe_report/ відрізняються лише три рядки імпорту з таблиці. Ось два інші модулі повністю:
Запуск з теки cafe_report/:
Звіт кафе: липень 2024, днів: 31, чеків: 86
пн — чеків: 17, виторг: 14561.67, середній: 856.57
вт — чеків: 9, виторг: 8332.69, середній: 925.85
ср — чеків: 17, виторг: 14020.64, середній: 824.74
чт — чеків: 14, виторг: 10434.54, середній: 745.32
пт — чеків: 12, виторг: 8032.91, середній: 669.41
сб — чеків: 6, виторг: 3416.60, середній: 569.43
нд — чеків: 11, виторг: 10220.77, середній: 929.16
Найкращий день: пн
За прийомом їжі: {'вечеря': 39, 'обід': 27, 'інше': 20}
Що відбувається в ключових рядках:
orders.py, рядки 9 і 17 — два види чека.RawOrder— те, що дає каса, з міткою часу.Order— той самийNamedTuple, що в уроці 8, тож функції звіту не довелося переписувати;- рядки 28 і 30 — власний генератор із зерном і довжина періоду як різниця двох дат:
.daysуtimedelta; - рядки 33–35 — мітка часу = початок періоду +
timedeltaз випадковими днями, годиною роботи кафе (9–22) і хвилинами; - рядки 42 і 47 — дві маленькі функції замість одного великого циклу:
in_monthвідбирає чеки,to_orderперетворюєRawOrder → Orderза правилами зrules.py; report.py, рядки 2, 4 і 8 —Counterзамінив ручний підрахунок з уроку 8. Рядок 33 — порядок днів береться зrules.DAYS: звіт іде від понеділка до неділі, хоч би в якому порядку прийшли чеки;- рядки 29–38 —
print_reportмайже не змінилася. Вона не знає ні про касу, ні про мітки часу: її вхід — списокOrder, як і раніше.
Модуль звіту окремо, на трьох тестових чеках:
Перевірки проєкту — у tests/test_cafe_report.py. Це теж модуль: він імпортує rules.rules, orders.orders, reports.report і main і перевіряє їх assert-ами, зокрема що import reports.report нічого не друкує. Запуск з теки cafe_report/: python -m tests.test_cafe_report.
Тека зветься tests, а не test: test — пакет стандартної бібліотеки, і своя тека з таким ім'ям його затінила б, як calendar.py вище.
Зміни приклад: чайові за прийомом їжі¶
Власниця хоче знати, коли гості залишають щедріші чайові. Додай у reports/report.py дві функції:
tip_percent(order)— частка чайових у чеку, у відсотках;tips_by_time(orders)— словник «прийом їжі → середній відсоток чайових», округлений до одного знака. Середнє порахуй черезstatistics.mean.
І рядок у кінці print_report:
Критерії перевірки:
python main.py 2024 7друкує рядок, як вище, останнім;tips_by_time([])повертає{}, а не падає;- імпорт
statisticsстоїть на початкуreports/report.py, у групі стандартної бібліотеки, надfrom rules.rules import DAYS; python -m reports.reportтеж друкує рядок про чайові — для трьох тестових чеків.
Підказка
Групуй, як bills_by_time в уроці 8: groups.setdefault(order.time, []).append(tip_percent(order)). Потім dict comprehension: {time: round(mean(values), 1) for time, values in groups.items()}. Для порожнього списку чеків цикл не виконається жодного разу, і comprehension поверне порожній словник.
Спробуй самостійно: звіт за тиждень¶
Бухгалтер просить звіт не за місяць, а за тиждень, що починається з указаної дати:
Звіт кафе: тиждень з 2024-07-15 до 2024-07-21, чеків: 21
пн — чеків: 4, виторг: 3586.74, середній: 896.68
ср — чеків: 4, виторг: 3609.78, середній: 902.45
чт — чеків: 3, виторг: 2465.85, середній: 821.95
пт — чеків: 5, виторг: 3284.45, середній: 656.89
сб — чеків: 4, виторг: 2436.49, середній: 609.12
нд — чеків: 1, виторг: 1291.95, середній: 1291.95
Найкращий день: ср
За прийомом їжі: {'інше': 9, 'вечеря': 8, 'обід': 4}
Створи в теці cafe_report/ новий модуль week.py. Правила:
- модулі в
rules/,orders/іreports/не змінюються:week.pyлише імпортує з них, якmain.py(from orders.orders import ...); - дату з
sys.argv[1]розбери черезdatetime.strptime, кінець тижня —start + timedelta(days=7); - чек потрапляє у звіт, якщо
start <= raw.timestamp < end: 22 липня 00:00 вже не входить; - у заголовку — останній день тижня (
end - timedelta(days=1)), дату виводь через.date()абоstrftime("%Y-%m-%d"); - без аргументу
week.pyдрукує підказку, як уmain.py; - код запуску — під
if __name__ == "__main__":.
У звіті немає вівторка: за цей тиждень у згенерованих даних не було жодного вівторкового чека, і print_report пропускає такі дні.
Поглиблення: пакети¶
Ноутбук заняття обходиться чотирма файлами в одній теці, а в «Практиці» ти вже бачив проєкт, де кожна роль лежить у своєму пакеті. Цей розділ — для охочих: як пакети влаштовані зсередини. Коли файлів стає два десятки, без пакетів не обійтися. І ти вже користувався пакетами, навіть не знаючи цього.
Пакет — це тека з модулями¶
Перенесемо модулі кафе в теку cafe/, а main.py залишимо зовні:
project/
├── main.py
└── cafe/ ← пакет
├── __init__.py ← позначає теку як пакет (може бути порожнім)
├── rules.py ← модуль cafe.rules
├── orders.py ← модуль cafe.orders
└── report.py ← модуль cafe.report
Пакет — це тека з модулями, яку можна імпортувати. Ім'я модуля всередині пакета пишеться через крапку: cafe.rules. Ті самі три способи імпорту працюють і тут (запуск з теки project/):
import cafe.rules
print(cafe.rules.meal_type_from_hour(19))
from cafe import rules
print(rules.DAYS[4])
from cafe.rules import meal_type_from_hour
print(meal_type_from_hour(12))
| Модуль | Пакет | |
|---|---|---|
| Що це на диску | один файл .py |
тека з модулями і __init__.py |
| Що містить | функції, класи, змінні | модулі та вкладені пакети |
| Як імпортувати | import rules |
import cafe.rules, from cafe import rules |
Пакети можна розкладати по-різному. Тут усі модулі кафе в одному пакеті cafe/. У cafe_report/ з «Практики» — окремий пакет на кожну роль, тому ім'я повторюється: rules.rules — пакет rules, модуль rules. Обидва способи робочі. Один пакет простіший, поки модулів мало; пакет на роль зручніший, коли в кожній ролі з'являється кілька модулів.
Перевір себе. У теці cafe/ з'явилася вкладена тека kitchen/ зі своїм __init__.py і файлом menu.py. Як імпортувати з нього функцію price?
Відповідь
from cafe.kitchen.menu import price. Кожна крапка — крок на рівень глибше: пакет cafe → вкладений пакет kitchen → модуль menu.
Імпорти всередині пакета¶
Після переїзду main.py падає, хоча orders.py не змінювався:
File ".../project/cafe/orders.py", line 6, in <module>
from rules import day_from_timestamp, meal_type_from_hour
ModuleNotFoundError: No module named 'rules'
Причина — у sys.path. Запустили python main.py, отже перша тека пошуку — project/. Там лежить тека cafe/, але не файл rules.py. Модуль тепер називається cafe.rules, і так його й треба імпортувати. Є два способи:
Крапка у відносному імпорті означає «з того самого пакета, де лежить цей файл», дві крапки (..) — з батьківського пакета. Абсолютний імпорт явно показує повний шлях, і PEP 8 радить саме його. Відносний коротший і не ламається, якщо перейменувати пакет.
Відносний імпорт має пастку. Спробуємо перевірити report.py окремо, як раніше:
Запущений напряму файл — це __main__, а не cafe.report, тому Python не знає, від якого пакета рахувати крапку. Модуль пакета запускають через -m і ім'я модуля, з теки project/:
Так report.py знає свій пакет, а __name__ у ньому все одно дорівнює "__main__", тож тестовий звіт спрацює. Лише імпорт під цією умовою теж треба оновити: from .orders import Order.
Що робить __init__.py¶
__init__.py — звичайний модуль, який виконується при першому імпорті пакета, за тими самими правилами кешу з sys.modules. Найчастіше його залишають порожнім. Друге типове призначення — винести найважливіші імена на верхній рівень пакета:
"""Пакет кафе: правила, чеки і звіт."""
print("ініціалізація пакета cafe")
from .rules import DAYS, meal_type_from_hour
Пакет імпортувався тричі, а __init__.py виконався один раз. print тут лише для того, щоб це побачити: у справжньому пакеті діє те саме правило, що й для модулів, — без побічних ефектів на верхньому рівні.
А якщо __init__.py немає?
З Python 3.3 тека без __init__.py теж імпортується. Це пакет простору імен (namespace package, PEP 420), механізм для пакетів, розкладених по кількох теках. Python і тут показує, що це інший вид пакета: <module 'cafe' (namespace) ...>. У звичайному проєкті __init__.py додають завжди: так одразу видно, що тека — пакет, а не просто тека з файлами.
Пакети стандартної бібліотеки¶
from collections import Counter з цього уроку — це імпорт з пакета. Видно за __file__:
random — один файл, тобто модуль. collections — тека з __init__.py, тобто пакет (шлях у тебе буде інший, залежно від того, де встановлено Python). Так само влаштовані json, email, urllib і більшість сторонніх бібліотек, які ставлять через pip install.
Підсумок¶
| Поняття | Що запам'ятати |
|---|---|
| Модуль | файл .py; його ім'я — простір імен: rules.DAYS |
import |
шукає по sys.path, виконує файл один раз, кешує в sys.modules |
__name__ |
"__main__" при запуску напряму, ім'я модуля при імпорті |
| Затінення | свій calendar.py у теці скрипта ховає стандартний calendar |
datetime |
.year, .weekday(), strftime/strptime, timedelta, порівняння |
Counter |
Counter(values), most_common(k), відсутній ключ — 0 |
sys.argv |
список рядків; [0] — ім'я скрипта |
| Пакет (поглиблення) | тека з __init__.py; cafe.rules, from .rules import ..., запуск python -m cafe.report |
Самоперевірка¶
- Чим
import rulesвідрізняється відfrom rules import meal_type_from_hourз погляду того, як далі викликати функцію? - У
report.pyна верхньому рівні стоїтьprint("звіт завантажено"). Що побачить користувач, який запускаєmain.py, деreportімпортується двічі? - Чому тестовий код модуля ховають під
if __name__ == "__main__":? - Поруч з
main.pyлежить твій файлrandom.py. Що станеться зorders.py, який робитьimport random? - Що надрукує
print(type(sys.argv[1]))дляpython main.py 2024 7? - Чому
generate_raw_ordersстворюєrandom.Random(seed), а не викликаєrandom.seed(seed)? - Власниця каже: «Середній чек у липні — 800 грн, але більшість гостей платить менше». Як таке можливо і який показник їй краще показати?
Відповіді
- Після
import rulesвикликаємо з префіксом:rules.meal_type_from_hour(19). Післяfrom rules import meal_type_from_hour— без нього:meal_type_from_hour(19), але ім'яrulesу програмі не з'являється. - Один рядок «звіт завантажено»: файл модуля виконується лише при першому імпорті, другий
importбере модуль ізsys.modules. - Щоб він виконувався, лише коли файл запускають напряму (
python report.py), і не виконувався, коли модуль імпортують. - Python знайде твій
random.pyпершим (тека скрипта стоїть першою вsys.path) і завантажить його замість стандартного. Викликrandom.Randomвпаде зAttributeError. <class 'str'>: усі аргументи командного рядка — рядки.random.seedзмінює спільний генератор усієї програми, тобто має побічний ефект. Власнийrandom.Random(seed)належить лише функції: результат відтворюваний, а інший код не зачеплено.- Кілька великих чеків (банкетів) підтягують середнє вгору. Медіана (
statistics.median) показує «типовий» чек і до таких викидів не чутлива.
Що далі¶
- Ноутбук заняття: Відкрити вправи в Colab Переглянути розв’язки — та сама історія кафе: модулі створюються прямо з ноутбука через
%%writefile, з вправами й перевірками. - Довідник:
notes_modules.ipynb— пакети й
__init__.py, відносні імпорти та ще десяток модулів стандартної бібліотеки:pathlib,re,itertools,functools,json,timeit. - Наступне заняття: Урок 14. Винятки.
python main.py 2024 липеньпадає зValueError, аpython main.py 2024 13— з помилкоюcalendar. Навчимося перехоплювати такі помилки. - Урок 15 — файли й JSON: каса віддаватиме чеки не з генератора, а з файлу, і
datetime.strptimeзнадобиться знову.
Документація і джерела¶
- Туторіал Python: Modules — модулі,
__name__,sys.path, пакети - Довідник мови: The import system;
sys.path,sys.modules,sys.argv __main__— Top-level code environment- Модулі уроку:
datetime(коди форматів — strftime() and strptime() Format Codes),collections.Counter,calendar,statistics,random,argparse - Перелік усіх модулів: The Python Standard Library
- Стиль імпортів: PEP 8 — Imports
- Для охочих:
- Harvard CS50P, лекція 4 «Libraries» — модулі,
random,statistics,sys.argv, власні модулі; - Princeton, Introduction to Programming in Python, розділ 2.2 «Modules and Clients» — програма з кількох модулів,
if __name__ == '__main__'для тестування модуля; - Doug Hellmann, Python 3 Module of the Week — приклади до кожного модуля стандартної бібліотеки.
- Harvard CS50P, лекція 4 «Libraries» — модулі,