Модулі, Пакети та Віртуальні Середовища
Модулі, Пакети та Віртуальні Середовища
Проблема монолітного коду: навіщо взагалі ділити програму
Уявіть, що ви розробляєте платформу для онлайн-торгівлі. Перші кілька тижнів все ідеально: один файл main.py, кілька функцій, чотири змінні. Але проходить місяць — і цей файл розростається до 2000 рядків. У ньому перемішані логіка авторизації, робота з базою даних, бізнес-правила знижок, генерація PDF-звітів та обробка платежів.
Це явище має назву — монолітний антипатерн. Він не є проблемою теоретичною: він реально гальмує розробку, ламає командну роботу та перетворює підтримку коду на катування.
Низька читабельність
calculate_vat серед 2000 рядків — це пригода. IDE допомагає, але лише до певної межі. Після неї — лише Ctrl+F і терпіння.Конфлікти у команді
Неможливість перевикористання
send_email з main.py потрібна у новому проекті? Доведеться або копіювати, або тягнути весь монолітний файл разом із зайвим кодом.Неможливість тестування
Рішення, що пропонує Python, — це чотирирівнева система організації коду: вирази → функції → модулі → пакети. Ця стаття присвячена двом верхнім рівням і тому, як забезпечити їхню ізоляцію між проектами через віртуальні середовища.
Частина I: Модулі
Що таке модуль: від файлу до об'єкта
З практичної точки зору, модуль — це будь-який файл з розширенням .py. Але це спрощення. З точки зору Python-рантайму, модуль — це об'єкт типу types.ModuleType, що має власний простір імен (namespace) і зберігається у кеші sys.modules.
Коли Python виконує import calculator, він не просто «підключає файл». Він:
- Знаходить файл
calculator.py(за алгоритмомsys.path) - Компілює його у байткод (
calculator.pyc) - Створює новий об'єкт типу
module - Виконує байткод у просторі імен цього об'єкта
- Зберігає об'єкт у словнику
sys.modules['calculator'] - Прив'язує ім'я
calculatorу поточному просторі імен
Розглянемо цей процес на практичному прикладі. Побудуємо структуру реального mini-проекту:
Простір імен модуля: захист від конфліктів
Коли ви пишете import calculator, Python не «вливає» імена add, subtract, PI у поточний простір імен. Замість цього він створює одне ім'я calculator, що вказує на об'єкт модуля. Доступ до вмісту — лише через крапку. Це і є простір імен (namespace) — ізольований контейнер для імен.
# main.py
import calculator
# Наша власна функція add — для роботи з рядками
def add(text1: str, text2: str) -> str:
return text1 + " " + text2
# Жодного конфлікту! Два різних простори імен:
my_text = add("Привіт", "Світ") # наша функція
calc_sum = calculator.add(2, 2) # функція з модуля
print(my_text) # "Привіт Світ"
print(calc_sum) # 4
Якби Python не використовував простори імен і замість import calculator вставляв усі імена безпосередньо, виникала б катастрофа: будь-яка функція з будь-якого модуля могла б перезаписати будь-яку іншу. Саме тому import module вважається більш безпечним підходом порівняно з from module import *.
Поглиблене розуміння просторів імен (Namespaces) та LEGB-правило
Щоб зрозуміти, як працює система імпорту та як уникнути помилок, пов'язаних із видимістю змінних, потрібно розібратися, що таке простір імен (namespace) на низькому рівні.
У Python простір імен — це звичайний словник (dict), де ключами є імена змінних (ідентифікатори), а значеннями — самі об'єкти, на які ці змінні посилаються. Коли ви пишете x = 42, Python просто додає запис {"x": 42} у відповідний словник простору імен.
Чотири рівні просторів імен та LEGB-правило
Коли ви звертаєтеся до змінної, Python шукає її не скрізь, а в чітко визначеному порядку. Цей порядок описується LEGB-правилом:
Local (Локальний)
Змінні, визначені всередині поточної функції (через def або lambda). Вони створюються при виклику функції і знищуються при її завершенні.
Enclosing (Охоплюючий)
Змінні у зовнішніх функціях (використовується при вкладених функціях або замиканнях).
Global (Глобальний)
Змінні, визначені на рівні модуля (файлу). Всі функції цього модуля мають доступ до цих змінних.
Built-in (Вбудований)
Назви, які автоматично завантажуються інтерпретатором при старті. Сюди входять вбудовані функції (print(), len(), range(), dict) та виключення (ValueError, KeyError).
Схематично ієрархію пошуку імен можна зобразити у вигляді вкладених областей:
Практичний експеримент: дослідження просторів імен
Ми можемо дослідити поточні простори імен за допомогою вбудованих функцій locals() та globals(), які повертають відповідні словники простору імен.
# namespace_demo.py
import math
global_var = "Я глобальна змінна"
def outer_function():
enclosing_var = "Я охоплююча змінна"
def inner_function():
local_var = "Я локальна змінна"
# Дивимося на локальний простір імен внутрішньої функції
print("--- Local Namespace inside inner_function() ---")
print(locals()) # Виведе тільки {'local_var': 'Я локальна змінна'}
# Спробуємо знайти global_var та print (built-in)
# Python пройде шлях: inner_local -> outer_enclosing -> global -> built-in
inner_function()
outer_function()
# Глобальний простір імен модуля
print("\n--- Global Namespace of module ---")
# globals() містить імпортований 'math', 'global_var', 'outer_function' та службові атрибути
print(list(globals().keys())[:10]) # покажемо перші 10 ключів
Модуль як об'єкт та його атрибут __dict__
Оскільки кожен модуль після імпорту стає об'єктом типу module, його простір імен зберігається в спеціальному атрибуті __dict__. Це звичайний словник.
Коли ви пишете calculator.add(5, 5), Python під капотом виконує:
calculator.__dict__['add'](5, 5)
Це означає, що ми можемо динамічно маніпулювати простором імен модуля навіть після його імпорту (хоча цим не варто зловживати):
import calculator
# Динамічно додаємо нове ім'я в простір імен модуля calculator
calculator.new_constant = 9.99
# Тепер це ім'я доступне звичайним шляхом!
print(calculator.new_constant) # 9.99
Цей динамізм показує, що простір імен модулів у Python є відкритим і гнучким, на відміну від статичних просторів імен у таких мовах, як C++ або C#.
Синтаксис імпорту: вичерпний огляд
Python надає кілька синтаксичних форм для імпорту, кожна з яких має власну область застосування та компроміси.
import calculator
import os
import sys
# Доступ лише через ім'я модуля — жодних конфліктів
result = calculator.add(10, 5)
cwd = os.getcwd()
paths = sys.path
Коли використовувати: у більшості випадків. Максимальна чіткість: завжди видно, звідки прийшло ім'я.
from calculator import add, PI
from os.path import join, exists
# Доступ безпосередньо
result = add(10, 5)
full_path = join("/home", "user", "project")
Коли використовувати: якщо з великого модуля потрібні 1-3 конкретних об'єкти, і імена не конфліктують з локальними.
import calculator as calc
import numpy as np # галузева конвенція
import matplotlib.pyplot as plt # галузева конвенція
import pandas as pd
result = calc.add(2, 3)
array = np.array([1, 2, 3])
Коли використовувати: якщо ім'я модуля довге або є загальноприйнятий псевдонім у спільноті.
# ❌ НІКОЛИ НЕ РОБІТЬ ТАК У PRODUCTION-КОДІ
from calculator import *
# Тепер у просторі імен є: add, subtract, multiply, circle_area, PI
# Але звідки вони взялися? Невідомо без читання calculator.py
result = add(5, 5) # яка це функція? наша? із calculator? з іншого модуля?
Чому це погано:
- Забруднює простір імен непередбачуваною кількістю імен
- Гарантовані приховані конфлікти: якщо
calculatorі ваш код мають функціюadd, одна мовчки перезапише іншу - IDE та статичні аналізатори не можуть розібратися у таких імпортах
Конвенція _private: приватні атрибути модуля
У Python немає ключових слів private або protected. Натомість існує конвенція: ім'я, що починається з одного підкреслення (_name), є сигналом «не призначене для публічного використання».
# calculator.py
PI = 3.14159 # публічний атрибут ✅
_precision = 10 # «приватний» атрибут — для внутрішнього використання
def add(a, b): # публічна функція ✅
return a + b
def _validate(x): # «приватна» функція — деталь реалізації
return isinstance(x, (int, float))
Технічно calculator._validate(5) спрацює без помилок — Python лише сигналізує, але не забороняє. Проте конвенція дотримується всією екосистемою: IDE підсвічують такий виклик, linter-и видають попередження, а рецензенти коду запитають: «Навіщо ти лізеш у внутрішності чужого модуля?»
Ця конвенція також впливає на from module import *: якщо ваш модуль не визначає __all__, зірковий імпорт пропускає всі імена з підкресленням — це одна з небагатьох корисних властивостей import *.
Система пошуку модулів: sys.path зсередини
Алгоритм пошуку import
Коли Python зустрічає import my_module, він не шукає файл одразу по всій файловій системі. Він послідовно перевіряє ієрархічний список директорій, що зберігається у sys.path — списку рядків, де кожен рядок є абсолютним шляхом до директорії.
Анатомія sys.path: що там насправді
import sys
print(f"Тип sys.path: {type(sys.path)}") # <class 'list'>
print(f"Кількість шляхів: {len(sys.path)}")
for i, path in enumerate(sys.path):
print(f" [{i}] {path}")
Звідки формується sys.path? Це не магія — список складається з кількох джерел у визначеному порядку пріоритету:
python /home/user/project/main.py Python автоматично ставить на перше місце /home/user/project/. Саме тому import calculator працює, якщо calculator.py лежить поруч з main.py. При запуску через -m або в інтерактивному режимі sys.path[0] дорівнює '' (порожній рядок = поточна директорія shell).PYTHONPATH=/my/libs:/other/libs, Python вставляє ці шляхи після sys.path[0]. Це спосіб глобально зареєструвати власні бібліотеки без встановлення через pip. Рідко потрібен у сучасних проектах з venv.os, sys, json, datetime тощо. Їхнє точне розташування залежить від ОС та версії Python, але вони завжди присутні у sys.path.pip install встановлює сторонні пакети. Зазвичай знаходиться у ~/.local/lib/pythonX.Y/site-packages/ (глобально) або venv/lib/pythonX.Y/site-packages/ (у venv). Саме тут лежать ваші requests, Django, numpy.Маніпуляції з sys.path: коли і як
sys.path — це звичайний Python-список, тому його можна змінювати в рантаймі. Це дає потужні (і небезпечні) можливості:
import sys
# Додати власний шлях перед усіма іншими (найвищий пріоритет)
sys.path.insert(0, '/path/to/my/custom/libs')
# Або в кінець (найнижчий пріоритет)
sys.path.append('/path/to/fallback/libs')
# Тепер Python шукатиме модулі і там
import my_custom_module # знайде у /path/to/my/custom/libs
sys.path у production-коді — антипатерн. Якщо ви додаєте шлях до sys.path у своєму коді, це означає, що ваша структура проекту або система встановлення пакетів налаштована неправильно. Правильне рішення — використовувати pip install -e . для локальної розробки або коректно структурований пакет. sys.path.append(...) допустимий лише у тимчасових скриптах та скриптах налагодження.Альтернатива — файл .pth у директорії site-packages: Python автоматично читає всі .pth-файли при запуску та додає зазначені в них шляхи до sys.path. Команда pip install -e . (editable install) використовує саме цей механізм.
Як влаштований імпорт під капотом: Finder та Loader
Багато розробників думають, що import — це просто пошук файлу на диску. Насправді Python використовує складну, але дуже гнучку двохетапну систему імпорту, що базується на пошуковцях (Finders) та завантажувачах (Loaders). Ця система повністю документована у PEP 302.
Коли ви викликаєте import my_module, виконуються такі кроки:
- Пошук модуля (Find Phase):
Python проходить по списку пошуковців, що зберігаються в
sys.meta_path. Типово там містяться:BuiltinImporter(для вбудованих модулів, наприклад,sys).FrozenImporter(для "заморожених" модулів, написаних на C і скомпільованих у двійковий файл інтерпретатора).PathFinder(шукає файли на диску за шляхами ізsys.path).
Кожен пошуковець намагається знайти модуль. Якщо знаходить, він повертає спеціальний об'єкт — специфікацію модуля (ModuleSpec), яка містить метадані: ім'я модуля, посилання на завантажувач та шлях до файлу. - Завантаження модуля (Load Phase):
Отримавши
ModuleSpec, Python викликає відповідний завантажувач (Loader). Завантажувач:- Створює порожній об'єкт модуля.
- Записує його в
sys.modules. - Виконує код файлу модуля в контексті цього об'єкта.
Цей механізм дозволяє розробникам писати власні імпортери (import hooks). Наприклад, можна написати Finder/Loader, який імпортує модулі безпосередньо з ZIP-архівів, бази даних або навіть завантажує їх через HTTP з віддаленого сервера!
Що таке .pth-файли
Файли з розширенням .pth (path configuration files) — це дуже простий і зручний спосіб додавання шляхів до sys.path без ручного редагування коду або налаштування змінної PYTHONPATH.
Якщо покласти файл my_paths.pth у директорію site-packages вашого віртуального середовища, Python під час ініціалізації прочитає його рядки і додасть усі вказані там шляхи до sys.path.
Кожен рядок у .pth-файлі має містити один абсолютний або відносний шлях. Якщо рядок починається з import , Python навіть виконає цей код (це використовується деякими складними бібліотеками для автоналаштування).
Саме цей механізм використовується при встановленні локального пакета в "режимі редагування":
pip install -e .
Замість того, щоб копіювати файли вашого проекту в site-packages, pip просто створює там .pth-файл, який містить шлях до вашої робочої директорії. Це дозволяє вам редагувати код, і зміни будуть миттєво видимі без повторного встановлення пакета.
Кешування імпортів: sys.modules та Singleton-поведінка
Чому модуль завантажується лише один раз
Python є мовою, де один і той самий модуль (os, json, datetime) може бути імпортований у десятках різних файлів одного проекту. Якби кожен import os перечитував файл з диска та виконував байткод заново — продуктивність була б катастрофічною.
Рішення — кеш модулів: словник sys.modules, що зберігає всі вже завантажені модулі.
# main.py — демонстрація кешування
import sys
print("=== Перший імпорт ===")
import calculator # <— тут: файл читається, байткод виконується
print("\n=== Другий імпорт ===")
import calculator # <— тут: Python бачить 'calculator' у sys.modules → пропускає
print("\n=== Третій імпорт ===")
import calculator # <— те саме: кеш-хіт, нульова вартість
print(f"\n'calculator' у кеші? {'calculator' in sys.modules}")
# Отримати об'єкт модуля з кешу напряму
cached = sys.modules['calculator']
print(f"Той самий об'єкт? {calculator is cached}") # True
print(f"Виклик через кеш: {cached.add(1, 2)}") # 3
Експеримент: ручне керування кешем у sys.modules
Щоб краще зрозуміти роль sys.modules, ми можемо втрутитися в його роботу безпосередньо. Якщо ми видалимо модуль із цього словника, Python "забуде", що він його колись імпортував, і при наступному import виконає код заново (так, ніби це перший імпорт).
# sys_modules_experiment.py
import sys
print("--- Крок 1: Перший імпорт ---")
import calculator # Виведе повідомлення про завантаження
print("\n--- Крок 2: Другий імпорт (кешований) ---")
import calculator # Нічого не виведе, оскільки calculator вже у sys.modules
# Перевіряємо наявність у кеші
print(f"\ncalculator у sys.modules? {'calculator' in sys.modules}")
print("\n--- Крок 3: Видалення з sys.modules та повторний імпорт ---")
# Ручне видалення запису з кешу
del sys.modules['calculator']
print(f"calculator у sys.modules після видалення? {'calculator' in sys.modules}")
import calculator # Python знову завантажує та виконує код модуля!
sys.modules, ніколи не видаляйте модулі з sys.modules вручну в реальних проектах. Це може зламати зв'язки між модулями та спричинити непередбачувані побічні ефекти (наприклад, якщо інші частини коду вже мають посилання на старий об'єкт модуля).sys.modules як Singleton-реєстр: спільний стан
Той факт, що Python повертає один і той самий об'єкт модуля всім імпортерам, породжує важливий архітектурний наслідок: модуль з глобальними змінними веде себе як Singleton.
# state.py — модуль зі спільним станом
print("Ініціалізація state.py")
request_count = 0
active_connections = []
# module_a.py — реєструє запити
import state
def handle_request(url: str) -> None:
state.request_count += 1
state.active_connections.append(url)
print(f"[A] Запит #{state.request_count} до {url}")
# module_b.py — читає статистику
import state
def get_stats() -> dict:
return {
"total": state.request_count,
"active": len(state.active_connections)
}
# main.py
import module_a
import module_b
module_a.handle_request("/api/users")
module_a.handle_request("/api/products")
stats = module_b.get_stats()
print(f"Статистика: {stats}")
# {'total': 2, 'active': 2}
# module_b бачить зміни, зроблені module_a!
Обидва модулі отримали одне й те саме посилання на об'єкт state з sys.modules. Зміни від module_a одразу видимі у module_b. Це потужний механізм для глобальної конфігурації та стану, але він вимагає свідомого підходу — ненавмисні зміни глобальних змінних модуля є поширеним джерелом важко відтворюваних багів.
Перезавантаження модуля: importlib.reload
У нормальному виробничому коді кеш sys.modules є бажаною поведінкою. Але при інтерактивній розробці (Jupyter Notebook, REPL) виникає ситуація: ви змінили файл модуля, але Python все одно використовує стару закешовану версію.
import importlib
import calculator
# Поточна версія
print(calculator.add(1, 2)) # 3
# ... ви зміни код calculator.py: add тепер повертає a + b + 100 ...
# Звичайний import нічого не зробить:
import calculator
print(calculator.add(1, 2)) # все одно 3!
# Примусово перезавантажити:
importlib.reload(calculator)
print(calculator.add(1, 2)) # тепер 103 ✅
importlib.reload не видаляє об'єкт модуля з sys.modules — він повторно виконує код файлу в тому ж самому об'єкті модуля, оновлюючи його атрибути. Це має важливе обмеження: якщо ви зробили from calculator import add (імпорт із прив'язкою до локального імені), reload(calculator) оновить модуль, але ваша локальна змінна add все одно вказуватиме на стару функцію.
importlib.reload — виключно інструмент для розробки. У production-коді перезавантаження модулів у рантаймі є джерелом важко передбачуваних помилок, пов'язаних із невідповідністю об'єктів, що були створені до і після reload. Не використовуйте його у фінальному коді.Подвійне призначення файлу: if __name__ == "__main__"
Як Python визначає «хто головний»
Кожен модуль Python несе атрибут __name__. Його значення залежить від того, в якому контексті виконується файл:
- Якщо файл запускається напряму (
python calculator.py):__name__ == "__main__" - Якщо файл імпортується в інший модуль:
__name__ == "calculator"(ім'я без.py)
Це розмежування дозволяє одному файлу виконувати дві різні ролі залежно від контексту: як самостійний скрипт із власною логікою запуску, і як бібліотека для інших модулів.
# calculator.py
PI = 3.14159
def add(a, b):
return a + b
def subtract(a, b):
return a - b
def _run_tests():
"""Вбудовані smoke-тести."""
assert add(2, 3) == 5, "add(2, 3) має дорівнювати 5"
assert subtract(10, 3) == 7, "subtract(10, 3) має дорівнювати 7"
print("✅ Всі smoke-тести пройдено!")
# Цей блок виконується ЛИШЕ при запуску: python calculator.py
# При import calculator — він повністю ігнорується
if __name__ == "__main__":
print(f"Запуск calculator.py як головного скрипта.")
print(f"Поточне __name__: {__name__}")
_run_tests()
Патерн main(): структурований точок входу
Поєднання if __name__ == "__main__" зі спеціальною функцією main() є галузевим стандартом для скриптів, що мають власну логіку виконання:
# data_processor.py
import sys
import json
def load_data(filepath: str) -> dict:
"""Завантажує JSON з файлу."""
with open(filepath) as f:
return json.load(f)
def process(data: dict) -> list:
"""Бізнес-логіка обробки даних."""
return [item for item in data.get("items", []) if item.get("active")]
def save_results(results: list, output_path: str) -> None:
"""Зберігає результати."""
with open(output_path, "w") as f:
json.dump(results, f, indent=2)
def main() -> int:
"""
Точка входу програми.
Повертає код завершення: 0 = успіх, 1 = помилка.
"""
if len(sys.argv) != 3:
print(f"Використання: python {sys.argv[0]} <input.json> <output.json>")
return 1
input_path = sys.argv[1]
output_path = sys.argv[2]
try:
data = load_data(input_path)
results = process(data)
save_results(results, output_path)
print(f"✅ Оброблено {len(results)} записів → {output_path}")
return 0
except FileNotFoundError as e:
print(f"❌ Файл не знайдено: {e}")
return 1
except json.JSONDecodeError as e:
print(f"❌ Невалідний JSON: {e}")
return 1
if __name__ == "__main__":
sys.exit(main()) # sys.exit передає код завершення в оболонку
Цей підхід дає три переваги: main() є звичайною функцією, яку можна легко тестувати; return 1 / return 0 дозволяє коректно сигналізувати про помилки в bash-скриптах; модуль залишається повністю придатним для import.
python -m: запуск модуля як скрипта
Окрім python script.py, Python підтримує синтаксис python -m module_name. Різниця тонка, але важлива:
| Команда | sys.path[0] | Для чого |
|---|---|---|
python path/to/script.py | директорія скрипта | прості скрипти |
python -m package.module | коренева директорія проекту | модулі всередині пакетів |
python -m venv .venv | — | вбудовані інструменти |
Прапорець -m каже Python знайти модуль за sys.path і запустити його з __name__ = "__main__". Це критично для пакетів, де відносні імпорти (from .utils import helper) не працюють при прямому python package/module.py, але коректно розв'язуються при python -m package.module.
Циклічні імпорти: архітектурний антипатерн
Як виникає циклічний імпорт
Циклічний імпорт виникає, коли модуль A імпортує модуль B, а модуль B імпортує модуль A (прямо чи через ланцюг інших модулів). Python обробляє цей сценарій, але результат часто несподіваний.
# a.py
print("Завантаження a.py...")
import b # Python призупиняє a.py і завантажує b.py
def func_a():
b.func_b()
print("func_a виконана")
print("a.py завантажено.")
# b.py
print("Завантаження b.py...")
import a # Python бачить: a вже є у sys.modules (частково!)
def func_b():
print("func_b виконана")
print("b.py завантажено.")
# main.py
import a
a.func_a() # AttributeError: partially initialized module 'b'...
Що відбувається покроково:
Python починає виконувати a.py
Виводить Завантаження a.py.... Бачить import b — призупиняється.
Python починає виконувати b.py
Виводить Завантаження b.py.... Бачить import a.
Python виявляє a у sys.modules
Але a ще не повністю завантажений — він призупинений на рядку import b. Python не падає в рекурсію, а повертає частковий об'єкт a. На момент повернення у a.__dict__ ще немає func_a (оголошення def func_a() ще не виконане!).
b.py успішно завантажується
Визначає func_b, виводить b.py завантажено.
Виконання повертається до a.py
a.py продовжує: визначає func_a, виводить a.py завантажено.
Виклик a.func_a() → b.func_b()
Якщо b.py намагається використати щось з a на рівні модуля (не всередині функції), воно буде недоступне.
Правильні рішення
Рішення 1 (найкраще): Рефакторинг — виділення спільного коду у третій модуль.
Циклічний імпорт майже завжди є симптомом поганої архітектури: два модулі мають взаємні залежності, що означає, що вони не є незалежними одиницями. Правильне рішення — знайти спільний код і винести його у незалежний модуль.
❌ Погана архітектура: ✅ Правильна архітектура:
a.py ←→ b.py shared.py (незалежний)
a.py → shared.py
b.py → shared.py, a.py
Рішення 2 (тимчасове): Локальний імпорт всередині функції.
# a.py
def func_a():
import b # ← імпорт всередині функції, відкладений до виклику
b.func_b()
print("func_a виконана")
Коли func_a() буде викликана, обидва модулі вже будуть повністю завантажені, тому b.func_b буде доступна. Але це «милиця» — вона ховає архітектурну проблему і уповільнює виклик функції (перевірка sys.modules при кожному виклику).
Частина II: Пакети
Від модулів до ієрархії: навіщо потрібні пакети
Коли проект виростає за межі кількох модулів, виникає нова проблема організації: навіть правильно розділений на модулі код перетворюється на хаотичний набір файлів, якщо всі вони лежать в одній директорії.
❌ Хаос без пакетів: ✅ Структура з пакетами:
myproject/ myproject/
main.py main.py
users.py users/
user_auth.py __init__.py
user_profile.py auth.py
products.py profile.py
product_catalog.py products/
product_pricing.py __init__.py
orders.py catalog.py
order_processing.py pricing.py
order_notifications.py orders/
database.py __init__.py
api_client.py processing.py
utils.py notifications.py
database.py
utils.py
Пакет — це директорія, що містить спеціальний файл __init__.py. Наявність цього файлу є сигналом для Python: «ця директорія — не просто папка, це модульна одиниця».
Анатомія пакета: реальний проект e-shop
Побудуємо повноцінний пакет для інтернет-магазину та дослідимо кожен його елемент:
Роль __init__.py: ворота пакета
Файл __init__.py виконує три ключові функції.
Функція 1: Позначення директорії як пакета. Без __init__.py Python 3 усе одно може знайти пакет (завдяки механізму «namespace packages»), але поведінка буде непередбачуваною. Явний __init__.py є гарантією.
Функція 2: Ініціалізаційний код. Все, що у __init__.py, виконується при першому import shop або import shop.products — але лише один раз (кеш sys.modules). Тут доречно: підключення до БД, завантаження конфігурації, ініціалізація глобального стану.
Функція 3: «Фасад» публічного API. Це найважливіша архітектурна роль. Ре-експортуючи функції у __init__.py, ви:
- Спрощуєте зовнішній API:
shop.list_products()замістьshop.products.list_products() - Дозволяєте змінювати внутрішню структуру пакета без зламу зовнішнього коду
# До рефакторингу: shop/products.py
# Після рефакторингу: shop/catalog/products.py
# Але __init__.py залишається незмінним:
from .catalog.products import list_products # ← просто змінили шлях
# Зовнішній код 'shop.list_products()' не сломається!
Абсолютний та відносний імпорт
Абсолютний імпорт: повний шлях від кореня
Абсолютний імпорт вказує повний шлях від кореня проекту (або від директорії у sys.path):
# Де б не знаходився цей файл у структурі проекту:
import shop.products # весь модуль
from shop.products import list_products # конкретна функція
from shop.orders.processing import create_order # з підпакета
Абсолютні імпорти є однозначними: завжди зрозуміло, звідки береться модуль. PEP 8 рекомендує абсолютний імпорт як основний для міжпакетних залежностей.
Відносний імпорт: шлях відносно поточного модуля
Відносний імпорт використовує крапкову нотацію для вказівки шляху відносно поточного модуля:
# Всередині shop/orders/processing.py:
from . import notifications # . = поточний пакет (shop/orders/)
from .notifications import send # конкретна функція з сусіднього модуля
from .. import products # .. = батьківський пакет (shop/)
from ..customers import get_customer # конкретна функція з батька
Відносні імпорти мають суттєве обмеження: вони не працюють у файлі, що виконується як головний скрипт (__name__ == "__main__"). Тому не використовуйте відносні імпорти у main.py або в будь-якому файлі, що планується запускати напряму.
- Абсолютний — для імпорту між різними пакетами:
from shop.products import ... - Відносний — для зв'язків усередині одного пакета:
from .utils import helper— гарантує, що пакет використовує свій власнийutils, а не однойменний модуль з іншого місця уsys.path
__all__: контракт публічного API
Змінна __all__ у __init__.py або у будь-якому модулі — це список рядків, що визначає, які імена є публічним API цього модуля/пакета:
# shop/products.py
__all__ = ["list_products", "get_product", "calculate_price_with_vat"]
# _products_db та будь-які внутрішні допоміжні функції
# не включені в __all__ і не будуть імпортовані через 'from shop.products import *'
def list_products(active_only=True): ...
def get_product(product_id): ...
def calculate_price_with_vat(product_id, vat_rate=0.20): ...
def _validate_product(data): ... # внутрішня, не у __all__
_products_db = [...] # приватні дані, не у __all__
__all__ виконує дві функції:
- Документація: чітко сигналізує, які об'єкти є стабільним публічним API
- Контроль
import *: тільки імена з__all__потрапляють у простір імен приfrom module import *
Частина III: Віртуальні Середовища
Проблема глобального Python: «пекло залежностей»
Уявіть типову ситуацію: у вас на комп'ютері два Python-проекти.
- Проект А (2019 рік): використовує
Django 2.2таrequests 2.20 - Проект Б (2024 рік): потребує
Django 4.2таrequests 2.31
Якщо встановити обидва проекти у глобальний Python (без ізоляції), виникає конфлікт: pip install Django==4.2 замінить Django==2.2 — і Проект А зламається. Встановити обидві версії одночасно у глобальний Python неможливо.
Це явище називається «пекло залежностей» (dependency hell).
Що таке venv: ізольований Python від стандартної бібліотеки
venv — це стандартний модуль Python (доступний без встановлення з Python 3.3+), що створює ізольоване середовище виконання. Технічно це директорія з окремою копією інтерпретатора Python та власною директорією site-packages.
При активації venv оболонка перенаправляє команди python та pip на копії у середовищі. Будь-яка встановлена бібліотека потрапляє у site-packages цього середовища — повністю ізольовано від глобального Python та інших середовищ.
Як venv працює під капотом
Багато хто вважає, що віртуальне середовище — це повна копія інтерпретатора Python. Насправді це дуже легка структура, яка працює завдяки кільком системним конфігураціям та логіці пошуку шляхів самого Python.
Ключовим елементом будь-якого venv є файл pyvenv.cfg, який лежить у корені папки середовища. Його вміст виглядає приблизно так:
home = /usr/bin
include-system-site-packages = false
version = 3.12.2
executable = /usr/bin/python3.12
command = /usr/bin/python3 -m venv /home/user/myproject/.venv
Коли ви запускаєте файл .venv/bin/python (або .venv/Scripts/python.exe на Windows), відбувається такий алгоритм:
- Пошук конфігурації:
Інтерпретатор шукає файл
pyvenv.cfgу своїй директорії або директорії на рівень вище. - Встановлення префіксів:
- Якщо файл
pyvenv.cfgзнайдено, Python розуміє, що він запущений всередині віртуального середовища. - Змінна
sys.base_prefixвстановлюється на шлях із параметраhome(це глобальна установка Python, де лежить реальний виконуваний файл і стандартна бібліотека). - Змінна
sys.prefixвстановлюється на директорію, де лежитьpyvenv.cfg(тобто на ваше віртуальне середовище).
- Якщо файл
- Формування
sys.path: При формуванні шляхів пошуку модулів Python використовує значенняsys.prefix. Завдяки цьому він підключаєsite-packagesіз папки.venv/lib/python3.X/site-packages/замість глобальногоsite-packages.
Скрипт активації (source .venv/bin/activate) насправді є дуже простим оболонковим (shell) скриптом, який робить лише дві речі:
- Додає шлях до
.venv/bin/на самий початок змінної середовища вашої системиPATH. Завдяки цьому, коли ви вводитеpythonабоpip, викликаються саме файли з вашого середовища, а не глобальні. - Тимчасово змінює промпт вашого терміналу, додаючи
(.venv).
source — це зручність для терміналу. Програми та IDE можуть працювати з venv без будь-якої активації — їм просто достатньо запускати інтерпретатор безпосередньо за шляхом .venv/bin/python.Повний цикл роботи з venv
Крок 1: Створення середовища
# python -m venv <назва_директорії>
# Конвенція: називати середовище 'venv' або '.venv'
python -m venv venv # звичайне середовище
python -m venv .venv # приховане (починається з крапки)
python3.12 -m venv .venv # явна версія Python
Після виконання команди Python створює таку структуру:
.venv/
├── bin/ # (Linux/macOS)
│ ├── python # символічне посилання на інтерпретатор
│ ├── python3
│ ├── pip
│ └── activate # скрипт активації!
├── lib/
│ └── python3.12/
│ └── site-packages/ # сюди pip встановлює пакети
└── pyvenv.cfg # конфігурація середовища
Крок 2: Активація середовища
Активація — це зміна змінних середовища оболонки (PATH, VIRTUAL_ENV) так, щоб команди python та pip вели до venv.
source .venv/bin/activate
# Підтвердження активації: у промпті з'явиться назва середовища
# (.venv) user@host:~/myproject$
which python # /home/user/myproject/.venv/bin/python
which pip # /home/user/myproject/.venv/bin/pip
.venv\Scripts\activate.bat
:: Або в PowerShell:
.venv\Scripts\Activate.ps1
:: Підтвердження: (.venv) C:\myproject>
where python
:: C:\myproject\.venv\Scripts\python.exe
source .venv/bin/activate.fish
Крок 3: Встановлення пакетів
Після активації pip install встановлює пакети ізольовано у .venv/lib/python3.X/site-packages/:
# Встановлення з PyPI
pip install requests # остання версія
pip install requests==2.31.0 # конкретна версія
pip install "requests>=2.28,<3.0" # діапазон версій
pip install django djangorestframework # кілька одразу
# Список встановлених пакетів
pip list
# Детальна інформація про пакет
pip show requests
Крок 4: Деактивація
deactivate
# Промпт повернувся до нормального вигляду:
# user@host:~/myproject$
which python # /usr/bin/python3 ← глобальний Python
requirements.txt: заморожування залежностей
Файл requirements.txt — це текстовий маніфест точних версій усіх пакетів, потрібних вашому проекту. Він є стандартом відтворюваності Python-проектів.
Генерація requirements.txt з активного середовища:
# pip freeze виводить всі встановлені пакети з точними версіями
pip freeze > requirements.txt
cat requirements.txt
Відтворення середовища з requirements.txt:
# На іншій машині або у CI/CD:
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# pip встановить точно ті ж версії, що зафіксовані у файлі
pyproject.toml (стандарт PEP 517/518) замість requirements.txt. Менеджери пакетів poetry, uv, pdm — надають більш потужне управління залежностями. Але requirements.txt + venv залишаються базовим стандартом, який потрібно знати.Частина IV: Сучасні менеджери пакетів
Стандартний тандем venv + pip + requirements.txt надійний і повсюдно підтримуваний. Але у виробничих проектах він має відомі недоліки:
pip freezeвключає транзитивні залежності (ті, що потрібні вашим залежностям), а не лише прямі — файл стає важким до читання і крихким при оновленні.- Немає детермінованого lockfile зі сумами хешів для кожної платформи окремо.
- Немає вбудованого управління версіями Python або запуску команд у ізольованому середовищі.
pipне вирішує конфлікти залежностей наперед — він просто встановлює і повідомляє про помилку вже після.
Два найпопулярніші інструменти, що вирішують ці проблеми: uv (наступне покоління, швидкість у 10–100 разів вища за pip) та Poetry (зрілий, широко розповсюджений у промисловому середовищі).
uv — ультрашвидкий менеджер нового покоління
uv — це інструмент від компанії Astral (автори лінтера ruff), написаний на Rust. Він замінює одразу кілька інструментів: pip, pip-tools, virtualenv, pyenv та частково poetry — з єдиного бінарного файлу.
10–100x швидше за pip
uv використовує паралельне завантаження пакетів і оптимізований резолвер залежностей. Встановлення Django з усіма залежностями займає менше секунди проти 10–30 секунд у pip.Єдиний інструмент
uv python install 3.12), середовищами (uv venv), залежностями (uv add/remove) та інструментами CLI (uvx ruff). Один бінарний файл замість п'яти.Детермінований lockfile
uv.lock фіксує точні версії та хеші для всіх платформ. Синхронізація через uv sync гарантує ідентичне середовище на будь-якій машині.pyproject.toml (PEP 621)
pyproject.toml як єдину точку конфігурації замість requirements.txt + setup.py + setup.cfg.Встановлення uv
# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
# Також через pip (якщо вже є Python):
pip install uv
# Перевірка встановлення
uv --version # uv 0.5.x (або новіше)
Створення нового проекту з нуля
# Ініціалізація проекту — створює pyproject.toml і базову структуру
uv init my-web-api
cd my-web-api
# Або ініціалізація у поточній директорії
mkdir my-project && cd my-project
uv init
Після uv init проект вже має pyproject.toml:
# pyproject.toml (генерується автоматично)
[project]
name = "my-web-api"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
Управління залежностями
# Додавання залежності — оновлює pyproject.toml і uv.lock автоматично
uv add fastapi
uv add "sqlalchemy>=2.0"
uv add "pydantic>=2.5,<3.0"
# Кілька залежностей одразу
uv add httpx aiohttp redis
# Залежності лише для розробки (не потрапляють у production)
uv add --dev pytest pytest-asyncio ruff mypy
uv add --dev black isort
# Видалення залежності
uv remove redis
# Синхронізація середовища з lockfile (аналог pip install -r requirements.txt)
uv sync
# Синхронізація без dev-залежностей (для production)
uv sync --no-dev
Після виконання uv add pyproject.toml оновлюється автоматично:
[project]
name = "my-web-api"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.115.6",
"uvicorn>=0.32.1",
]
[dependency-groups]
dev = [
"pytest>=8.3.4",
"ruff>=0.8.4",
"mypy>=1.13.0",
]
Запуск коду та команд
uv run запускає скрипт або команду у керованому середовищі проекту — без потреби вручну активувати середовище:
# Запуск скрипта
uv run python main.py
# Запуск застосунку
uv run uvicorn main:app --reload
# Запуск тестів
uv run pytest
# Запуск лінтера
uv run ruff check .
# uv run також може виконувати скрипти без встановлення:
# навіть якщо pytest не у залежностях — uv встановить тимчасово
uv run --with pytest pytest tests/
uv run автоматично синхронізує середовище перед запуском, якщо uv.lock та встановлені пакети не відповідають одне одному. Це робить його ідеальним для CI/CD: не потрібні окремі кроки venv create та pip install.Управління версіями Python
Одна з найпотужніших функцій uv — вбудоване управління версіями Python (аналог pyenv):
# Встановлення конкретної версії Python
uv python install 3.12
uv python install 3.11 3.13 # кілька версій одразу
# Перегляд доступних та встановлених версій
uv python list
# Прив'язка версії Python до проекту
uv python pin 3.12 # записує у .python-version
# Використання конкретної версії при ініціалізації проекту
uv init --python 3.11 legacy-service
Запуск CLI-інструментів через uvx
uvx — це аналог npx з Node.js: запускає CLI-інструменти без встановлення у проект, у тимчасовому ізольованому середовищі:
# Запуск без встановлення (uvx = uv tool run)
uvx ruff check . # запускає ruff для перевірки поточної директорії
uvx black myfile.py # форматує файл
uvx httpie GET httpbin.org/get # HTTP-клієнт для тестування API
# Встановлення інструментів глобально (available everywhere)
uv tool install ruff
uv tool install black
uv tool list # список встановлених інструментів
uvx особливо зручний у CI/CD та скриптах, де не хочеться «забруднювати» залежності проекту інструментами розробки. uvx ruff check . замість pip install ruff && ruff check ..uv як замінник pip у існуючих проектах
Якщо у вас вже є проект з requirements.txt, uv можна підключити без переходу на pyproject.toml:
# uv як прямий замінник pip (набагато швидший)
uv pip install requests
uv pip install -r requirements.txt
uv pip freeze > requirements.txt
# Створення venv через uv
uv venv .venv
source .venv/bin/activate
# Компіляція requirements (аналог pip-tools)
uv pip compile requirements.in -o requirements.txt
Довідник команд uv
pyproject.toml, README.md та .python-version.uv init # у поточній папці
uv init my-project # у новій папці my-project/
uv init --python 3.11 api # з фіксованою версією Python
pyproject.toml та uv.lock. Підтримує версійні обмеження, extras та групи.uv add fastapi # остання версія
uv add "sqlalchemy>=2.0,<3" # з обмеженнями версій
uv add uvicorn[standard] # з extras
uv add --dev pytest ruff mypy # dev-залежності
uv remove redis # видалення
uv.lock. Аналог pip install -r requirements.txt, але точний і детермінований.uv sync # встановити всі залежності
uv sync --no-dev # тільки production (без dev-групи)
uv sync --frozen # не оновлювати lockfile, лише встановити
uv run python main.py # запуск скрипта
uv run uvicorn app:app --reload # запуск застосунку
uv run pytest # тести
uv run --with httpx python -c "import httpx; print(httpx.__version__)"
pyenv для більшості сценаріїв.uv python install 3.12 # завантажити та встановити
uv python install 3.11 3.13 # кілька версій одразу
uv python list # переглянути всі доступні
uv python pin 3.12 # зафіксувати у .python-version
pip з тим самим синтаксисом, але у рази швидша. Для інтеграції з існуючими проектами без переходу на pyproject.toml.uv pip install requests # встановлення пакета
uv pip install -r requirements.txt # з файлу
uv pip freeze > requirements.txt # збереження списку
uv pip list # перелік встановлених
uv venv .venv # створення середовища
uvx запускає CLI-інструменти без встановлення у проект (у тимчасовому ізольованому середовищі). uv tool install — встановлює глобально.uvx ruff check . # запуск без встановлення
uvx black myfile.py
uvx --from httpie http GET httpbin.org/get
uv tool install ruff # встановити глобально
uv tool list # список глобальних інструментів
uv tool uninstall ruff # видалити
uv.lock. Може експортувати залежності у формат requirements.txt для інструментів, що не підтримують uv.lock.uv lock # оновити lockfile
uv export -f requirements-txt > requirements.txt # експорт у pip-формат
uv export --no-dev -f requirements-txt > req-prod.txt
Poetry — зрілий менеджер для складних проектів
Poetry — це інструмент, що вийшов у 2018 році і зараз широко використовується у виробничих проектах. Він вирішує ту саму проблему, що і uv, але з акцентом на управління залежностями з групами і публікацію пакетів на PyPI.
Автоматичне управління середовищем
python -m venv .venv && source .venv/bin/activate — достатньо poetry install.poetry.lock — точне відтворення
poetry install завжди відтворює ідентичне середовище на будь-якій машині або у CI.Групи залежностей
poetry install --without dev.Публікація на PyPI
poetry publish — єдина команда для збірки та публікації пакету. Замінює setuptools, twine і ручне редагування setup.py.Встановлення Poetry
# Рекомендований спосіб (isolated installer)
curl -sSL https://install.python-poetry.org | python3 -
# macOS через Homebrew
brew install poetry
# Перевірка встановлення
poetry --version # Poetry (version 1.8.x)
# Важливе налаштування: зберігати venv всередині проекту
# (замість ~/.cache/pypoetry/virtualenvs/)
poetry config virtualenvs.in-project true
poetry config virtualenvs.in-project true — рекомендоване налаштування для більшості розробників. Середовище буде у .venv/ всередині вашого проекту, що зручно для IDE та Docker.Створення нового проекту
# Новий проект з повною структурою
poetry new my-service
# Або ініціалізація інтерактивно у поточній папці
mkdir existing-project && cd existing-project
poetry init
pyproject.toml у Poetry
Poetry використовує pyproject.toml — але зі своїми секціями [tool.poetry.*]:
[tool.poetry]
name = "my-service"
version = "0.1.0"
description = "Production-ready REST API"
authors = ["Developer <dev@example.com>"]
readme = "README.md"
packages = [{include = "my_service"}]
[tool.poetry.dependencies]
python = "^3.12" # версія Python — обов'язкова
fastapi = ">=0.115.0" # прямі залежності проекту
sqlalchemy = "^2.0"
pydantic = "^2.5"
uvicorn = {extras = ["standard"], version = "^0.32"}
[tool.poetry.group.dev.dependencies] # група dev — не у production
pytest = "^8.0"
pytest-asyncio = "^0.24"
ruff = "^0.8"
mypy = "^1.13"
black = "^24.0"
[tool.poetry.group.test.dependencies] # окрема група для тестів
httpx = "^0.28" # HTTP-клієнт для тестування FastAPI
factory-boy = "^3.3"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
Управління залежностями
# Встановлення залежностей з pyproject.toml (також синхронізує lock)
poetry install
# Встановлення без dev-залежностей (для production)
poetry install --without dev
# Додавання нової залежності (оновлює pyproject.toml і lock автоматично)
poetry add fastapi
poetry add "sqlalchemy>=2.0,<3.0"
poetry add uvicorn[standard] # з extras
# Додавання dev-залежності
poetry add --group dev pytest
poetry add --group dev ruff black mypy
# Видалення залежності
poetry remove redis
# Оновлення всіх залежностей (в межах версійних обмежень)
poetry update
# Оновлення конкретного пакету
poetry update fastapi
Запуск коду у середовищі Poetry
Poetry не вимагає ручної активації середовища — використовуйте poetry run:
# Запуск скрипта у середовищі проекту
poetry run python main.py
# Запуск застосунку
poetry run uvicorn my_service.main:app --reload
# Запуск тестів
poetry run pytest
# Запуск лінтера
poetry run ruff check .
poetry run mypy my_service/
# Або активувати середовище вручну (як у venv)
poetry shell # відкриває нову оболонку з активованим середовищем
exit # повернутись
Довідник команд poetry
poetry new створює проект з готовою структурою папок. poetry init — інтерактивно додає pyproject.toml до існуючої директорії.poetry new my-service # новий проект зі структурою
poetry init # додати poetry до існуючого проекту
pyproject.toml та poetry.lock. Підтримує групи, extras та версійні обмеження.poetry add fastapi # остання версія
poetry add "sqlalchemy>=2.0,<3" # з обмеженнями версій
poetry add uvicorn[standard] # з extras
poetry add --group dev pytest ruff # dev-група
poetry add --group test httpx # test-група
poetry remove redis # видалення
poetry.lock. Якщо lockfile відсутній — вирішує залежності і створює його.poetry install # встановити всі залежності
poetry install --without dev # тільки production
poetry install --only test # тільки вказана група
poetry install --no-root # без встановлення самого проекту
poetry shell.poetry run python main.py # запуск скрипта
poetry run uvicorn app:app --reload # запуск застосунку
poetry run pytest # тести
poetry run ruff check . # лінтер
poetry shell # активувати оболонку
pyproject.toml. Оновлює poetry.lock.poetry update # оновити всі пакети
poetry update fastapi # оновити конкретний пакет
poetry update --dry-run # показати що зміниться (без застосування)
poetry show # список всіх встановлених
poetry show --tree # дерево залежностей
poetry show fastapi # деталі конкретного пакету
poetry show --outdated # застарілі пакети
poetry env info # поточне середовище та Python
poetry env list # всі середовища проекту
poetry env remove 3.12 # видалити середовище
poetry env use 3.11 # перемкнути на іншу версію Python
requirements.txt для інструментів, що не підтримують pyproject.toml.poetry export -f requirements.txt --output requirements.txt
poetry export -f requirements.txt --without dev --output req-prod.txt
setuptools, twine та ручне редагування setup.py.poetry build # зібрати dist/ (wheel + tar.gz)
poetry publish # опублікувати на PyPI
poetry publish --build # зібрати і опублікувати одразу
poetry publish --repository testpypi # на тестовий PyPI
Робота з git: що робити після git clone?
Один із найпоширеніших питань початківців: «Я склонував репозиторій. Що далі?». Відповідь залежить від того, яким менеджером пакетів користується проект.
Як розпізнати менеджер пакетів проекту
Першим ділом — подивіться на файли у корені репозиторію:
uv-проект. Команда для старту: uv sync.my-project/
├── pyproject.toml ← залежності та метадані
├── uv.lock ← детермінований lockfile
├── .python-version ← зафіксована версія Python
└── src/
poetry.lock — це Poetry-проект. Команда для старту: poetry install.my-project/
├── pyproject.toml ← залежності та метадані
├── poetry.lock ← детермінований lockfile
└── src/
requirements.txt — класичний підхід. Команда для старту: pip install -r requirements.txt.my-project/
├── requirements.txt ← список залежностей
└── app.py
Флоу після git clone — uv-проект
Клонувати та перейти у директорію
git clone https://github.com/org/my-project.git
cd my-project
Перевірити версію Python (опційно)
uv прочитає .python-version автоматично. Якщо потрібна версія відсутня — uv завантажить її сам:
cat .python-version # наприклад: 3.12
uv python list # перевірити що встановлено
Синхронізувати середовище
Одна команда — і все готово. uv сам створить .venv/ і встановить точні версії з uv.lock:
uv sync
Запустити проект
uv run python main.py # скрипт
uv run uvicorn app:app --reload # FastAPI
uv run pytest # тести
source .venv/bin/activate — uv run все робить сам. Але якщо хочете активувати середовище явно для IDE або оболонки: source .venv/bin/activate.Флоу після git clone — Poetry-проект
Клонувати та перейти у директорію
git clone https://github.com/org/my-project.git
cd my-project
Перевірити налаштування Poetry (один раз)
# Рекомендовано: зберігати venv всередині проекту
poetry config virtualenvs.in-project true
Встановити залежності
Poetry прочитає poetry.lock і встановить точні версії:
poetry install
Запустити проект
poetry run python main.py
poetry run uvicorn app:app --reload
poetry run pytest
Що комітити у git? Таблиця
Правило просте: lockfile завжди комітити, .venv — ніколи.
| Файл | Комітити? | Причина |
|---|---|---|
pyproject.toml | ✅ Так | Метадані та прямі залежності проекту |
uv.lock | ✅ Так | Детермінований lockfile для відтворення |
poetry.lock | ✅ Так | Детермінований lockfile для відтворення |
requirements.txt | ✅ Так | Якщо проект класичний pip |
.python-version | ✅ Так | Фіксує версію Python для команди |
.venv/ | ❌ Ні | Велика, специфічна для ОС — у .gitignore |
__pycache__/ | ❌ Ні | Кеш байткоду — у .gitignore |
*.pyc | ❌ Ні | Компільований байткод — у .gitignore |
.env | ❌ Ні | Секрети та конфіги — у .gitignore |
Стандартний .gitignore для Python-проекту
# Віртуальні середовища
.venv/
venv/
env/
# Байткод та кеш
__pycache__/
*.py[cod]
*$py.class
*.pyo
# Артефакти збірки
dist/
build/
*.egg-info/
*.egg
# Тести та покриття
.pytest_cache/
.coverage
htmlcov/
# Типи та лінтери
.mypy_cache/
.ruff_cache/
# Середовище та секрети
.env
.env.local
*.env
# IDE
.idea/
.vscode/
*.swp
Статичний аналіз типів: mypy та Pyright
Python — мова з динамічною типізацією: типи перевіряються під час виконання. Але з Python 3.5+ з'явилися анотації типів (def foo(x: int) -> str:), і на цій основі побудовані статичні аналізатори — інструменти, що знаходять помилки типів до запуску програми.
mypy
Pyright
pyright.Навіщо потрібен статичний аналіз типів?
# Без анотацій — помилка знайдеться лише під час виконання
def get_user_age(user):
return user["age"] + 1 # KeyError? TypeError? Дізнаємось пізно
# З анотаціями — mypy/Pyright знаходять помилки відразу
from typing import TypedDict
class User(TypedDict):
name: str
age: int
def get_user_age(user: User) -> int:
return user["age"] + 1 # mypy: OK
# return user["nme"] + 1 # mypy: ERROR — key "nme" не існує у User
mypy — встановлення та базове використання
# Встановлення
uv add --dev mypy
# або
pip install mypy
# Запуск для файлу
mypy main.py
# Запуск для всього проекту
mypy .
# Перевірка конкретного пакету
mypy my_package/
Конфігурація mypy у pyproject.toml
[tool.mypy]
python_version = "3.12"
strict = true # найсуворіший режим — рекомендовано для нових проектів
# Що перевіряти
check_untyped_defs = true # перевіряти навіть нетиповані функції
disallow_untyped_defs = true # вимагати анотацій для всіх функцій
disallow_any_generics = true # заборонити голі Generic (list замість list[str])
warn_return_any = true # попереджати про return Any
# Що ігнорувати
ignore_missing_imports = true # якщо бібліотека не має стабів
# Виключення конкретних модулів
[[tool.mypy.overrides]]
module = ["tests.*"]
disallow_untyped_defs = false # у тестах дозволяємо без анотацій
Практичний приклад: типовані структури даних
from typing import Optional, Union
from collections.abc import Sequence
# TypedDict — словник зі строгою структурою
from typing import TypedDict
class Address(TypedDict):
street: str
city: str
postal_code: str
class UserProfile(TypedDict, total=False): # total=False — всі поля необов'язкові
bio: str
avatar_url: str
class User(TypedDict):
id: int
name: str
email: str
address: Address
profile: UserProfile # вкладений TypedDict
# Функція з повними анотаціями
def find_users_by_city(
users: Sequence[User],
city: str,
limit: Optional[int] = None,
) -> list[User]:
"""Знаходить користувачів із зазначеного міста."""
result = [u for u in users if u["address"]["city"] == city]
if limit is not None:
result = result[:limit]
return result
# mypy перевірить:
# - що users є Sequence[User]
# - що city є str
# - що limit є Optional[int] (може бути None)
# - що функція повертає list[User]
Pyright — швидший альтернативний аналізатор
# Встановлення через uv
uv add --dev pyright
# або глобально
uvx pyright --version
# Запуск
pyright .
pyright main.py --pythonversion 3.12
Конфігурація Pyright — pyrightconfig.json
{
"pythonVersion": "3.12",
"venvPath": ".",
"venv": ".venv",
"typeCheckingMode": "strict",
"include": ["src", "tests"],
"exclude": ["**/__pycache__"],
"reportMissingImports": true,
"reportMissingTypeStubs": false,
"reportUnknownVariableType": false
}
Або у pyproject.toml:
[tool.pyright]
pythonVersion = "3.12"
venvPath = "."
venv = ".venv"
typeCheckingMode = "strict"
include = ["src"]
exclude = ["**/__pycache__"]
Порівняння mypy vs Pyright
| Характеристика | mypy | Pyright |
|---|---|---|
| Мова реалізації | Python | TypeScript |
| Швидкість | ⚠️ Повільніший на великих проектах | ⚡ Значно швидший, інкрементальний |
| Строгість | ✅ Налаштовувана | ✅ Налаштовувана |
| IDE-інтеграція | PyCharm, VSCode (mypy extension) | VSCode (Pylance базується на Pyright) |
| Підтримка стандартів | ✅ PEP 484, 526, 544... | ✅ PEP 484, 526, 544... |
| Plugins | ✅ Є (Django, SQLAlchemy) | ⚠️ Менше plugins |
| CI/CD | ✅ Стандарт де-факто | ✅ Зростає |
| Рекомендація | Зрілі проекти, Django, MLops | FastAPI, нові проекти, VSCode-workflow |
django-stubs, sqlalchemy-stubs).Інтеграція у workflow з uv
# Додавання аналізаторів у dev-залежності
uv add --dev mypy pyright
# Запуск у CI/CD або pre-commit
uv run mypy .
uv run pyright .
# Часто використовують разом з ruff (лінтер)
uv add --dev ruff
uv run ruff check .
uv run ruff format .
pyproject.toml — повна конфігурація типового проекту
[project]
name = "my-service"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["fastapi>=0.115", "sqlalchemy>=2.0"]
[dependency-groups]
dev = ["mypy>=1.13", "pyright>=1.1", "ruff>=0.8", "pytest>=8.0"]
[tool.mypy]
python_version = "3.12"
strict = true
ignore_missing_imports = true
[tool.pyright]
pythonVersion = "3.12"
typeCheckingMode = "basic"
venvPath = "."
venv = ".venv"
[tool.ruff]
line-length = 88
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "UP"]
Порівняльна таблиця: venv+pip vs uv vs Poetry
| Можливість | venv + pip | uv | Poetry |
|---|---|---|---|
| Швидкість встановлення | 🐢 Базова | ⚡ 10–100x швидше | 🐇 Швидша за pip |
| Lockfile | ❌ Немає (лише freeze) | ✅ uv.lock | ✅ poetry.lock |
| Управління версіями Python | ❌ Ні | ✅ Вбудовано | ❌ Потрібен pyenv |
| Групи залежностей | ❌ Ні | ✅ dev groups | ✅ dev/test/docs groups |
| Автоматичне середовище | ❌ Вручну | ✅ uv run | ✅ poetry run |
| Публікація на PyPI | ❌ Потрібен twine | ❌ Не підтримується | ✅ Вбудовано |
| Сумісність зі стандартами | ✅ Стандарт | ✅ PEP 621 | ⚠️ Частково PEP 621 |
| Навчальна крива | ✅ Проста | ✅ Проста | ⚠️ Середня |
| Зрілість і поширеність | ✅ Стандарт | 🆕 Активно зростає | ✅ Широко у production |
Обирайте venv + pip
Обирайте uv
Обирайте Poetry
Частина V: Стандартна бібліотека — «Батарейки у комплекті»
Філософія "Batteries Included"
Одна з найсильніших сторін Python — філософія "batteries included" (батарейки у комплекті). Це означає, що разом з інтерпретатором ви отримуєте величезний набір готових, оптимізованих та протестованих модулів стандартної бібліотеки. Вони покривають більшість повсякденних завдань розробника: від математичних обчислень до мережевої взаємодії та роботи з файловою системою.
Використання стандартної бібліотеки гарантує:
- Портативність: ваш код працюватиме на будь-якому комп'ютері, де встановлено Python, без необхідності встановлювати сторонні залежності.
- Стабільність: ці модулі підтримуються та оновлюються ядром команди розробників Python.
- Швидкість: багато критичних частин стандартної бібліотеки написані на мові C для максимальної продуктивності.
Розглянемо ключові модулі, які формують основу розробки на Python.
Математичні обчислення: модуль math
Модуль math надає доступ до математичних функцій та констант, визначених стандартом мови C. Він працює виключно з дійсними числами (для комплексних чисел існує окремий модуль cmath).
Константи та округлення
import math
# Ключові константи
print(f"pi: {math.pi}") # 3.141592653589793
print(f"e: {math.e}") # 2.718281828459045
print(f"inf: {math.inf}") # Позитивна нескінченність
print(f"nan: {math.nan}") # Not a Number (не число)
# Стратегії округлення
value = 5.67
print(f"Округлення вгору (ceil): {math.ceil(value)}") # 6
print(f"Округлення вниз (floor): {math.floor(value)}") # 5
print(f"Відкидання дробової частини (trunc): {math.trunc(value)}") # 5
Степені, корені та логарифми
# Квадратний корінь
print(math.sqrt(25)) # 5.0 (завжди повертає float)
# Найбільший спільний дільник (НСД) та найменше спільне кратне (НСК)
print(math.gcd(48, 64)) # 16
print(math.lcm(48, 64)) # 192 (доступно в Python 3.9+)
# Логарифми
print(math.log(math.e)) # 1.0 (натуральний логарифм)
print(math.log2(1024)) # 10.0
print(math.log10(1000)) # 3.0
round(), яка не потребує імпорту math. Але пам'ятайте, що round() у Python використовує банківське округлення (округлює до найближчого парного числа при закінченні на .5, наприклад round(2.5) буде 2, а round(3.5) буде 4).Псевдовипадковість: модуль random
Модуль random генерує псевдовипадкові числа за допомогою алгоритму Вихру Мерсенна (Mersenne Twister). Він підходить для моделювання, симуляцій та ігор, але не є безпечним для криптографії (для безпечних токенів або паролів використовуйте модуль secrets).
Генерація значень
import random
# Дійсне число від 0.0 до 1.0
print(f"random(): {random.random()}")
# Ціле число в діапазоні [1, 10] включно
print(f"randint(1, 10): {random.randint(1, 10)}")
# Дійсне число в діапазоні [1.5, 9.5]
print(f"uniform(1.5, 9.5): {random.uniform(1.5, 9.5)}")
# Випадкове парне число від 0 до 100
print(f"randrange(0, 101, 2): {random.randrange(0, 101, 2)}")
Робота з послідовностями
cards = ["Туз", "Король", "Дама", "Валет", "10"]
# Вибір одного випадкового елемента
print(f"Випадкова карта: {random.choice(cards)}")
# Вибір кількох унікальних елементів (без повторень)
print(f"Роздача на двох: {random.sample(cards, 2)}")
# Перемішування списку на місці (модифікує оригінал!)
random.shuffle(cards)
print(f"Перемішана колода: {cards}")
Концепція відтворюваності: random.seed()
Алгоритм генерації є математичним, тому якщо задати початкову точку (seed), послідовність "випадкових" чисел буде абсолютно ідентичною при кожному запуску.
random.seed(100)
print(random.randint(1, 100)) # Завжди виведе 19
print(random.randint(1, 100)) # Завжди виведе 59
random.seed(100) # Скидаємо в ту саму початкову точку
print(random.randint(1, 100)) # Знову 19!
random.seed() у модульних тестах, коли вам потрібно протестувати логіку, яка залежить від випадкових факторів, щоб забезпечити стабільні та передбачувані результати тестів.Робота з часом: модуль datetime
Час є однією з найскладніших концепцій у програмуванні через високосні роки, часові пояси та переходи на літній час. Модуль datetime надає класи для обробки дат і часу.
Основні класи
datetime.date: представлення дати (рік, місяць, день).datetime.time: представлення часу (година, хвилина, секунда, мікросекунда).datetime.datetime: комбінація дати та часу.datetime.timedelta: тривалість або різниця між двома датами/часом.
Створення та арифметика
import datetime
# Поточні значення
now = datetime.datetime.now()
today = datetime.date.today()
print(f"Зараз: {now}")
print(f"Сьогодні: {today}")
# Створення конкретної дати
deadline = datetime.datetime(2026, 12, 31, 23, 59, 0)
# Розрахунок різниці (timedelta)
time_left = deadline - now
print(f"До дедлайну залишилося: {time_left.days} днів та {time_left.seconds // 3600} годин")
# Додавання інтервалу часу
future_date = today + datetime.timedelta(weeks=4)
print(f"Через 4 тижні буде: {future_date}")
Форматування та парсинг
Для перетворення часу в рядок використовується метод strftime (format time), а для зворотного парсингу з рядка — strptime (parse time).
import datetime
now = datetime.datetime.now()
# Використовуємо спеціальні директиви (специфікатори)
formatted = now.strftime("%Y-%m-%d %H:%M:%S")
print(formatted) # "2026-06-13 20:30:15"
human_friendly = now.strftime("%d %B %Y, (%A)")
print(human_friendly) # "13 June 2026, (Saturday)"
import datetime
date_str = "15/09/2025 18:45"
# Шаблон MUST точно збігатися з форматом рядка
parsed = datetime.datetime.strptime(date_str, "%d/%m/%Y %H:%M")
print(parsed) # 2025-09-15 18:45:00
print(type(parsed)) # <class 'datetime.datetime'>
Naive vs Aware об'єкти часу
За замовчуванням об'єкти datetime є naive (наївними) — вони не містять інформації про часовий пояс. Це небезпечно для серверних систем, де клієнти можуть бути з різних куточків світу. Об'єкти з інформацією про часовий пояс називаються aware (усвідомленими).
З Python 3.9 вбудовано модуль zoneinfo для зручної роботи з базою даних часових поясів IANA:
from datetime import datetime
from zoneinfo import ZoneInfo
# Створення часу в часовому поясі Києва
kyiv_time = datetime.now(ZoneInfo("Europe/Kyiv"))
print(f"Київ: {kyiv_time}") # Наприкінці з'явиться зміщення UTC: +03:00 (або +02:00)
# Конвертація в часовий пояс Нью-Йорка
ny_time = kyiv_time.astimezone(ZoneInfo("America/New_York"))
print(f"Нью-Йорк: {ny_time}") # Час автоматично перераховується
Системна взаємодія: os vs sys vs pathlib
Ці модулі забезпечують взаємодію з операційною системою та інтерпретатором Python.
Порівняння призначення os та sys
os: відповідає за роботу з операційною системою (робота з файлами на диску, створення папок, змінні оточення ОС, системні виклики).sys: відповідає за взаємодію з самим інтерпретатором Python (параметри запуску скрипта, керування шляхами імпортуsys.path, внутрішні налаштування пам'яті та лімітів рекурсії).
pathlib — сучасний стандарт для роботи зі шляхами
Раніше для роботи зі шляхами використовувався модуль os.path. Сьогодні рекомендованим є модуль pathlib, який надає об'єктно-орієнтований підхід до шляхів.
from pathlib import Path
# Створення об'єкта шляху
project_dir = Path("/home/user/project")
# Об'єднання шляхів через оператор '/' (дуже зручно!)
file_path = project_dir / "src" / "config.json"
print(file_path) # /home/user/project/src/config.json
# Перевірки та операції
print(f"Чи існує? {file_path.exists()}")
print(f"Розширення: {file_path.suffix}")
print(f"Ім'я файлу: {file_path.name}")
# Створення папки разом із батьківськими
Path("new_folder/sub_folder").mkdir(parents=True, exist_ok=True)
import os
project_dir = "/home/user/project"
# Об'єднання шляхів через os.path.join
file_path = os.path.join(project_dir, "src", "config.json")
print(file_path) # /home/user/project/src/config.json
# Перевірки та операції
print(f"Чи існує? {os.path.exists(file_path)}")
print(f"Розширення: {os.path.splitext(file_path)[1]}")
print(f"Ім'я файлу: {os.path.basename(file_path)}")
# Створення папки
os.makedirs("new_folder/sub_folder", exist_ok=True)
Ключові можливості sys
import sys
# Аргументи командного рядка (наприклад: python main.py data.txt)
# sys.argv[0] — це завжди ім'я самого скрипта
print(f"Аргументи запуску: {sys.argv}")
# Платформа та версія
print(sys.platform) # 'darwin' (macOS), 'win32' (Windows), 'linux' (Linux)
print(sys.version) # Детальна версія інтерпретатора
# Безпечне завершення роботи скрипта з кодом виходу
# 0 = успіх, будь-яке інше число = помилка
sys.exit(0)
Спеціалізовані контейнери: модуль collections
Стандартні типи dict, list, set та tuple чудово підходять для більшості завдань, але модуль collections надає альтернативні контейнери з розширеною поведінкою.
namedtuple — кортеж з іменованими полями
Коли вам потрібен легковажний, незмінний об'єкт (наприклад, для представлення точки на карті чи запису користувача), писати повноцінний клас занадто довго. Звичайний кортеж Point = (50.45, 30.52) змушує звертатися за індексами Point[0], що шкодить читабельності.
from collections import namedtuple
# Створюємо тип (фабрику) Geopoint з полями latitude та longitude
Geopoint = namedtuple("Geopoint", ["latitude", "longitude"])
# Створюємо екземпляр
kyiv = Geopoint(latitude=50.4501, longitude=30.5234)
# Звернення за іменами полів!
print(f"Широта: {kyiv.latitude}, Довгота: {kyiv.longitude}")
# Але це все ще кортеж (сумісність збережена)
print(f"Індекс 0: {kyiv[0]}")
lat, lon = kyiv # Розпакування працює
defaultdict — словник зі значенням за замовчуванням
При роботі зі звичайним dict звернення до неіснуючого ключа викликає KeyError. defaultdict автоматично створює значення за замовчуванням при першому зверненні до нового ключа.
from collections import defaultdict
# Словник, який за замовчуванням створює порожній список (list) для нових ключів
grouped_students = defaultdict(list)
# Нам не потрібно перевіряти, чи є ключ "Python" у словнику!
grouped_students["Python"].append("Олег")
grouped_students["Python"].append("Марія")
grouped_students["Go"].append("Іван")
print(grouped_students)
# defaultdict(<class 'list'>, {'Python': ['Олег', 'Марія'], 'Go': ['Іван']})
Counter — зручний лічильник елементів
Клас Counter призначений для швидкого підрахунку кількості об'єктів. Він є підкласом dict, тому успадковує всі його методи.
from collections import Counter
words = ["яблуко", "банан", "яблуко", "апельсин", "банан", "яблуко"]
counter = Counter(words)
print(counter) # Counter({'яблуко': 3, 'банан': 2, 'апельсин': 1})
print(f"Кількість бананів: {counter['банан']}")
print(f"Кількість неіснуючих елементів: {counter['груша']}") # Повертає 0 замість KeyError!
# Отримання топ-N найпопулярніших елементів
print(f"Найпопулярніші: {counter.most_common(2)}") # [('яблуко', 3), ('банан', 2)]
deque — оптимізована двостороння черга
Звичайний список Python list оптимізований для операцій у кінці структури. Додавання або видалення елемента з початку списку (list.insert(0, val) або list.pop(0)) має складність $O(N)$, оскільки всі інші елементи в пам'яті мають зсунутися на один крок.
deque (double-ended queue) реалізований як двобічно зв'язаний список, що робить додавання та видалення елементів з обох кінців надзвичайно швидким — за $O(1)$.
from collections import deque
queue = deque(["Користувач 1", "Користувач 2"])
# Швидке додавання в кінець
queue.append("Користувач 3")
# Швидке додавання на початок
queue.appendleft("VIP Користувач")
print(queue) # deque(['VIP Користувач', 'Користувач 1', ...])
# Видалення з обох кінців
first_in = queue.popleft() # 'VIP Користувач'
last_in = queue.pop() # 'Користувач 3'
deque для реалізації стеків, черг завдань (FIFO/LIFO) або для зберігання логів фіксованого розміру (задавши параметр maxlen при створенні, наприклад deque(maxlen=100) — при додаванні 101-го елемента перший автоматично видалиться).Повний цикл роботи: від нуля до готового проекту
Це стандартна послідовність дій при створенні нового Python-проекту:
# 1. Створити директорію проекту
mkdir my-awesome-project
cd my-awesome-project
# 2. Ініціалізувати git-репозиторій (опціонально, але рекомендується)
git init
# 3. Створити файл .gitignore — ОДРАЗУ, до будь-яких комітів
cat > .gitignore << 'EOF'
# Віртуальне середовище — ніколи не комітимо!
.venv/
venv/
# Байткод Python
__pycache__/
*.pyc
*.pyo
# Середовища та IDE
.env
.DS_Store
.idea/
.vscode/
EOF
# 4. Створити та активувати venv
python -m venv .venv
source .venv/bin/activate # (.venv) з'явиться у промпті
# 5. Встановити залежності проекту
pip install requests django pytest
# 6. Зафіксувати залежності
pip freeze > requirements.txt
# 7. Написати код...
# 8. Перед завершенням — оновити requirements.txt
pip freeze > requirements.txt
git add requirements.txt
git commit -m "chore: update dependencies"
.venv/ у Git-репозиторій. Вона містить сотні файлів і повністю залежить від конкретної машини та версії Python. Замість цього комітьте requirements.txt — це єдине, що потрібно колезі або CI/CD для відтворення вашого середовища.Практичний приклад від А до Я: CLI-інструмент завантаження та оптимізації зображень
Щоб об'єднати всі вивчені концепції (модулі, пакети, venv, sys.path, sys.modules, sys.argv, Counter та pathlib) в єдине ціле, створимо реальний виробничий інструмент командного рядка — Image Processor CLI.
Постановка задачі
Нам потрібно написати утиліту, яка:
- Приймає текстовий файл зі списком URL-адрес зображень.
- Створює ізольовану вихідну директорію для збереження результатів.
- Завантажує зображення з мережі (потребує стороннього пакета
requests). - Змінює розмір зображень до заданої ширини та конвертує їх у сучасний оптимізований формат WebP (потребує стороннього пакета
Pillow). - Збирає детальну статистику про хід виконання (успіх, помилка мережі, помилка обробки) за допомогою
collections.Counterта повертає відповідний код виходу в систему.
Архітектура проекту
Проект буде організований як повноцінний пакет Python з такою структурою директорій:
Пакетна реалізація проекту крок за кроком
Налаштування директорії та ізоляції (venv)
Створимо структуру папок та ініціалізуємо ізольоване віртуальне середовище, щоб сторонні пакети requests та Pillow не конфліктували з іншими версіями у глобальній системі.
Встановлення та фіксація залежностей
Встановимо необхідні сторонні бібліотеки через pip та запишемо їх у requirements.txt.
Створення вихідних файлів коду
Запишіть код для кожного з файлів структури проекту image_processor відповідно до архітектурного опису, наведеного вище.
image_processor/__init__.py:
from .core.downloader import download_image
Це гарантує локальну цілісність пакета. Натомість у файлі main.py, який виступає точкою запуску, використовуються абсолютні імпорти:
from image_processor import process_images
оскільки при запуску через python -m точка входу має викликатися з кореня sys.path.Підготовка тестових даних
Створимо файл urls.txt в корені проекту my_project/ зі списком реальних тестових зображень для завантаження:
# urls.txt
# Список зображень для оптимізації
https://images.unsplash.com/photo-1579783900882-c0d3dad7b119?w=1200
https://images.unsplash.com/photo-1541701494587-cb58502866ab?w=1200
# Невалідний URL для тестування стійкості до помилок:
https://non-existent-domain-xyz.com/image.jpg
Запуск та аналіз роботи CLI
Запустимо наш пакет із кореневої папки проекту за допомогою прапорця інтерпретатора -m. Ми вкажемо вхідний файл urls.txt, папку збереження optimized_images та бажану ширину 600 пікселів.
Перевірка результатів на диску
Перевіримо вміст автоматично створеної директорії optimized_images за допомогою pathlib-подібних команд або звичайної утиліти перегляду.
Порівняльна таблиця: стратегії імпорту
| Синтаксис | Простір імен | Ризик конфлікту | Читабельність | Рекомендація |
|---|---|---|---|---|
import module | ізольований | ❌ мінімальний | ✅ висока | ✅ основний підхід |
from module import name | поточний | ⚠️ є | ⚠️ середня | ✅ для 1-3 об'єктів |
import module as alias | ізольований | ❌ мінімальний | ✅ висока (якщо alias відомий) | ✅ для довгих імен |
from module import * | поточний | ❌❌ гарантований | ❌ низька | ❌ уникати завжди |
Підсумки та найкращі практики
Модулі
.py-файл — модуль. Ділить код на логічні одиниці. Завантажується один раз, кешується у sys.modules. Пишіть if __name__ == "__main__" для двоїстих файлів.sys.path і sys.modules
sys.path — список директорій для пошуку. sys.modules — кеш завантажених модулів. Розуміння цих двох структур пояснює більшість «магії» імпорту.Пакети
__init__.py. Організує модулі ієрархічно. __init__.py слугує фасадом публічного API. __all__ документує контракт модуля.venv + pip
uv
Poetry
Золоті правила, що варто запам'ятати:
- ✅ Завжди використовуйте ізольоване середовище для кожного проекту (
venv,uv venvабо Poetry) - ✅ Завжди додавайте
.venv/у.gitignore - ✅ Завжди фіксуйте залежності:
pip freeze > requirements.txtабо через lockfile (uv.lock/poetry.lock) - ✅ Для нових проектів — обирайте
uv(швидкість + простота) абоPoetry(якщо плануєте публікацію на PyPI) - ✅ Використовуйте
import moduleабоfrom module import name, уникайтеfrom module import * - ✅ Пишіть
if __name__ == "__main__":у кожному файлі, що може запускатися напряму - ✅ Структуруйте код у пакети при наявності більш ніж 5–7 модулів
- ✅ Використовуйте абсолютні імпорти між пакетами, відносні — всередині одного пакета
Пакування та розповсюдження JavaFX-додатків
Від вихідного коду до готового інсталятора: створення Fat JAR, використання jpackage для генерації нативних інсталяторів під Windows, macOS та Linux, публікація релізів у GitHub Releases.
Класи та Об'єкти
Глибоке дослідження механізму класів і об'єктів у Python — від фундаментальних принципів інстанціювання до CPython internals, різниці між __init__ та __new__, оптимізації пам'яті через __slots__ та природи self як неявного першого аргументу.