Python для начинающих
1.24K subscribers
546 photos
3 videos
232 files
74 links
Python для начинающих
Download Telegram
Введение в модуль bisect для работы с отсортированными списками
Введение в модуль bisect: быстрые операции с отсортированными списками

Когда вы работаете с отсортированным списком, вставка новых элементов может испортить порядок — приходится либо искать позицию вручную, либо каждый раз вызывать sort(). Модуль bisect решает эту проблему: он помогает искать позицию для элемента в отсортированном списке и вставлять его так, чтобы порядок сохранился.

---

## Основные функции bisect

Импортируем модуль:

import bisect


Четыре ключевые функции:

- bisect_left(a, x) — найти позицию для вставки x слева (перед равными элементами)
- bisect_right(a, x) или просто bisect.bisect(a, x) — позиция для вставки справа (после равных)
- insort_left(a, x) — вставить x слева и сохранить сортировку
- insort_right(a, x) или insort(a, x) — вставить x справа

---

## Поиск позиции без ручных циклов

import bisect

scores = [10, 20, 20, 30, 40]

pos_left = bisect.bisect_left(scores, 20)
pos_right = bisect.bisect_right(scores, 20)

print(pos_left) # 1 -> перед первой 20
print(pos_right) # 3 -> после последней 20


Так вы мгновенно узнаете, куда встанет элемент, не перебирая список вручную.

---

## Вставка в отсортированный список

Вставляем и сразу поддерживаем порядок:

import bisect

scores = [10, 20, 30, 40]

bisect.insort(scores, 25)
bisect.insort(scores, 30)

print(scores) # [10, 20, 25, 30, 30, 40]


insort делает insert + бинарный поиск под капотом. Для вас это одна строка — и список по-прежнему отсортирован.

---

## Реальный пример: поиск "позиции в рейтинге"

Допустим, у вас есть отсортированный список результатов, и вы хотите узнать, каким по счету будет новый результат:

import bisect

high_scores = [100, 90, 80, 70, 60]
high_scores.sort(reverse=True) # гарантируем порядок по убыванию

# Для работы с bisect развернем по возрастанию
asc_scores = sorted(high_scores)

new_score = 85
pos = len(asc_scores) - bisect.bisect_left(asc_scores, new_score)

print(f"New score rank: {pos}") # позиция в рейтинге по убыванию


---

Модуль bisect — это маленький, но очень полезный инструмент, когда вам нужно много раз искать и вставлять элементы в отсортированный список без лишних затрат и переписывания алгоритмов бинарного поиска вручную.
🔥2
Создание простого таймера обратного отсчета
Создание простого таймера обратного отсчёта на Python

Таймер обратного отсчёта — отличный мини‑проект для новичка: он знакомит с циклами, модулями стандартной библиотеки и работой со временем. А ещё его реально можно использовать: для помодоро-сессий, тренировок или напоминаний.

---

### Вариант 1: Самый простой консольный таймер

Используем модуль time и цикл while:

import time

def countdown(seconds):
while seconds > 0:
mins = seconds // 60
secs = seconds % 60
time_format = f"{mins:02d}:{secs:02d}"
print(time_format, end="\r")
time.sleep(1)
seconds -= 1
print("00:00")
print("Time is up!")

countdown(10)


Что здесь важно:

- time.sleep(1) — «усыпляет» программу на 1 секунду.
- end="\r" — перезаписывает строку в консоли, вместо того чтобы печатать новую.
- Формат f"{mins:02d}" добавляет ведущий ноль (например, 03:07).

Попробуйте поменять countdown(10) на любое количество секунд.

---

### Вариант 2: Таймер с вводом от пользователя

Добавим интерактивность:

import time

def countdown(seconds):
while seconds > 0:
mins = seconds // 60
secs = seconds % 60
print(f"{mins:02d}:{secs:02d}", end="\r")
time.sleep(1)
seconds -= 1
print("00:00")
print("Time is up!")

user_input = int(input("Enter time in seconds: "))
countdown(user_input)


Здесь мы получаем число секунд от пользователя и передаём его в функцию. Если хотите, можете добавить проверку ввода, чтобы обрабатывать нечисловые значения и отрицательные числа.

---

### Вариант 3: Таймер до конкретного момента времени

Теперь немного «прокачаемся» и используем модуль datetime: сделаем обратный отсчёт до заданного времени в будущем.

import time
from datetime import datetime, timedelta

def countdown_to(target_time):
while True:
now = datetime.now()
delta = target_time - now
if delta.total_seconds() <= 0:
print("00:00:00")
print("Time is up!")
break
total_seconds = int(delta.total_seconds())
hours = total_seconds // 3600
mins = (total_seconds % 3600) // 60
secs = total_seconds % 60
print(f"{hours:02d}:{mins:02d}:{secs:02d}", end="\r")
time.sleep(1)

target = datetime.now() + timedelta(minutes=1)
countdown_to(target)


Здесь:

- datetime.now() — текущее время.
- timedelta(minutes=1) — интервал в 1 минуту.
- delta.total_seconds() — сколько секунд осталось.

---

На таком простом таймере вы затрагиваете сразу несколько фундаментальных вещей: циклы, функции, работу с модулями time и datetime, форматирование строк и простую логику. Это именно тот тип мини‑проекта, который помогает быстро почувствовать живой результат от кода.
👍6
Организация кода в виде модулей и импорт с относительными путями
Python для начинающих: как приручить модули и относительные импорты

Когда скрипт вырастает до 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 с typing
Generics в Python: зачем они нужны и как ими пользоваться

Если вы уже видели 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
Работа с фиксированными структурами данных с помощью struct
### Работа с фиксированными структурами данных с помощью struct

Когда Pythonу приходится общаться с «железом» или чужим бинарным форматом (сетевой протокол, файл игры, формат датасета), удобные списки и словари уже не спасают. Там правит мир байтов и фиксированных структур. Для таких задач в стандартной библиотеке есть модуль struct.

---

## Зачем нужен struct

struct умеет:
- упаковывать 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. Чтение бинарного файла по записям

Представим файл, состоящий из записей:

- timestampdouble (8 байт)
- valuefloat (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
Создаем мини-базу данных на JSON: простой путь без SQL

Иногда для небольших проектов полноценная база данных — это как использовать экскаватор, чтобы посадить цветок. Для заметок, простого 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
Изучение math и cmath: работа с действительными и комплексными числами
Изучение 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.

Понимание разницы между ними — первый шаг к задачам уровнем выше простых калькуляторных вычислений.
👍31
Преобразование кодировок текста в Python с использованием codecs
Преобразование кодировок текста в Python с использованием codecs

Когда начинаешь работать с текстом, кажется, что строки — это просто строки. А потом внезапно появляются кракозябры вместо букв, и становится ясно: кодировки — реальны, боль — тоже. Модуль codecs в Python помогает эту боль контролировать.

### Зачем нужен codecs, если есть open?

Современный способ — использовать встроенный open(..., encoding="utf-8"). Но codecs полезен, когда нужно:

- читать/писать файлы в нестандартных кодировках (cp1251, koi8-r и т.п.);
- конвертировать текст из одной кодировки в другую;
- тонко управлять обработкой ошибок.

### Базовый пример: чтение и запись с codecs.open

import 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-отправщика с smtplib
Создаем простой email-отправщик с 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
Использование метода zip для параллельной обработки списков
Использование 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