Python для начинающих: как приручить модули и относительные импорты
Когда скрипт вырастает до 300+ строк, его уже страшно открывать, не то что править. Решение простое и взрослое — разбивать код на модули и пакеты. Давай разберёмся, как это делать аккуратно и зачем вообще нужны относительные импорты.
---
## Модули и пакеты по-простому
- Модуль — любой
- Пакет — папка с файлом
Пример структуры проекта:
Теперь
---
## Абсолютный импорт
Самый прямолинейный способ:
Плюс: ясно, откуда что приезжает.
Минус: если переименуешь пакет
---
## Зачем нужны относительные импорты
Внутри пакета можно ссылаться на «соседей» относительно текущего модуля, а не от корня проекта.
Используются точки:
-
-
-
### Пример
Это удобно, если структура пакетов логична сама по себе и ты не хочешь, чтобы имя «корневого» пакета было зашито в каждый импорт.
---
## Важный момент: как запускать проект
Относительные импорты корректно работают, если запускать код как модуль, а не «проваливаться» внутрь пакета.
Правильно:
Неправильно:
Когда запускаешь модуль через
---
## Практический мини-шаблон
---
Главная идея: модули — это способ разрезать код на логические куски, а относительные импорты — способ связывать эти куски так, чтобы проект можно было безболезненно переорганизовывать. Как только твой файл перестанет влезать «на один экран», самое время завести свой первый пакет.
Когда скрипт вырастает до 300+ строк, его уже страшно открывать, не то что править. Решение простое и взрослое — разбивать код на модули и пакеты. Давай разберёмся, как это делать аккуратно и зачем вообще нужны относительные импорты.
---
## Модули и пакеты по-простому
- Модуль — любой
.py файл. - Пакет — папка с файлом
__init__.py (может быть пустым).Пример структуры проекта:
my_project/
app.py
core/
__init__.py
models.py
services.py
utils/
__init__.py
validators.py
Теперь
core и core.utils — это пакеты, а остальные .py — модули.---
## Абсолютный импорт
Самый прямолинейный способ:
# app.py
from core.models import User
from core.services import UserService
from core.utils.validators import validate_email
Плюс: ясно, откуда что приезжает.
Минус: если переименуешь пакет
core в backend, придется править полпроекта.---
## Зачем нужны относительные импорты
Внутри пакета можно ссылаться на «соседей» относительно текущего модуля, а не от корня проекта.
Используются точки:
-
. — текущий пакет -
.. — на уровень вверх -
... — на два уровня вверх и т.д.### Пример
# core/services.py
from .models import User # из того же пакета core
from .utils.validators import validate_email # из подпакета utils
# core/utils/validators.py
from ..models import User # поднялись из core/utils в core и взяли models
Это удобно, если структура пакетов логична сама по себе и ты не хочешь, чтобы имя «корневого» пакета было зашито в каждый импорт.
---
## Важный момент: как запускать проект
Относительные импорты корректно работают, если запускать код как модуль, а не «проваливаться» внутрь пакета.
Правильно:
python -m my_project.app
Неправильно:
cd my_project/core
python services.py # относительные импорты могут упасть
Когда запускаешь модуль через
-m, Python понимает структуру пакетов и не путается с путями.---
## Практический мини-шаблон
# core/models.py
class User:
def __init__(self, email: str):
self.email = email
# core/utils/validators.py
def validate_email(email: str) -> bool:
return "@" in email and "." in email
# core/services.py
from .models import User
from .utils.validators import validate_email
def create_user(email: str) -> User:
if not validate_email(email):
raise ValueError("Invalid email")
return User(email=email)
# app.py
from core.services import create_user
if __name__ == "__main__":
user = create_user("test@example.com")
print(user.email)
---
Главная идея: модули — это способ разрезать код на логические куски, а относительные импорты — способ связывать эти куски так, чтобы проект можно было безболезненно переорганизовывать. Как только твой файл перестанет влезать «на один экран», самое время завести свой первый пакет.
👍2🔥1
Generics в Python: зачем они нужны и как ими пользоваться
Если вы уже видели
---
### Зачем нужны generics
Generics позволяют описать шаблон типа: однажды задаёте логику, а потом подставляете конкретные типы. Например, у вас есть класс
---
### Базовый пример с
Минимальный кирпичик generics —
Функция одна, логика одна, но тип результата зависит от типа элементов списка.
---
### Обобщённый класс
Сделаем свой
---
### Ограничения типов (
Иногда нужно разрешить не «любой тип», а только наследников какого-то базового класса.
Теперь
---
### Generic по ключам и значениям
Можно описывать несколько параметров типов:
---
### Где это реально помогает
- IDE подсказывает корректные типы в ваших обобщённых функциях и классах.
- Mypy/pyright ловят ошибки:
- Код становится самодокументируемым: сигнатура явно показывает, что функция/класс универсальны.
Generics — это не про «сложность ради сложности», а про то, чтобы один раз описать универсальную логику и дальше безопасно переиспользовать её с разными типами.
Если вы уже видели
List[int] или Dict[str, float] и подумали: «Окей, типы, понятно», — то generics (обобщения) — следующий шаг. С их помощью вы можете описывать свои универсальные типы так же, как это делает стандартная библиотека.---
### Зачем нужны generics
Generics позволяют описать шаблон типа: однажды задаёте логику, а потом подставляете конкретные типы. Например, у вас есть класс
Box, который хранит «что угодно», но вы хотите, чтобы для конкретного использования Python (и IDE) знали, что именно внутри: Box[int], Box[str] и т.д.---
### Базовый пример с
TypeVarМинимальный кирпичик generics —
TypeVar:from typing import TypeVar, List
T = TypeVar("T")
def first(items: List[T]) -> T | None:
if not items:
return None
return items[0]
nums = first([1, 2, 3]) # -> int | None
names = first(["a", "b", "c"]) # -> str | None
Функция одна, логика одна, но тип результата зависит от типа элементов списка.
---
### Обобщённый класс
Сделаем свой
Box, который «знает», что он хранит:from typing import Generic, TypeVar
T = TypeVar("T")
class Box(Generic[T]):
def __init__(self, value: T) -> None:
self.value = value
def get(self) -> T:
return self.value
def set(self, value: T) -> None:
self.value = value
user_box = Box[str]("admin")
age_box = Box[int](42)
reveal_user = user_box.get() # str
reveal_age = age_box.get() # int
Box[T] — это шаблон. А Box[int], Box[str] — уже конкретные типы.---
### Ограничения типов (
bound и constraints)Иногда нужно разрешить не «любой тип», а только наследников какого-то базового класса.
from typing import TypeVar, Protocol
class SupportsId(Protocol):
id: int
U = TypeVar("U", bound=SupportsId)
def get_id(obj: U) -> int:
return obj.id
Теперь
get_id принимает только объекты, у которых есть id: int.---
### Generic по ключам и значениям
Можно описывать несколько параметров типов:
from typing import TypeVar, Dict, Generic
K = TypeVar("K")
V = TypeVar("V")
class SimpleCache(Generic[K, V]):
def __init__(self) -> None:
self._data: Dict[K, V] = {}
def set(self, key: K, value: V) -> None:
self._data[key] = value
def get(self, key: K) -> V | None:
return self._data.get(key)
user_cache = SimpleCache[int, str]()
user_cache.set(1, "Alice")
name = user_cache.get(1) # str | None
---
### Где это реально помогает
- IDE подсказывает корректные типы в ваших обобщённых функциях и классах.
- Mypy/pyright ловят ошибки:
Box[int] уже не позволит положить внутрь строку. - Код становится самодокументируемым: сигнатура явно показывает, что функция/класс универсальны.
Generics — это не про «сложность ради сложности», а про то, чтобы один раз описать универсальную логику и дальше безопасно переиспользовать её с разными типами.
👍2🔥1
### Работа с фиксированными структурами данных с помощью
Когда Pythonу приходится общаться с «железом» или чужим бинарным форматом (сетевой протокол, файл игры, формат датасета), удобные списки и словари уже не спасают. Там правит мир байтов и фиксированных структур. Для таких задач в стандартной библиотеке есть модуль
---
## Зачем нужен
- упаковывать Python-значения в байтовую строку фиксированного формата;
- распаковывать байты обратно в числа, строки и т.п.;
- контролировать порядок байт (big-endian, little-endian) и выравнивание.
Формат описывается строкой: типы идут подряд:
-
-
-
-
-
- префикс
---
## Пример 1. Упаковка заголовка файла
Допустим, у нас есть простой бинарный формат:
-
-
-
А теперь распакуем:
Важно:
---
## Пример 2. Фиксированная строка в структуре
Частая задача: записать имя пользователя длиной ровно 16 байт.
Так обрабатываются C-подобные структуры, где строки всегда занимают ровно N байт.
---
## Пример 3. Чтение бинарного файла по записям
Представим файл, состоящий из записей:
-
-
---
structКогда Pythonу приходится общаться с «железом» или чужим бинарным форматом (сетевой протокол, файл игры, формат датасета), удобные списки и словари уже не спасают. Там правит мир байтов и фиксированных структур. Для таких задач в стандартной библиотеке есть модуль
struct.---
## Зачем нужен
structstruct умеет:- упаковывать Python-значения в байтовую строку фиксированного формата;
- распаковывать байты обратно в числа, строки и т.п.;
- контролировать порядок байт (big-endian, little-endian) и выравнивание.
Формат описывается строкой: типы идут подряд:
-
i — 4-байтовое целое (int)-
f — 4-байтовый float-
d — 8-байтовый float (double)-
h — 2-байтовое целое (short)-
s — строка фиксированной длины (10s — 10 байт)- префикс
> — big-endian, < — little-endian, ! — сетевой порядок (big-endian)---
## Пример 1. Упаковка заголовка файла
Допустим, у нас есть простой бинарный формат:
-
magic — 4 байта, строка -
version — 1 байт, число -
records_count — 4 байта, число (little-endian)import struct
fmt = "<4sBI" # 4s - 4 байта строки, B - unsigned char, I - unsigned int
magic = b"DATA"
version = 1
records_count = 42
header_bytes = struct.pack(fmt, magic, version, records_count)
print(header_bytes) # b'DATA\x01*\x00\x00\x00'
print(len(header_bytes)) # 9 байт
А теперь распакуем:
unpacked = struct.unpack(fmt, header_bytes)
magic_u, version_u, records_count_u = unpacked
print(magic_u, version_u, records_count_u)
Важно:
struct всегда возвращает кортеж — даже если значение одно.---
## Пример 2. Фиксированная строка в структуре
Частая задача: записать имя пользователя длиной ровно 16 байт.
import struct
fmt = "<I16s" # I - id, 16s - имя фиксированной длины
user_id = 7
name = "Alice"
name_bytes = name.encode("utf-8")
name_padded = name_bytes.ljust(16, b"\x00") # добиваем нулями
packed = struct.pack(fmt, user_id, name_padded)
print(packed)
user_id_u, raw_name = struct.unpack(fmt, packed)
name_u = raw_name.rstrip(b"\x00").decode("utf-8")
print(user_id_u, name_u)
Так обрабатываются C-подобные структуры, где строки всегда занимают ровно N байт.
---
## Пример 3. Чтение бинарного файла по записям
Представим файл, состоящий из записей:
-
timestamp — double (8 байт)-
value — float (4 байта)import struct
record_fmt = "<df"
record_size = struct.calcsize(record_fmt)
def iter_records(path):
with open(path, "rb") as f:
while chunk := f.read(record_size):
if len(chunk) < record_size:
break
yield struct.unpack(record_fmt, chunk)
for ts, value in iter_records("data.bin"):
print(ts, value)
struct.calcsize гарантирует, что вы читаете ровно столько байт, сколько занимает одна структура.---
struct — это мост между удобным миром Python и жесткими бинарными протоколами. Если вы когда-нибудь захотите написать свой формат файла или разобрать чужой, этот модуль станет вашим основным инструментом.❤2👍1
Создаем мини-базу данных на JSON: простой путь без SQL
Иногда для небольших проектов полноценная база данных — это как использовать экскаватор, чтобы посадить цветок. Для заметок, простого TODO, небольшого чата или прототипа вполне хватит обычного файла JSON. Давай сделаем свою мини-БД шаг за шагом.
---
### Почему JSON?
- Читается человеком (открыл файл — и сразу видно данные).
- Встроенная поддержка в Python (
- Легко переносить между проектами и языками.
---
### Базовые операции «мини-БД»
Нам нужны минимум 4 вещи:
1. Сохранить данные
2. Прочитать данные
3. Добавить запись
4. Найти запись по условию
Создадим файл
Теперь напишем функции для работы с записями. Пусть это будет мини-БД пользователей:
Использование:
---
### Добавляем «фишки»: фильтрация и обновление
Мини-БД станет полезнее, если можно будет:
- фильтровать по произвольному условию
- обновлять записи
Пример:
---
### Когда такой подход годится
Подходит, если:
- проект маленький;
- данных мало (до нескольких тысяч записей);
- не нужны сложные запросы и транзакции.
Если же требуется многопользовательский доступ, высокая нагрузка, сложная аналитика — тогда пора смотреть в сторону SQLite, PostgreSQL и других БД.
Но для начала изучения Python и быстрых прототипов JSON-«база» — отличный и наглядный инструмент, который ты полностью контролируешь.
Иногда для небольших проектов полноценная база данных — это как использовать экскаватор, чтобы посадить цветок. Для заметок, простого TODO, небольшого чата или прототипа вполне хватит обычного файла JSON. Давай сделаем свою мини-БД шаг за шагом.
---
### Почему JSON?
- Читается человеком (открыл файл — и сразу видно данные).
- Встроенная поддержка в Python (
json модуль).- Легко переносить между проектами и языками.
---
### Базовые операции «мини-БД»
Нам нужны минимум 4 вещи:
1. Сохранить данные
2. Прочитать данные
3. Добавить запись
4. Найти запись по условию
Создадим файл
db.json и модуль mini_db.py.# mini_db.py
import json
from pathlib import Path
DB_FILE = Path("db.json")
def load_data():
if not DB_FILE.exists():
return []
with DB_FILE.open("r", encoding="utf-8") as f:
return json.load(f)
def save_data(data):
with DB_FILE.open("w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
Теперь напишем функции для работы с записями. Пусть это будет мини-БД пользователей:
def add_user(name, age):
data = load_data()
new_id = (max((item["id"] for item in data), default=0) + 1)
user = {"id": new_id, "name": name, "age": age}
data.append(user)
save_data(data)
return user
def find_users_by_name(name):
data = load_data()
return [item for item in data if item["name"] == name]
Использование:
if __name__ == "__main__":
add_user("Alice", 25)
add_user("Bob", 30)
print(find_users_by_name("Alice"))
---
### Добавляем «фишки»: фильтрация и обновление
Мини-БД станет полезнее, если можно будет:
- фильтровать по произвольному условию
- обновлять записи
def filter_users(condition):
data = load_data()
return [item for item in data if condition(item)]
def update_user(user_id, **fields):
data = load_data()
updated = None
for item in data:
if item["id"] == user_id:
item.update(fields)
updated = item
break
if updated:
save_data(data)
return updated
Пример:
# Все пользователи старше 20
adults = filter_users(lambda u: u["age"] > 20)
# Обновляем возраст
update_user(1, age=26)
---
### Когда такой подход годится
Подходит, если:
- проект маленький;
- данных мало (до нескольких тысяч записей);
- не нужны сложные запросы и транзакции.
Если же требуется многопользовательский доступ, высокая нагрузка, сложная аналитика — тогда пора смотреть в сторону SQLite, PostgreSQL и других БД.
Но для начала изучения Python и быстрых прототипов JSON-«база» — отличный и наглядный инструмент, который ты полностью контролируешь.
👍2
Изучение
Большинство задач в Python решаются на обычных (действительных) числах. Но как только появляются углы, корни из отрицательных чисел или работа с сигналами — без комплексных чисел не обойтись. За них в Python отвечают два модуля:
---
###
Основные возможности:
- тригонометрия:
- корни, логарифмы, экспонента:
- константы:
---
###
Ключевые функции и особенности:
- те же
- полярная форма:
---
### Как понять, что тебе нужен
- Нужен корень из «подозрительного» выражения (может стать отрицательным)?
- Работаешь с сигналами, Фурье, электрическими цепями, квантовой механикой?
- В формулах явно фигурирует
Тогда используй
---
Итого:
-
-
Понимание разницы между ними — первый шаг к задачам уровнем выше простых калькуляторных вычислений.
math и cmath: когда числа становятся сложнымиБольшинство задач в Python решаются на обычных (действительных) числах. Но как только появляются углы, корни из отрицательных чисел или работа с сигналами — без комплексных чисел не обойтись. За них в Python отвечают два модуля:
math и cmath.---
###
math: строгий мир действительных чиселmath работает только с int и float. Попробуйте передать комплексное число — получите ошибку.Основные возможности:
- тригонометрия:
sin, cos, tan, переводы радиан/градусов;- корни, логарифмы, экспонента:
sqrt, log, exp;- константы:
pi, e, tau.import math
angle_deg = 60
angle_rad = math.radians(angle_deg)
h = 10
base = h * math.cos(angle_rad)
height = h * math.sin(angle_rad)
print(base, height) # расчет проекции вектора на оси
math строг к вводимым значениям. Например, math.sqrt(-1) выдаст ValueError: для него отрицательный корень — недопустим.---
###
cmath: когда отрицательные корни — это нормальноcmath — аналог math, но для комплексных чисел. Он спокойно принимает как float, так и complex, и возвращает именно комплексные числа.import cmath
z = -1
root = cmath.sqrt(z)
print(root) # (0+1j)
print(root.real) # действительная часть
print(root.imag) # мнимая часть
Ключевые функции и особенности:
- те же
sin, cos, exp, log, sqrt, но для комплексных;- полярная форма:
phase(z), polar(z), rect(r, phi).import cmath
z = 1 + 1j
r, phi = cmath.polar(z) # модуль и аргумент
print(r, phi)
z2 = cmath.rect(r, phi) # обратно в алгебраическую форму
print(z2)
---
### Как понять, что тебе нужен
cmath, а не math?- Нужен корень из «подозрительного» выражения (может стать отрицательным)?
- Работаешь с сигналами, Фурье, электрическими цепями, квантовой механикой?
- В формулах явно фигурирует
i (или j)?Тогда используй
complex и cmath:import cmath
a, b, c = 1, 2, 5 # дискриминант отрицательный
d = b**2 - 4*a*c
x1 = (-b + cmath.sqrt(d)) / (2*a)
x2 = (-b - cmath.sqrt(d)) / (2*a)
print(x1, x2)
---
Итого:
-
math — быстрый и строгий мир действительных чисел. -
cmath — тот же набор инструментов, но без страха перед мнимой единицей j. Понимание разницы между ними — первый шаг к задачам уровнем выше простых калькуляторных вычислений.
👍3❤1
Преобразование кодировок текста в Python с использованием
Когда начинаешь работать с текстом, кажется, что строки — это просто строки. А потом внезапно появляются кракозябры вместо букв, и становится ясно: кодировки — реальны, боль — тоже. Модуль
### Зачем нужен
Современный способ — использовать встроенный
- читать/писать файлы в нестандартных кодировках (cp1251, koi8-r и т.п.);
- конвертировать текст из одной кодировки в другую;
- тонко управлять обработкой ошибок.
### Базовый пример: чтение и запись с
Здесь Python сам декодирует байты из cp1251 в строку (
### Прямое преобразование байтов
Иногда текст уже в памяти в виде байтов, и нужно просто перекодировать:
Тот же эффект можно получить с
### Обработка ошибок:
Если в потоке байтов встречаются “битые” символы, полезно управлять тем, как Python на это реагирует:
Популярные варианты:
-
-
-
### Потоковое преобразование:
Если нужно конвертировать “на лету”, не загружая весь файл в память:
Здесь
---
codecsКогда начинаешь работать с текстом, кажется, что строки — это просто строки. А потом внезапно появляются кракозябры вместо букв, и становится ясно: кодировки — реальны, боль — тоже. Модуль
codecs в Python помогает эту боль контролировать.### Зачем нужен
codecs, если есть open?Современный способ — использовать встроенный
open(..., encoding="utf-8"). Но codecs полезен, когда нужно:- читать/писать файлы в нестандартных кодировках (cp1251, koi8-r и т.п.);
- конвертировать текст из одной кодировки в другую;
- тонко управлять обработкой ошибок.
### Базовый пример: чтение и запись с
codecs.openimport codecs
# Читаем файл в кодировке cp1251 и сохраняем в utf-8
with codecs.open("input_cp1251.txt", "r", encoding="cp1251") as f_in:
text = f_in.read()
with codecs.open("output_utf8.txt", "w", encoding="utf-8") as f_out:
f_out.write(text)
Здесь Python сам декодирует байты из cp1251 в строку (
str), а затем кодирует её в utf-8 при записи.### Прямое преобразование байтов
Иногда текст уже в памяти в виде байтов, и нужно просто перекодировать:
import codecs
data_cp1251 = b'\xcf\xf0\xe8\xe2\xe5\xf2' # байты в cp1251
# Декодируем байты -> str, затем кодируем в другую кодировку
text = data_cp1251.decode("cp1251")
data_utf8 = text.encode("utf-8")
Тот же эффект можно получить с
codecs.decode и codecs.encode:import codecs
data_cp1251 = b'\xcf\xf0\xe8\xe2\xe5\xf2'
text = codecs.decode(data_cp1251, "cp1251")
data_utf8 = codecs.encode(text, "utf-8")
### Обработка ошибок:
errors="ignore", replace, strictЕсли в потоке байтов встречаются “битые” символы, полезно управлять тем, как Python на это реагирует:
import codecs
with codecs.open("broken.txt", "r", encoding="utf-8", errors="replace") as f:
text = f.read()
Популярные варианты:
-
strict — по умолчанию, выбросит ошибку UnicodeDecodeError;-
ignore — пропустит проблемные символы;-
replace — заменит их на ? или спецсимвол �.### Потоковое преобразование:
codecs.StreamReaderWriterЕсли нужно конвертировать “на лету”, не загружая весь файл в память:
import codecs
with open("input_cp1251.txt", "rb") as f_in, \
open("output_utf8.txt", "wb") as f_out:
reader = codecs.getreader("cp1251")(f_in)
writer = codecs.getwriter("utf-8")(f_out)
for line in reader:
writer.write(line)
Здесь
reader читает байты и сразу выдаёт строки, а writer принимает строки и записывает их в нужной кодировке.---
codecs — это “швейцарский нож” для кодировок в Python. Даже если в повседневной работе вы используете только open(..., encoding="utf-8"), умение при необходимости достать из рукава codecs часто спасает проекты, столкнувшиеся с “наследственными” файлами и старыми системами.👍2🔥1
Создаем простой email-отправщик с
Рано или поздно почти каждому Python-разработчику нужно “научить” скрипт отправлять письма: отчеты, уведомления, результаты парсинга. Для этого в стандартной библиотеке уже есть всё необходимое — модуль
---
## Минимальный отправщик письма
Отправим простое письмо через SMTP-сервер (например, Gmail).
Ключевые моменты:
-
-
-
-
Для Gmail и некоторых других сервисов часто нужен не “обычный” пароль, а пароль приложения (app password).
---
## Несколько получателей и HTML-письмо
Сделаем отправщик чуть умнее: добавим список адресов и HTML-тело.
Теперь письмо красиво отображается в почтовом клиенте, а если HTML отключен — будет использована текстовая версия.
---
## Быстрая обертка в функцию
Чтобы не копировать один и тот же код в каждом проекте, удобно сделать маленькую утилиту:
Теперь любой скрипт может за пару строк отправить уведомление:
Так из сухого протокола SMTP получается удобный инструмент автоматизации: скрипт не просто “что-то делает”, а сам сообщает о результате.
smtplibРано или поздно почти каждому Python-разработчику нужно “научить” скрипт отправлять письма: отчеты, уведомления, результаты парсинга. Для этого в стандартной библиотеке уже есть всё необходимое — модуль
smtplib.---
## Минимальный отправщик письма
Отправим простое письмо через SMTP-сервер (например, Gmail).
import smtplib
from email.mime.text import MIMEText
smtp_server = "smtp.gmail.com"
smtp_port = 587
sender_email = "you@example.com"
sender_password = "app_password"
receiver_email = "friend@example.com"
subject = "Test email from Python"
body = "Hello!\nThis is a test email sent from a Python script."
msg = MIMEText(body, "plain", "utf-8")
msg["Subject"] = subject
msg["From"] = sender_email
msg["To"] = receiver_email
with smtplib.SMTP(smtp_server, smtp_port) as server:
server.starttls() # шифруем соединение
server.login(sender_email, sender_password)
server.send_message(msg)
Ключевые моменты:
-
smtplib.SMTP — подключение к серверу.-
starttls() — перевод соединения в защищенный режим (обязательно для большинства провайдеров).-
login() — авторизация.-
send_message() — отправка готового объекта письма.Для Gmail и некоторых других сервисов часто нужен не “обычный” пароль, а пароль приложения (app password).
---
## Несколько получателей и HTML-письмо
Сделаем отправщик чуть умнее: добавим список адресов и HTML-тело.
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import smtplib
smtp_server = "smtp.gmail.com"
smtp_port = 587
sender_email = "you@example.com"
sender_password = "app_password"
receivers = ["user1@example.com", "user2@example.com"]
msg = MIMEMultipart("alternative")
msg["Subject"] = "Daily report"
msg["From"] = sender_email
msg["To"] = ", ".join(receivers)
text_part = MIMEText("Plain text fallback", "plain", "utf-8")
html_part = MIMEText(
"""
<h1>Daily report</h1>
<p>Everything is <b>OK</b>.</p>
""",
"html",
"utf-8"
)
msg.attach(text_part)
msg.attach(html_part)
with smtplib.SMTP(smtp_server, smtp_port) as server:
server.starttls()
server.login(sender_email, sender_password)
server.sendmail(sender_email, receivers, msg.as_string())
Теперь письмо красиво отображается в почтовом клиенте, а если HTML отключен — будет использована текстовая версия.
---
## Быстрая обертка в функцию
Чтобы не копировать один и тот же код в каждом проекте, удобно сделать маленькую утилиту:
def send_email(subject, body, to_emails):
import smtplib
from email.mime.text import MIMEText
smtp_server = "smtp.gmail.com"
smtp_port = 587
sender_email = "you@example.com"
sender_password = "app_password"
msg = MIMEText(body, "plain", "utf-8")
msg["Subject"] = subject
msg["From"] = sender_email
msg["To"] = ", ".join(to_emails)
with smtplib.SMTP(smtp_server, smtp_port) as server:
server.starttls()
server.login(sender_email, sender_password)
server.sendmail(sender_email, to_emails, msg.as_string())
Теперь любой скрипт может за пару строк отправить уведомление:
send_email("Job finished", "Your script has completed successfully.", ["admin@example.com"])
Так из сухого протокола SMTP получается удобный инструмент автоматизации: скрипт не просто “что-то делает”, а сам сообщает о результате.
🔥3👍1
Использование
В Python есть маленькая, но очень мощная функция —
### Базовый пример: два списка одновременно
Допустим, у нас есть список имён и список оценок:
Цикл одновременно берет по одному элементу из каждого списка: сначала
Важно:
---
### Создание словаря из двух списков
Классическая задача для начинающих — склеить два списка в словарь: ключи и значения.
Получается читабельно и без лишних циклов и индексов.
---
### Обработка более чем двух списков
Так удобно считать суммы, собирать отчеты, формировать строки для вывода.
---
### Распаковка с помощью
Трюк наоборот: «поворот» структуры. Допустим, у нас уже есть список пар:
Оператор
---
###
Если списки разной длины и важно не терять элементы, берите:
Здесь недостающие элементы будут заменены
---
zip для параллельной обработки списковВ Python есть маленькая, но очень мощная функция —
zip. Она решает типичную задачу: «у меня есть несколько списков, нужно пройтись по ним параллельно». Без zip код часто превращается в кашу из индексов.### Базовый пример: два списка одновременно
Допустим, у нас есть список имён и список оценок:
names = ["Alice", "Bob", "Charlie"]
scores = [95, 82, 77]
for name, score in zip(names, scores):
print(name, "->", score)
Цикл одновременно берет по одному элементу из каждого списка: сначала
("Alice", 95), потом ("Bob", 82) и так далее. Важно:
zip останавливается, когда самый короткий список заканчивается.---
### Создание словаря из двух списков
Классическая задача для начинающих — склеить два списка в словарь: ключи и значения.
keys = ["host", "port", "debug"]
values = ["localhost", 8080, True]
config = dict(zip(keys, values))
print(config)
# {'host': 'localhost', 'port': 8080, 'debug': True}
Получается читабельно и без лишних циклов и индексов.
---
### Обработка более чем двух списков
zip не ограничивается двумя последовательностями:products = ["Book", "Pen", "Bag"]
prices = [10.5, 1.2, 25.0]
quantities = [2, 10, 1]
for product, price, qty in zip(products, prices, quantities):
total = price * qty
print(product, "total:", total)
Так удобно считать суммы, собирать отчеты, формировать строки для вывода.
---
### Распаковка с помощью
zip(*)Трюк наоборот: «поворот» структуры. Допустим, у нас уже есть список пар:
pairs = [("x1", 10), ("x2", 20), ("x3", 30)]
labels, values = zip(*pairs)
print(labels) # ('x1', 'x2', 'x3')
print(values) # (10, 20, 30)
Оператор
* распаковывает список, а zip пересобирает его по столбцам. Это удобно при работе с данными, которые приходят в виде списков кортежей.---
###
zip_longest из itertoolsЕсли списки разной длины и важно не терять элементы, берите:
from itertools import zip_longest
a = [1, 2, 3]
b = ["a", "b"]
for x, y in zip_longest(a, b, fillvalue=None):
print(x, y)
Здесь недостающие элементы будут заменены
fillvalue.---
zip — это способ сделать код более декларативным: вы говорите «обойди эти списки вместе», а не «пройди по индексам и вытащи элементы руками». Чем меньше индексов и вспомогательных счетчиков, тем проще читать и отлаживать программы.👍2🔥1
### Создание внутренней документации с использованием docstrings
Одна из самых недооценённых суперспособностей Python — встроенная документация через docstrings. Это те самые строки в тройных кавычках сразу под объявлением функции, класса или модуля. Они не просто для красоты: по ним работают
---
## Зачем нужны docstrings
Без них ваш код быстро превращается в загадку:
- вы забываете, что делает функция;
- непонятно, какие аргументы ожидать;
- сложно понять, что именно возвращается.
Docstring — это краткий контракт функции: что она делает, какие принимает параметры и что возвращает.
---
## Базовый пример docstring
Теперь достаточно вызвать:
и вы получите аккуратную справку по функции прямо в консоли.
---
## Docstrings в модулях и классах
Документировать стоит не только функции:
Такие docstrings позволяют быстро понять архитектуру модуля: о чём он, какие есть классы, чем они занимаются.
---
## Какой стиль использовать
Наиболее популярны два стиля:
1. Google-style (как в примерах выше:
2. NumPy-style (разделы
Главное — выбрать один стиль и придерживаться его в проекте. Это делает документацию предсказуемой и удобной.
---
## Мини‑лайфхаки
- Первая строка docstring — краткое описание в одном предложении.
- Дальше — подробности, пример использования, нюансы.
- Не описывайте «что написано в коде», описывайте зачем это нужно и какие ожидания у функции.
Хорошо написанные docstrings превращают ваш код в само-документируемую систему. А это уже уровень разработчика, с которым приятно работать не только компьютеру, но и людям.
Одна из самых недооценённых суперспособностей Python — встроенная документация через docstrings. Это те самые строки в тройных кавычках сразу под объявлением функции, класса или модуля. Они не просто для красоты: по ним работают
help(), IDE-подсказки и автогенерация документации.---
## Зачем нужны docstrings
Без них ваш код быстро превращается в загадку:
- вы забываете, что делает функция;
- непонятно, какие аргументы ожидать;
- сложно понять, что именно возвращается.
Docstring — это краткий контракт функции: что она делает, какие принимает параметры и что возвращает.
---
## Базовый пример docstring
def calculate_discount(price, discount):
"""
Calculate final price after applying a percentage discount.
Args:
price (float): Original price.
discount (float): Discount in percent, from 0 to 100.
Returns:
float: Price after discount.
Raises:
ValueError: If discount is not between 0 and 100.
"""
if not 0 <= discount <= 100:
raise ValueError("Discount must be between 0 and 100.")
return price * (1 - discount / 100)
Теперь достаточно вызвать:
help(calculate_discount)
и вы получите аккуратную справку по функции прямо в консоли.
---
## Docstrings в модулях и классах
Документировать стоит не только функции:
"""
tools.pricing module.
Contains utilities for working with prices and discounts.
"""
class PriceCalculator:
"""
Helper class for price calculation logic.
"""
def __init__(self, tax_rate):
"""
Initialize calculator.
Args:
tax_rate (float): Tax in percent.
"""
self.tax_rate = tax_rate
def with_tax(self, price):
"""
Add tax to the price.
Args:
price (float): Base price.
Returns:
float: Price including tax.
"""
return price * (1 + self.tax_rate / 100)
Такие docstrings позволяют быстро понять архитектуру модуля: о чём он, какие есть классы, чем они занимаются.
---
## Какой стиль использовать
Наиболее популярны два стиля:
1. Google-style (как в примерах выше:
Args:, Returns:, Raises:)2. NumPy-style (разделы
Parameters, Returns с табличным форматированием)Главное — выбрать один стиль и придерживаться его в проекте. Это делает документацию предсказуемой и удобной.
---
## Мини‑лайфхаки
- Первая строка docstring — краткое описание в одном предложении.
- Дальше — подробности, пример использования, нюансы.
- Не описывайте «что написано в коде», описывайте зачем это нужно и какие ожидания у функции.
Хорошо написанные docstrings превращают ваш код в само-документируемую систему. А это уже уровень разработчика, с которым приятно работать не только компьютеру, но и людям.
🔥2❤1👍1
Как использовать
Одна из самых частых задач в Python — избавиться от дубликатов в списке. Можно писать циклы, проверять, есть ли элемент в новом списке… а можно использовать встроенный тип
### Что такое
Ключевые свойства:
- в
- элементы должны быть хешируемыми (числа, строки, кортежи и т.д.);
- порядок не гарантируется.
Создать множество из списка можно так:
Мы просто «заставили» Python выкинуть всё лишнее.
### Возврат к списку без дубликатов
Часто нужен именно список (с индексами и возможностью изменять элементы). Тогда:
Важно: порядок элементов может измениться. Для чисел это обычно не критично, но для, скажем, событий во времени — уже проблема.
### Как сохранить порядок и убрать дубликаты?
Комбинируем
Здесь:
-
-
### Короткая версия через
Если важен порядок, но не хочется писать цикл:
До Python 3.7 порядок ключей в
### Где это полезно?
- очистка списков e-mail адресов от повторов;
- фильтрация тегов;
- удаление дубликатов из данных перед анализом.
Главное:
set для удаления дубликатов из спискаОдна из самых частых задач в Python — избавиться от дубликатов в списке. Можно писать циклы, проверять, есть ли элемент в новом списке… а можно использовать встроенный тип
set и сделать всё в одну строку.### Что такое
set?set — это множество: неупорядоченная коллекция уникальных элементов. Ключевые свойства:
- в
set не бывает дубликатов;- элементы должны быть хешируемыми (числа, строки, кортежи и т.д.);
- порядок не гарантируется.
Создать множество из списка можно так:
numbers = [1, 2, 2, 3, 3, 3]
unique_numbers = set(numbers)
print(unique_numbers) # {1, 2, 3}
Мы просто «заставили» Python выкинуть всё лишнее.
### Возврат к списку без дубликатов
Часто нужен именно список (с индексами и возможностью изменять элементы). Тогда:
numbers = [1, 2, 2, 3, 3, 3]
unique_list = list(set(numbers))
print(unique_list) # порядок не гарантируется
Важно: порядок элементов может измениться. Для чисел это обычно не критично, но для, скажем, событий во времени — уже проблема.
### Как сохранить порядок и убрать дубликаты?
Комбинируем
set и цикл:items = ["a", "b", "a", "c", "b", "d"]
seen = set()
result = []
for item in items:
if item not in seen:
seen.add(item)
result.append(item)
print(result) # ['a', 'b', 'c', 'd']
Здесь:
-
seen хранит уже встреченные элементы;-
result — итоговый список без повторов, в исходном порядке.### Короткая версия через
dict.fromkeysЕсли важен порядок, но не хочется писать цикл:
items = ["a", "b", "a", "c", "b", "d"]
result = list(dict.fromkeys(items))
print(result) # ['a', 'b', 'c', 'd']
До Python 3.7 порядок ключей в
dict официально не гарантировался, но в современных версиях этот трюк работает надёжно.### Где это полезно?
- очистка списков e-mail адресов от повторов;
- фильтрация тегов;
- удаление дубликатов из данных перед анализом.
Главное:
set — быстрый и простой способ получить уникальные значения. Используйте set, когда порядок не важен, и сочетайте его с дополнительной логикой, когда порядок нужно сохранить.👍3🔥1
Создание простых Webhook-обработчиков с Flask
Иногда нужно, чтобы ваш скрипт реагировал на внешние события: оплата на сайте, новый комментарий в соцсети, пуш из GitHub и т.п. Сервисы решают это через webhooks — они отправляют HTTP-запросы (обычно
Самый простой способ в Python — мини-фреймворк Flask.
---
### Минимальный Webhook на Flask
Установим Flask:
Создадим
Что тут важно:
-
-
- Возвращаем JSON-ответ и HTTP-код 200, чтобы внешний сервис понял, что все прошло успешно.
---
### Как протестировать без реального сервиса
Можно «прикинуться» внешним сервисом с помощью
В консоли сервера вы увидите распечатанный JSON.
---
### Добавляем простую проверку подписи
Реальные сервисы почти всегда подписывают запросы, чтобы никто не мог отправить фальшивый webhook. Простейший вариант — общий секрет и заголовок с HMAC:
Ключевые моменты:
- Используем
-
- При неверной подписи сразу возвращаем 401.
---
### Зачем это все начинающему?
Webhook-обработчик — это реальная задачка уровня «боевого» кода:
- вы работаете с HTTP, JSON, заголовками и кодами ответа;
- думаете о безопасности (подписи, проверка метода);
- учитесь разделять «прием запроса» и «бизнес-логику».
А Flask позволяет уложить все это в несколько десятков строк понятного кода — отличный старт для вхождения в мир веб-разработки на Python.
Иногда нужно, чтобы ваш скрипт реагировал на внешние события: оплата на сайте, новый комментарий в соцсети, пуш из GitHub и т.п. Сервисы решают это через webhooks — они отправляют HTTP-запросы (обычно
POST) на ваш URL. Ваша задача — принять запрос и что-то сделать.Самый простой способ в Python — мини-фреймворк Flask.
---
### Минимальный Webhook на Flask
Установим Flask:
pip install flask
Создадим
app.py:from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route("/webhook", methods=["POST"])
def webhook_handler():
data = request.get_json(silent=True) or {}
event_type = data.get("event", "unknown")
# Примитивная обработка события
if event_type == "payment_succeeded":
# здесь могла бы быть запись в базу или отправка email
print("Payment succeeded:", data)
else:
print("Unknown event:", data)
return jsonify({"status": "ok"}), 200
if __name__ == "__main__":
app.run(port=5000, debug=True)
Что тут важно:
-
methods=["POST"] — принимаем только POST (классический формат webhook’ов).-
request.get_json() — достаем JSON-тело запроса.- Возвращаем JSON-ответ и HTTP-код 200, чтобы внешний сервис понял, что все прошло успешно.
---
### Как протестировать без реального сервиса
Можно «прикинуться» внешним сервисом с помощью
curl:curl -X POST http://127.0.0.1:5000/webhook \
-H "Content-Type: application/json" \
-d '{"event":"payment_succeeded","amount":1500}'
В консоли сервера вы увидите распечатанный JSON.
---
### Добавляем простую проверку подписи
Реальные сервисы почти всегда подписывают запросы, чтобы никто не мог отправить фальшивый webhook. Простейший вариант — общий секрет и заголовок с HMAC:
import hmac
import hashlib
from flask import abort
SECRET_TOKEN = "super-secret-key"
def verify_signature(raw_body: bytes, received_signature: str) -> bool:
mac = hmac.new(SECRET_TOKEN.encode(), msg=raw_body, digestmod=hashlib.sha256)
expected_sig = mac.hexdigest()
return hmac.compare_digest(expected_sig, received_signature or "")
@app.route("/secure-webhook", methods=["POST"])
def secure_webhook():
raw_body = request.data
signature = request.headers.get("X-Signature")
if not verify_signature(raw_body, signature):
abort(401, description="Invalid signature")
payload = request.get_json(silent=True) or {}
print("Secure event:", payload)
return jsonify({"status": "accepted"}), 200
Ключевые моменты:
- Используем
request.data, а не уже разобранный JSON — подпись считается по «сырому» телу.-
hmac.compare_digest защищает от атак по времени сравнения.- При неверной подписи сразу возвращаем 401.
---
### Зачем это все начинающему?
Webhook-обработчик — это реальная задачка уровня «боевого» кода:
- вы работаете с HTTP, JSON, заголовками и кодами ответа;
- думаете о безопасности (подписи, проверка метода);
- учитесь разделять «прием запроса» и «бизнес-логику».
А Flask позволяет уложить все это в несколько десятков строк понятного кода — отличный старт для вхождения в мир веб-разработки на Python.
👍4