diff --git a/course/module-0/intro-community/article.md b/course/module-0/intro-community/article.md
index dd2e3cd..844ed3f 100644
--- a/course/module-0/intro-community/article.md
+++ b/course/module-0/intro-community/article.md
@@ -1,24 +1,19 @@
-# Сообщество
-
-Мы понимаем, что коммуникация – очень важная часть образовательного процесса, поэтому предлагаем вступить в наше сообщество, где
-вы сможете получить помощь и поддержку от других студентов или же сами подсказать в случае возникновения вопросов от других членов сообщества.
+---
+meta:
+ title: "Сообщество Python Academy"
+ description: "Чат и канал Python Academy в Telegram: где спросить, если застряли на задаче, и куда написать при проблемах с сервисом или оплатой премиума."
+---
-Коммуникация организована через:
-
-- **[канал в Telegram](https://telegram.me/pythonacademyofficial)**
+# Сообщество
-На странице сообщества мы выкладываем информацию по последним наиболее важным обновлениям, а также много
-образовательного контента по Python, аналитике данных и бекенд разработке, и как с помощью них найти работу или
-получить повышение.
+Застрять в одиночку легко: код не работает, текст ошибки ничего не проясняет, а спросить некого. Ровно для таких моментов есть **чат в Telegram** — там и те, кто проходит курс прямо сейчас, и те, кто прошёл его раньше.
-Для общения между участниками есть чат **[в Telegram](https://telegram.me/pythonacademychat)**.
+Спрашивать не стыдно. Отвечать, кстати, не менее полезно: пока объясняешь чужую ошибку, окончательно разбираешься в теме сам.
-## Решение проблем
+Если разговоры не нужны, а следить за курсом хочется — есть **канал в Telegram**. Там выходят важные обновления и материалы по Python, аналитике данных и бэкенд-разработке — в том числе о том, как с их помощью найти работу или получить повышение.
-По вопросам, возникающим при работе сервиса, замечаниям и рекомендациям по содержанию курса или же при проблемах с оплатой премиум аккаунта можно использовать
-следующие способы связи:
+## Если что-то сломалось
-- Написать письмо на адрес
- **[feedback@python-academy.org](mailto:feedback@python-academy.org)**
+Чат хорош для вопросов по коду. А вот если вы наткнулись на ошибку в самом сервисе, хотите поправить нас по содержанию курса или у вас проблема с оплатой премиума — напишите на **[feedback@python-academy.org](mailto:feedback@python-academy.org)**.
-Рады видеть Вас в нашем сообществе 😍
+Рады видеть вас в нашем сообществе 😍
diff --git a/course/module-0/intro-intro/article.md b/course/module-0/intro-intro/article.md
index 833611d..c848b25 100644
--- a/course/module-0/intro-intro/article.md
+++ b/course/module-0/intro-intro/article.md
@@ -1,15 +1,18 @@
+---
+meta:
+ title: "Введение в курс по Python"
+ description: "Бесплатный онлайн-курс по Python: как устроены курс, тренажёр и песочница, на каких устройствах заниматься и зачем нужен премиум-аккаунт."
+---
+
# О курсе
-Данный курс, направленный на изучение Python, спроектирован иначе, чем большинство других курсов.
-Мы не будем пытаться выучить все возможные библиотеки Python, вместо этого мы акцентируем наше внимание на принципы программирования и
-на фундаментальные основы Python, которые позволят вам создавать любые приложения.
+Можно посмотреть десяток часов видео про Python, кивать в такт и не написать при этом ни строчки самому. Ощущение «на лекции всё понятно, а сесть и сделать не получается» берётся ровно отсюда.
+
+Этот курс собран от обратного. Мы не пытаемся перебрать все библиотеки Python — вместо этого разбираемся, как устроен сам язык, и сразу пишем код, не отходя от текста статьи. Нужную библиотеку вы потом освоите за вечер, если понимаете фундамент. В обратную сторону это не работает.
-Для достижения данной цели курс снабжён большим количеством упражнений, которые должны помочь построить те самые необходимые связи в нашем мозге, чтоб в дальнейшем вам не приходилось
-сильно задумываться при написании Python кода для ваших приложений.
+Поэтому дальше будет много упражнений. Они не для галочки: нужные связи в голове строятся, только когда вы сами воюете с ошибкой, а не читаете чужое готовое решение. И если что-то буксует — это нормальная часть процесса, а не признак того, что «программирование не ваше».
-При прохождении курса мы настоятельно рекомендуем вам культивировать «мышление роста»: если вы будете сталкиваться с какими-либо сложностями, пытаясь
-понять новую тему или выполняя упражнения, пытайтесь трансформировать возникающий разочаровывающий опыт в продуктивный.
-И самая важная вещь в данном курсе – **получайте удовольствие!**
+И самое важное – **получайте удовольствие!**
## Проходите курс публично
@@ -19,27 +22,38 @@
## Поддержка устройств
-Данный курс спроектирован для прохождения с компьютера и ноутбука – наиболее оптимальных устройств для написании Python кода. Курс также можно проходить с планшета или телефона,
-но некоторый функционал может быть недоступен или работать некорректно. Просим отнестись к этому с пониманием.
+Курс рассчитан на компьютер или ноутбук — с клавиатурой писать код всё-таки удобнее всего. С планшета и телефона заниматься тоже можно,
+но часть возможностей будет недоступна или заработает не так, как задумано. Просим отнестись к этому с пониманием.
## Как организован наш ресурс
-
+
+
На нашем ресурсе есть 3 основных раздела, каждый из которых будет по-разному полезен:
-- **[Данный курс](/guide)** – это ваш пошаговый помощник при изучении Python. Здесь представлены последовательные, структурированные материалы, а также связанные упражнения для закрепления знаний.
-- **[Тренажёр](/trainer)** – это инструмент проверки ваших знаний. Задания в нем проранжированы по сложности, поэтому вы можете подбирать подходящие для вашего текущего уровня.
- Также прорешивание заданий из тренажёра необходимо для получения сертификата, подтверждающего знания в области Python.
-- **[Песочница](/sandbox)** – это пространство для свободного экспериментирования с Python кодом и проверки полученных знаний.
+- ** Данный курс** – это ваш пошаговый помощник при изучении Python. Здесь представлены последовательные, структурированные материалы, а также связанные упражнения для закрепления знаний.
+- **Тренажёр** – это инструмент проверки ваших знаний. Задания в нем проранжированы по сложности, поэтому вы можете подбирать подходящие для вашего текущего уровня.
+ Также прорешивание заданий из тренажёра необходимо для получения сертификата, подтверждающего знания в области Python.
+- **Песочница** – это пространство для свободного экспериментирования с Python кодом и проверки полученных знаний.
+
+## Премиум-аккаунт
+
+Базовый курс, тренажёр и песочница доступны бесплатно. Премиум-аккаунт — это необязательное дополнение для тех, кто хочет учиться глубже и пользоваться всеми возможностями платформы.
+
+С премиум-аккаунтом вы получаете:
+
+- доступ к готовым решениям заданий из тренажёра;
+- продвинутые курсы «Backend на FastAPI с нуля» и «Продуктовая аналитика на Python»;
+- расширенный доступ к AI-ассистенту и задания с собеседований в топовые компании;
+- возможность выпускать именные сертификаты.
-## Премиум аккаунт
+Премиум действует 6 месяцев с момента оплаты. Весь прогресс, накопленный до покупки, сохранится и будет учитываться при получении сертификатов. После окончания срока действия премиума аккаунт снова станет бесплатным, а прогресс и уже выпущенные сертификаты останутся у вас.
-Покупка [премиум аккаунта](/premium) – это необязательная часть, курс мы предоставляем для всех абсолютно бесплатно. Но с ним для вас открывается больше возможностей для
-более продуктивного изучения, открывается доступ к заданиям из реальных собеседований в топовые компании и даётся возможность получить сертификат. К тому же это отличная возможность поддержать нас в развитии данного сервиса.
+Покупая премиум-аккаунт, вы также поддерживаете развитие Python Academy и помогаете нам создавать новые учебные материалы и возможности.
## Прогресс и получение сертификата
-Если вы авторизованы, то в разделе **[Тренажёр](/trainer)** можете увидеть прогресс-бар.
+Если вы авторизованы, то в разделе **Тренажёр** можете увидеть прогресс-бар.

diff --git a/course/module-0/intro-structure-of-course/article.md b/course/module-0/intro-structure-of-course/article.md
index 1759525..6caeb27 100644
--- a/course/module-0/intro-structure-of-course/article.md
+++ b/course/module-0/intro-structure-of-course/article.md
@@ -1,34 +1,47 @@
-# Структура курса
-
-Курс разбит на 3 крупных модуля, не считая модуля «Введение». Каждый модуль можно рассматривать как мини-курс,
-нацеленный на изучение определённой группы тем в Python.
+---
+meta:
+ title: "Структура курса"
+ description: "Из чего состоит курс Python: три модуля — от переменных до асинхронности. Как устроены блоки с исполняемым кодом и задания для самопроверки."
+---
-Хоть модули можно проходить независимо друг от друга, мы все же рекомендуем проходить курс по порядку.
+# Структура курса
-В конце большинства практических тем находится блок «Задания для самопроверки» для того, чтобы можно было сразу попрактиковаться в использовании
-новых знаний 🎓.
+Курс — это три модуля, не считая «Введения», которое вы читаете прямо сейчас. Каждый модуль устроен как мини-курс: берёт свою группу тем и доводит её до конца.
-## Интерфейсы
+- **Основы Python I** — переменные, типы данных, функции, условия, циклы и коллекции. То, из чего состоит любая программа на Python.
+- **Основы Python II** — библиотеки и модули, работа с файлами, классы и объекты, исключения, декораторы, даты.
+- **Продвинутый Python** — базы данных, тестирование, многопоточность и асинхронность.
-В ходе прохождения курса вы встретите нестандартные компоненты, поэтому далее опишем, как с ними взаимодействовать.
+Порядок здесь не случайный: каждый следующий модуль стоит на предыдущем. Во втором вы будете писать функции и перебирать списки из первого, а третий без них не соберётся вообще. Если основы вам уже знакомы — пролистывайте знакомое, но не начинайте с середины.
-### Блок исполняемого кода
+## Блок с кодом
-Пожалуй, наиболее популярным компонентом будет блок исполняемого кода, он выглядит следующим образом:
+Почти в каждой статье вам встретится блок с кодом. Выглядит он так:
-```python-executable
+```python
print("Hi, I love Python 😊")
+
```
-Если в нём есть активная синяя кнопка, то код можно запустить. При нажатии на кнопку код откроется в новой вкладке — в песочнице.
+Строка со значком `>_` — это уже не код, а результат: то, что Python напечатал, выполнив строку выше. Так что читать блок нужно сверху вниз: сначала что мы попросили, потом что получили.
+
+Справа в шапке блока живут четыре значка:
+
+
+
+Значок ▶ есть не у всех блоков — только у тех, чей код можно выполнить. Он открывает код в **песочнице**: это отдельная вкладка, где его можно менять и запускать сколько угодно раз.
+
+Одна тонкость про ▶: он отправляет в песочницу только свой блок, без соседних. Поэтому каждый пример самодостаточен — всё, что нужно коду, объявлено прямо в нём, и запускать блоки можно в любом порядке.
-
+## Задания для самопроверки
-### Задания для самопроверки
+Не в каждой статье, но во многих — особенно в «Основах Python I» — в конце ждёт блок с задачами:
-Настоятельно рекомендуется не пропускать задачи в конце каждой статьи. Они выглядят следующим образом:
+
-
+Не пропускайте их. Прочитать про цикл и написать цикл — разные вещи, и вторая обнаруживается только за клавиатурой 🎓.
-Перед решением задач рекомендуется войти в свой аккаунт, так все ваши решения будут сохраняться.
+Перед решением войдите в аккаунт, иначе решения не сохранятся.
diff --git a/course/module-0/python-installation/article.md b/course/module-0/python-installation/article.md
index e08368c..e9dca40 100644
--- a/course/module-0/python-installation/article.md
+++ b/course/module-0/python-installation/article.md
@@ -1,120 +1,107 @@
-# Установка Python
-
-При прохождении нашего курса установка Python не обязательна: все предлагаемые задачи запускаются на наших серверах, а произвольный Python‑код всегда можно выполнить в **[Песочнице](/sandbox)**.
+---
+meta:
+ title: "Установка Python"
+ description: "Как установить Python 3 на Windows, macOS или Linux: пошаговый гайд, галочка Add to PATH и проверка, что всё заработало."
+---
-Но если вы хотите запускать код у себя локально, ниже — короткий гайд по установке.
-
-В этом коротком гайде вы установите актуальную версию Python 3 на свою систему и проверите, что всё работает. Поехали! 🚀
+# Установка Python
-## Шаг 1. Проверьте, не установлен ли Python
+Для курса Python устанавливать не обязательно: задачи запускаются на наших серверах, а любой код можно выполнить в **Песочнице**. Так что если вам не терпится начать — смело листайте дальше, эта глава никуда не денется.
-Откройте терминал/консоль и выполните команды проверки версии.
+Но рано или поздно захочется писать код у себя: свои файлы, свои проекты, без вкладки в браузере. Тогда возвращайтесь сюда — ниже короткий путь от «ничего нет» до «всё работает» 🚀.
-- **Windows (PowerShell или CMD):**
+## Шаг 1. Проверьте, вдруг Python уже есть
- ```bash
- python --version
- py --version
- pip --version
- ```
+Python мог приехать вместе с системой или установиться раньше — на macOS и Linux он есть почти всегда. Проверять будем в терминале: это окно, где команды набирают текстом, а не мышкой.
-- **macOS / Linux (Terminal):**
+- **Windows** — нажмите `Win`, наберите «PowerShell» и откройте его.
+- **macOS** — нажмите `Cmd + пробел`, наберите «Терминал» и откройте его.
+- **Linux** — обычно `Ctrl + Alt + T`.
- ```bash
- python3 --version
- pip3 --version
- ```
+Наберите команду и нажмите `Enter`:
-Если видите номер версии (например, `Python 3.12.x`), Python уже установлен. Если команды не найдены — устанавливаем Python.
+**macOS / Linux**
-## Установка на Windows
+```sh
+python3 --version
+```
-1. Скачайте официальный установщик: **[python.org → Downloads → Windows](https://www.python.org/downloads/windows/)**.
-2. Запустите установщик.
-3. На первом экране поставьте галочку **«Add python.exe to PATH»** (очень важно!) и нажмите **Install Now**.
-4. Дождитесь завершения и закройте установщик.
-5. Проверьте установку в PowerShell/CMD:
+**Windows**
-```bash
-py --version
+```powershell
python --version
-pip --version
```
-Если команда `python` не находится, попробуйте `py` или выйдите/войдите в систему. При необходимости перезагрузите компьютер.
+Если в ответ пришёл номер версии — например, `Python 3.12.4` — всё уже на месте, и шаг 2 можно пропустить. Годится любая версия, которая начинается с цифры 3.
-## Установка на macOS
+Ответ вида «команда не найдена» тоже нормальный: значит, Python просто ещё не установлен. Идём дальше.
-Есть два удобных способа — выберите любой.
+## Шаг 2. Установите Python
-### Способ A: Официальный установщик
+### Windows
-1. Скачайте **[python.org → Downloads → macOS](https://www.python.org/downloads/macos/)**.
-2. Откройте `.pkg` и следуйте инструкциям установщика.
-3. Проверьте версии:
+1. Откройте **python.org → Downloads → Windows** и скачайте установщик.
+2. Запустите его.
+3. На первом же экране поставьте галочку **«Add python.exe to PATH»** — и только потом нажимайте **Install Now**.
+4. Дождитесь конца установки и закройте установщик.
-```bash
-python3 --version
-pip3 --version
-```
+Про эту галочку стоит сказать отдельно, потому что её легко пропустить, а последствия неприятные. PATH — это список папок, в которых система ищет программы, когда вы набираете команду. Без галочки Python установится, но система про него не узнает: вы наберёте `python`, а в ответ получите «команда не найдена». Поставить галочку — секунда, а искать потом причину можно полчаса.
-### Способ B: Через Homebrew
+Если галочку всё-таки пропустили, проще всего запустить установщик заново и выбрать **Modify**.
-1. Убедитесь, что установлен Homebrew (`brew -v`). Если нет, следуйте инструкции на **[brew.sh](https://brew.sh/)**.
-2. Установите Python:
+### macOS
-```bash
-brew install python
-```
+Способ первый, без лишних инструментов: скачайте установщик с **python.org → Downloads → macOS**, откройте `.pkg` и пройдите по шагам установщика.
-3. Проверьте версии:
+Способ второй — через **Homebrew**, если он у вас уже стоит (проверить: `brew -v`):
```bash
-python3 --version
-pip3 --version
+brew install python
```
-Примечание: на macOS обычно используется команда `python3` (а не `python`). Это нормально.
+### Linux
-## Установка на Linux
+Команда зависит от дистрибутива:
-Откройте терминал и выполните команды для вашего дистрибутива.
-
-- **Ubuntu / Debian:**
+- **Ubuntu / Debian:**
```bash
sudo apt update
sudo apt install -y python3 python3-pip
```
-- **Fedora:**
+- **Fedora:**
```bash
sudo dnf install -y python3 python3-pip
```
-- **Arch Linux:**
+- **Arch Linux:**
```bash
sudo pacman -S python python-pip
```
-Проверьте версии:
+## Шаг 3. Проверьте, что всё заработало
-```bash
+Закройте терминал и откройте заново — он должен узнать о новой программе. Теперь та же команда, что и в шаге 1:
+
+**macOS / Linux**
+
+```sh
python3 --version
-pip3 --version
```
-## Частые вопросы
+**Windows**
-- **Команда `python` не найдена.** На Windows попробуйте `py`. На macOS/Linux используйте `python3` и `pip3` — это нормально.
-- **Обновить `pip`:**
+```powershell
+python --version
+```
- ```bash
- python -m pip install --upgrade pip
- # или
- python3 -m pip install --upgrade pip
- ```
+Номер версии в ответ — готово, Python ваш.
+
+Если Windows отвечает «команда не найдена», попробуйте `py --version`. Не помогло — почти наверняка дело в той самой галочке PATH: перезапустите установщик и выберите **Modify**. Иногда достаточно просто перезайти в систему.
+
+На macOS и Linux команда называется `python3`, а не `python` — это не поломка, а норма: короткое имя `python` там исторически занято.
-Удачи в изучении Python! ✨
+Удачи! ✨
diff --git a/course/module-1/basic-data-types/article.md b/course/module-1/basic-data-types/article.md
index 4c41807..5088b03 100644
--- a/course/module-1/basic-data-types/article.md
+++ b/course/module-1/basic-data-types/article.md
@@ -1,38 +1,84 @@
+---
+meta:
+ title: "Базовые типы данных в Python"
+ description: "Зачем Python привязывает к каждому значению тип: как один и тот же оператор ведёт себя по-разному, как узнать тип через type и обзор встроенных типов."
+---
+
# Базовые типы данных в Python
-Тип данных — это важная концепция в программировании, определяющая характеристики данных,
-их возможные значения и операции, которые можно с ними выполнять.
+Посмотрите на две строки. Оператор один и тот же, а результат разный:
+
+```python
+print(10 + 5)
+
+
+print("10" + "5")
+
+```
+
+В первом случае `+` сложил числа, во втором — склеил две строки подряд. Python ведёт себя по-разному, потому что `10` и `"10"` для него не одно и то же: первое — число, второе — текст. К каждому значению Python привязывает **тип**, и именно тип решает, что означает операция над значением.
+
+Поэтому путаница в типах — частый источник ошибок у новичков: складываете два значения, ожидаете 15, а получаете «105», потому что значения на самом деле строки.
+
+## Как узнать тип значения
+
+Если непонятно, с чем вы имеете дело, тип можно спросить напрямую функцией `type()`.
-Переменные могут хранить данные различных типов, и разные типы могут выполнять
-разные действия.
+```python
+print(type(42))
+
+
+print(type(3.14))
+
+
+print(type("Python"))
+
+
+print(type(True))
+
+```
-Например, со строками можно выполнять операции конкатенации,
-а с числами — арифметические операции.
+В выводе `int` — целое число, `float` — число с дробной частью, `str` — строка, `bool` — логическое значение (истина или ложь). Объявлять тип заранее не нужно: Python определяет его сам по тому, что вы записали.
```python
-number = 42 # integer
-pi = 3.14 # float
-name = "Python" # string
-is_active = True # boolean
+number = 42 # int — целое число
+pi = 3.14 # float — число с дробной частью
+name = "Python" # str — строка
+is_active = True # bool — логическое значение
```
-## Встроенные типы данных в Python
+## Какие типы есть в Python
+
+Кроме четырёх базовых, у Python есть встроенные типы для коллекций и более специальных задач. Вот основные категории:
+
+| Категория | Типы |
+| ------------------- | ------------------------------ |
+| Текст | `str` |
+| Числа | `int`, `float` |
+| Логический тип | `bool` |
+| Коллекции | `list`, `tuple`, `set`, `dict` |
+| Отсутствие значения | `NoneType` (значение `None`) |
+
+В ближайших главах мы разберём числа, строки и логические значения подробно, а затем перейдём к коллекциям — спискам, кортежам, множествам и словарям. Пока достаточно одной мысли: у каждого значения есть тип, и от него зависит, что с этим значением можно делать.
+
+## Проверка понимания
-Python имеет следующие типы данных, встроенные по умолчанию:
+**Что выведет `print("3" + "4")`?**
-| Категория | Типы данных |
-| ------------------------ | ---------------------------------- |
-| Текстовый тип | `str` |
-| Числовые типы | `int`, `float`, `complex` |
-| Типы последовательностей | `list`, `tuple`, `range` |
-| Словари | `dict` |
-| Типы множеств | `set`, `frozenset` |
-| Логический тип | `bool` |
-| Бинарные типы | `bytes`, `bytearray`, `memoryview` |
-| Тип None | `NoneType` |
+1. 7 — Числа сложились бы в 7, если бы это были числа: 3 + 4. Но в кавычках это строки, а для строк + означает не сложение, а склейку.
-Это делает Python очень гибким и удобным для быстрой разработки, но
-важно понимать базовые типы данных и их особенности, чтобы писать эффективный код и
-избегать ошибок.
+2. **Правильный ответ:** 34 — Для строк оператор + склеивает их одну за другой, поэтому "3" и "4" дают "34". Чтобы получить 7, значения должны быть числами: 3 + 4.
-В дальнейшем мы обязательно рассмотрим каждый из этих типов более подробно.
+3. Ошибка: нельзя складывать строки — Складывать строки как раз можно, это разрешённая операция склейки. Ошибка возникла бы при попытке сложить строку с числом, например "3" + 4.
diff --git a/course/module-1/boolean-data-types/article.md b/course/module-1/boolean-data-types/article.md
index caaca8d..858c8b5 100644
--- a/course/module-1/boolean-data-types/article.md
+++ b/course/module-1/boolean-data-types/article.md
@@ -1,88 +1,102 @@
+---
+meta:
+ title: "Логические данные в Python"
+ description: "Подробное описание логического типа данных bool в Python: создание логических значений, операции, преобразование типов и практические примеры использования."
+---
+
# Логические данные в Python
-Давайте погрузимся в мир логических данных в Python.
-Эта тема может казаться простой, но она невероятно важна — ведь именно с помощью логических значений ваши программы будут принимать решения! 🧠
+Когда программа решает, что делать дальше, выводить ли сообщение, пропустить ли пользователя, повторить ли запрос, она работает с логическими значениями. Тип `bool` в Python хранит ровно два значения: `True` и `False`, и из них собираются все условия и проверки.
## Что такое логические данные?
-В повседневной жизни мы часто задаем вопросы с ответами "да" или "нет".
-Например: "Сегодня идет дождь?", "Вы любите пиццу?", "2 + 2 = 5?".
+В Python для значений «истина» и «ложь» существует специальный тип данных `bool`, который может принимать только два значения:
-В Python для таких ответов существует специальный тип данных — `bool`, который может принимать только два значения:
+- `True` (истина)
+- `False` (ложь)
-- `True` (истина, "да") ✅
-- `False` (ложь, "нет") ❌
+Спросим у Python тип, и он подтвердит, что перед ним `bool`:
```python
-# Создаем логические переменные
-is_raining = True # Да, идет дождь
-likes_pizza = True # Да, я люблю пиццу
-
-print(type(is_raining)) #
+is_raining = True
+print(type(is_raining))
+
```
-> 💡 **Важно**: `True` и `False` всегда пишутся с большой буквы.
-> Если написать `true` или `false`, Python не поймет и выдаст ошибку!
+> **Важно**: `True` и `False` всегда пишутся с большой буквы. Если написать `true` или `false`, Python не поймёт и выдаст ошибку.
## Логические операторы
-В жизни мы часто объединяем несколько условий.
-Например: "Поеду на пляж, если будет солнечно **И** тепло" или "Куплю этот телефон, если он красивый **ИЛИ** недорогой".
+Часто нужно объединять несколько условий. Например: «поеду на пляж, если будет солнечно **И** тепло» или «куплю этот телефон, если он красивый **ИЛИ** недорогой». В Python для этого есть три оператора: `and`, `or`, `not`.
-Python дает нам три основных логических оператора:
-
-### 1. Оператор and (логическое И)
+### Оператор and (логическое И)
Возвращает `True` только если **оба** значения истинны:
```python
-sunny = True # Солнечно
-warm = True # Тепло
+sunny = True
+warm = True
# Поеду на пляж, если солнечно И тепло
going_to_beach = sunny and warm
print(f"Солнечно: {sunny}, Тепло: {warm}")
+
print(f"Идем на пляж? {going_to_beach}")
+
# Что если погода изменится?
warm = False # Стало холодно
going_to_beach = sunny and warm
print(f"Солнечно: {sunny}, Тепло: {warm}")
+
print(f"Идем на пляж? {going_to_beach}")
+
```
-### 2. Оператор or (логическое ИЛИ)
+### Оператор or (логическое ИЛИ)
Возвращает `True`, если **хотя бы одно** значение истинно:
```python
-phone_is_beautiful = True # Телефон красивый
-phone_is_cheap = False # Но не дешевый
+phone_is_beautiful = True
+phone_is_cheap = False
# Куплю телефон, если он красивый ИЛИ недорогой
will_buy_phone = phone_is_beautiful or phone_is_cheap
print(f"Телефон красивый: {phone_is_beautiful}, Телефон дешевый: {phone_is_cheap}")
+
print(f"Купим телефон? {will_buy_phone}")
-
-```
-
-Таблица истинности для оператора `or`:
-
-```python
-print("True or True =", True or True)
-print("True or False =", True or False)
-print("False or True =", False or True)
-print("False or False =", False or False)
+
```
-### 3. Оператор not (логическое НЕ)
+### Оператор not (логическое НЕ)
Инвертирует значение: `True` становится `False`, и наоборот:
```python
have_homework = True
print(f"У меня есть домашнее задание: {have_homework}")
+
print(f"У меня НЕТ домашнего задания: {not have_homework}")
+
```
## Сравнение значений
@@ -95,23 +109,57 @@ my_age = 25
friend_age = 30
print(f"Мой возраст: {my_age}, возраст друга: {friend_age}")
+
print(f"Наш возраст одинаковый? {my_age == friend_age}")
+
print(f"Наш возраст разный? {my_age != friend_age}")
+
print(f"Я младше? {my_age < friend_age}")
+
# Сравнение строк (по алфавиту)
print(f"'apple' < 'banana': {'apple' < 'banana'}")
+
+```
+
+### Оператор is: сравнение по идентичности
+
+Помимо `==` в Python есть оператор `is`. Они кажутся похожими, но проверяют разные вещи:
+
+- `==` сравнивает **значения** (что у нас внутри)
+- `is` сравнивает **идентичность** (это один и тот же объект в памяти или нет)
-# Сравнение списков
+```python
my_scores = [90, 85, 95]
friend_scores = [90, 85, 95] # Такой же список, но другой объект
-same_list = my_scores # Тот же самый объект
+same_list = my_scores # Тот же самый объект
print(f"Содержимое одинаковое? {my_scores == friend_scores}")
+
print(f"Это один и тот же объект? {my_scores is friend_scores}")
+
print(f"same_list это тот же объект что и my_scores? {my_scores is same_list}")
+
```
+**Правило:** в подавляющем большинстве случаев нужен `==`. Оператор `is` уместен только для проверок на специальные синглтоны: `is None`, `is True`, `is False`. Применять `is` к числам, строкам, спискам почти всегда ошибка.
+
## Приоритет операторов: что вычисляется первым?
Операторы выполняются в следующем порядке (от высшего к низшему):
@@ -131,17 +179,25 @@ has_visa = True
can_travel = has_ticket and (has_passport or has_visa)
print(f"has_ticket and (has_passport or has_visa) = {can_travel}")
+
# Разберем вычисление пошагово:
step1 = has_passport or has_visa # Сначала вычисляется выражение в скобках
-print(f"\nШаг 1: has_passport or has_visa = {step1}")
+print(f"Шаг 1: has_passport or has_visa = {step1}")
+
step2 = has_ticket and step1 # Затем применяется оператор and
print(f"Шаг 2: has_ticket and (результат шага 1) = {step2}")
+
```
-> 💡 **Совет**: Если сомневаетесь в порядке выполнения, используйте скобки!
-> Они делают код более понятным и точно контролируют порядок вычислений.
+> **Совет**: если сомневаетесь в порядке выполнения, используйте скобки. Они делают код более понятным и точно контролируют порядок вычислений.
## Преобразование в логический тип: что считается истиной?
@@ -151,9 +207,21 @@ Python может преобразовать любое значение в ло
```python
print(bool(100))
+
print(bool(0))
+
print(bool("Hello"))
+
print(bool(""))
+
```
### Что считается истинным и ложным?
@@ -162,24 +230,29 @@ print(bool(""))
Ложными (`False`) считаются только:
-- `False` (логическое "нет")
-- `None` (отсутствие значения)
-- Нули: `0`, `0.0`, `0j`
-- Пустые контейнеры: `""`, `()`, `[]`, `{}`
+- `False` (логическое «нет»)
+- `None` (отсутствие значения)
+- Нули: `0`, `0.0`, `0j`
+- Пустые контейнеры: `""`, `()`, `[]`, `{}`
```python
-# Примеры в условиях
money = 0
if money:
- print("У меня есть деньги!")
+ print("У меня есть деньги")
else:
- print("Мой кошелек пуст :(") # Выведется это
+ print("Мой кошелек пуст")
+
name = "Алекс"
if name:
- print(f"Привет, {name}!") # Выведется это
+ print(f"Привет, {name}")
else:
- print("Привет, незнакомец!")
+ print("Привет, незнакомец")
+
```
## Проверка понимания
@@ -192,7 +265,10 @@ else:
result = (False or True) and not (False and True or True)
```
+1. True — Разберём по шагам: (False or True) = True; внутри второй скобки (False and True) = False, потом False or True = True; not True = False; True and False = False. Правильный ответ — False.
+
+2. **Правильный ответ:** False — Разберём по шагам: (False or True) = True; внутри второй скобки (False and True) = False, потом False or True = True; not True = False; True and False = False.
+
+3. None — None здесь взяться неоткуда: в выражении участвуют только True и False, поэтому результатом будет одно из них. Стоит запомнить на будущее: and и or возвращают не «истину вообще», а одно из тех значений, к которым применены, — окажись среди них None, он мог бы и вернуться.
-Теперь вы знаете, как работают логические данные в Python.
-Хотя они могут принимать всего два значения — `True` и `False`,
-их значение в программировании трудно переоценить.
+4. Ошибка синтаксиса — Это корректное логическое выражение в Python.
diff --git a/course/module-1/complex-data-types/article.md b/course/module-1/complex-data-types/article.md
index 189d317..85a0b7a 100644
--- a/course/module-1/complex-data-types/article.md
+++ b/course/module-1/complex-data-types/article.md
@@ -1,6 +1,14 @@
+---
+meta:
+ title: "Сложные типы данных в Python"
+ description: "Обзор сложных типов данных в Python: списки, кортежи, множества и словари. Их особенности, отличия и применение."
+---
+
# Сложные типы данных в Python
-Мы уже познакомились с простыми (скалярными) типами данных в Python. Теперь пришло время перейти к более мощным инструментам — сложным (составным) типам данных, которые позволяют хранить и обрабатывать коллекции значений.
+До сих пор каждая переменная хранила одно значение: имя, возраст, цену. Но данные редко приходят поодиночке. Список покупок — это много товаров. Точка на карте — широта и долгота вместе. Профиль пользователя — имя, почта и город под одним объектом. Заводить отдельную переменную под каждое значение быстро становится невозможно.
+
+Для таких случаев в Python есть сложные типы: они хранят сразу много значений под одним именем. Их четыре, и различаются они тем, как организуют данные внутри.
## Что такое сложные типы данных?
@@ -33,39 +41,41 @@ person = {"name": "Алекс", "age": 30, "city": "Москва"}
Ключевые различия между основными сложными типами данных:
-| Тип данных | Синтаксис | Основные свойства | Когда использовать |
-| ------------------- | ------------------ | -------------------------------------------------------------------------- | ------------------------------------------------------ |
-| **Список (list)** | `[1, 2, 3]` | ✅ Изменяемый
✅ Упорядоченный
✅ Доступ по индексу | Когда важен порядок и нужно изменять коллекцию |
-| **Кортеж (tuple)** | `(1, 2, 3)` | ❌ Неизменяемый
✅ Упорядоченный
✅ Доступ по индексу | Для защиты данных от изменений (координаты, константы) |
-| **Множество (set)** | `{1, 2, 3}` | ✅ Изменяемый
❌ Неупорядоченный
❌ Нет индексов | Когда нужны только уникальные элементы |
-| **Словарь (dict)** | `{"key": "value"}` | ✅ Изменяемый
✅ Упорядоченный (с Python 3.7)
✅ Доступ по ключу | Для связи значений с уникальными ключами |
+| Тип данных | Синтаксис | Основные свойства | Когда использовать |
+| ------------------- | ------------------ | ---------------------------------------------------------------------------- | ------------------------------------------------------ |
+| **Список (list)** | `[1, 2, 3]` | ✅ Изменяемый
✅ Упорядоченный
✅ Доступ по индексу | Когда важен порядок и нужно изменять коллекцию |
+| **Кортеж (tuple)** | `(1, 2, 3)` | ❌ Неизменяемый
✅ Упорядоченный
✅ Доступ по индексу | Для защиты данных от изменений (координаты, константы) |
+| **Множество (set)** | `{1, 2, 3}` | ✅ Изменяемый
❌ Неупорядоченный
❌ Нет индексов | Когда нужны только уникальные элементы |
+| **Словарь (dict)** | `{"key": "value"}` | ✅ Изменяемый
✅ Упорядоченный (с Python 3.7)
✅ Доступ по ключу | Для связи значений с уникальными ключами |
+
+> **Внимание**: фигурные скобки `{}` без элементов создают **пустой словарь**, а не пустое множество. Для создания пустого множества используйте `set()`.
## Когда использовать разные типы данных?
Выбор подходящего типа данных зависит от задачи:
-- **Список (list)** — когда важен порядок элементов и коллекция может изменяться
+- **Список (list)** — когда важен порядок элементов и коллекция может изменяться
```python
# Список задач на день
todo_list = ["Купить продукты", "Выгулять собаку", "Написать код"]
```
-- **Кортеж (tuple)** — когда данные не должны изменяться и важен порядок
+- **Кортеж (tuple)** — когда данные не должны изменяться и важен порядок
```python
# Координаты точки в 3D пространстве
point_3d = (10.5, 8.3, 9.1)
```
-- **Множество (set)** — когда нужны только уникальные элементы и не важен порядок
+- **Множество (set)** — когда нужны только уникальные элементы и не важен порядок
```python
# Уникальные имена пользователей
unique_users = {"alice", "bob", "charlie"}
```
-- **Словарь (dict)** — когда данные организованы в виде пар ключ-значение
+- **Словарь (dict)** — когда данные организованы в виде пар ключ-значение
```python
# Информация о пользователе
user = {"username": "alex123", "email": "alex@example.com", "active": True}
@@ -96,15 +106,12 @@ my_dict = dict(pairs) # {"a": 1, "b": 2, "c": 3}
**Какой тип данных лучше всего подходит для хранения уникальных элементов, когда порядок не имеет значения?**
+1. Список (list) — Списки сохраняют порядок элементов, но не гарантируют их уникальность.
-## Заключение
-
-Сложные типы данных в Python — это мощные инструменты, которые позволяют организовывать,
-хранить и обрабатывать коллекции значений.
+2. Кортеж (tuple) — Кортежи, как и списки, сохраняют порядок элементов, но не гарантируют их уникальность.
-Выбор правильного типа данных для конкретной задачи может значительно упростить ваш код и сделать его более эффективным.
+3. **Правильный ответ:** Множество (set) — Множества хранят только уникальные элементы и не сохраняют порядок их добавления.
-В следующих статьях мы подробно рассмотрим каждый из этих типов, изучим их методы и
-особенности работы с ними.
+4. Словарь (dict) — Словари хранят пары ключ-значение, где ключи уникальны, но это не оптимальный выбор, если вам нужно просто хранить набор уникальных элементов.
-Давайте начнем с изучения [списков](/guide/lists), самого гибкого и часто используемого сложного типа данных в Python!
+В следующих уроках разберём каждый из этих типов подробно. Начнём со [списков](https://python-academy.org/ru/guide/lists), самого гибкого и часто используемого сложного типа.
diff --git a/course/module-1/dictionaries/article.md b/course/module-1/dictionaries/article.md
index 7a1b4b0..1d73052 100644
--- a/course/module-1/dictionaries/article.md
+++ b/course/module-1/dictionaries/article.md
@@ -1,299 +1,285 @@
+---
+meta:
+ title: "Словари в Python"
+ description: "Словари в Python: зачем они нужны вместо параллельных списков, доступ через квадратные скобки и get, добавление и удаление ключей, перебор и подсчёт частоты."
+---
+
# Словари в Python
-В этой статье мы разберем словари (dict) — один из самых мощных и гибких типов данных
-в Python.
+Допустим, вы храните оценки студентов. Первая мысль — два списка: имена и оценки, и одинаковая позиция связывает их.
-## Что такое словарь?
+```python
+names = ["John", "Mary", "Kate"]
+grades = [90, 75, 80]
+```
-Словарь в Python — это коллекция пар "ключ-значение".
-Можно представить словарь как телефонную книгу,
-где имена людей (ключи) связаны с их номерами телефонов (значения).
+Чтобы узнать оценку Мэри, сначала нужно найти, на какой позиции она стоит в `names`, а потом прочитать ту же позицию в `grades`. Добавляете студента — нельзя забыть дописать значение во второй список. Стоит одному списку сбиться на один элемент, и Мэри получает чужую оценку, а программа об этом даже не сообщит. Связь между именем и оценкой держится только в вашей голове, а не в коде.
-Основные свойства словарей:
+Словарь убирает посредника-индекс и связывает имя со значением напрямую.
+
+```python
+grades = {"John": 90, "Mary": 75, "Kate": 80}
+print(grades["Mary"])
+
+```
-- **Ключи должны быть уникальными** — нельзя иметь два одинаковых ключа в одном словаре
-- **Ключи должны быть неизменяемыми** — в качестве ключей можно использовать строки, числа, кортежи, но не списки или словари
-- **Значения могут быть любого типа** — числа, строки, списки, другие словари и т.д.
-- **Начиная с Python 3.7, словари сохраняют порядок добавления элементов** (раньше порядок не гарантировался)
+> Словарь (dict) — это коллекция пар «ключ — значение». Вы обращаетесь к значению по ключу, а не по числовой позиции.
-## Создание словарей
+Телефонная книга устроена так же: вы ищете человека по имени, а не по номеру строки.
-Существует несколько способов создать словарь в Python:
+## Как создать словарь
-### 1. С помощью фигурных скобок {}
+Чаще всего словарь записывают прямо, в фигурных скобках: пара `ключ: значение`, пары через запятую.
```python
-# Пустой словарь
-empty_dict = {}
+# Пустой словарь, который заполним позже
+prices = {}
-# Словарь с данными
+# Словарь сразу с данными
person = {"name": "John", "age": 30, "city": "New York"}
print(person)
-
-# Вложенные словари
-nested = {
- "person": {"name": "Mary", "age": 25},
- "contacts": {"email": "mary@example.com", "phone": "+1234567890"}
-}
-print(nested["person"])
+
```
-### 2. С помощью конструктора dict()
+Если пары уже лежат где-то готовыми, например приходят парами, словарь можно собрать функцией `dict()`.
```python
-# Пустой словарь
-empty_dict = dict()
-
-# Из списка кортежей (пар ключ-значение)
pairs = [("name", "Anna"), ("age", 28), ("city", "Berlin")]
person = dict(pairs)
print(person)
-
-# С использованием именованных аргументов (только для строковых ключей)
-settings = dict(theme="dark", font_size=12, notifications=True)
-print(settings)
+
```
-### 3. Генераторы словарей (dict comprehensions)
+## Чтение значения: скобки против get
-```python
-# Словарь квадратов чисел (число: квадрат числа)
-squares = {x: x**2 for x in range(1, 6)}
-print(squares)
-```
-
-### 4. Метод fromkeys() для создания словаря с одинаковыми значениями
+Самый прямой способ получить значение — квадратные скобки с ключом.
```python
-# Создание словаря с заданными ключами и одинаковым значением для всех
-keys = ["name", "age", "city"]
-defaults = dict.fromkeys(keys, None)
-print(defaults)
+person = {"name": "John", "age": 30}
+print(person["name"])
+
```
-## Доступ к элементам словаря
-
-Доступ к значениям в словаре осуществляется через ключи:
-
-### Обращение по ключу
+Но если ключа в словаре нет, скобки не возвращают пустоту, а останавливают программу с ошибкой `KeyError`.
```python
-person = {"name": "John", "age": 30, "city": "New York"}
-
-# Получение значения по ключу
-name = person["name"]
-print(name)
-
-# Безопасный способ с помощью метода get()
-phone = person.get("phone") # Вернет None, если ключа нет
-print(phone)
-
-# Метод get() с значением по умолчанию
-phone = person.get("phone", "Not specified")
-print(phone)
+person = {"name": "John", "age": 30}
+print(person["phone"]) # KeyError: 'phone' — такого ключа в словаре нет
```
-### Проверка наличия ключа
+Это не недостаток, а защита: чаще всего обращение к несуществующему ключу — это опечатка или сломанная логика, и лучше узнать о ней сразу. Скобки уместны, когда вы уверены, что ключ есть.
+
+Когда ключа может и не быть, есть метод `get()`. Он вернёт `None` вместо ошибки, а если передать второй аргумент — то его как значение по умолчанию.
```python
-person = {"name": "John", "age": 30, "city": "New York"}
+person = {"name": "John", "age": 30}
-# Проверка наличия ключа
-print("name" in person)
+print(person.get("phone"))
+
-print("phone" not in person)
+print(person.get("phone", "не указан"))
+
```
-### Доступ к вложенным словарям
-
-```python
-nested = {
- "person": {"name": "Mary", "age": 25},
- "contacts": {"email": "mary@example.com", "phone": "+1234567890"}
-}
-
-# Доступ к значениям во вложенных словарях
-name = nested["person"]["name"]
-print(name)
-
-# Безопасный доступ с помощью get()
-email = nested.get("contacts", {}).get("email")
-print(email)
-
-# Если путь не существует, вернется None или значение по умолчанию
-website = nested.get("contacts", {}).get("website", "Not specified")
-print(website)
-```
+Отсюда простое правило выбора: скобки — когда отсутствие ключа это ошибка, `get()` — когда отсутствие это нормальный случай, который вы готовы обработать.
-## Изменение словарей
+## Добавление и изменение
-Словари в Python являются изменяемыми, поэтому их можно легко модифицировать:
-
-### Добавление и изменение элементов
+У словаря один и тот же синтаксис и добавляет ключ, и меняет существующий: присваивание по ключу. Если ключа не было — он появится, если был — значение перезапишется.
```python
-# Создаем словарь
person = {"name": "John", "age": 30}
-# Добавление нового ключа и значения
+# Ключа "city" не было — он добавляется
person["city"] = "New York"
print(person)
+
-# Изменение значения существующего ключа
+# Ключ "age" уже есть — значение заменяется
person["age"] = 31
print(person)
-
-# Использование метода update() для массового обновления
-person.update({"age": 32, "job": "developer", "language": "Python"})
-print(person)
+
```
-### Удаление элементов
+Когда нужно внести сразу несколько изменений, удобнее `update()`: он добавляет новые ключи и перезаписывает совпавшие за один вызов.
```python
-person = {"name": "John", "age": 30, "city": "New York", "job": "developer"}
-
-# Удаление элемента по ключу с помощью del
-del person["job"]
-print(person)
-
-# Удаление и возврат элемента с помощью pop()
-age = person.pop("age")
-print(age)
-print(person)
-
-# pop() с значением по умолчанию (не вызывает ошибку, если ключа нет)
-job = person.pop("job", "Not specified")
-print(job)
-
-# Удаление и возврат произвольного элемента с помощью popitem()
-# В Python 3.7+ возвращает последний добавленный элемент
-item = person.popitem()
-print(item)
-print(person)
-
-# Удаление всех элементов
-person.clear()
+person = {"name": "John", "age": 31}
+person.update({"age": 32, "job": "developer"})
print(person)
+
```
-## Методы словарей
+## Проверка наличия ключа
-Python предоставляет множество полезных методов для работы со словарями:
-
-### Получение ключей, значений и пар ключ-значение
+Чтобы заранее узнать, есть ли ключ, используют оператор `in`. Это и есть способ не нарваться на `KeyError`, когда скобки всё-таки нужны.
```python
-person = {"name": "John", "age": 30, "city": "New York"}
+person = {"name": "John", "age": 30}
-# Получение всех ключей
-keys = person.keys()
-print(keys)
+print("name" in person)
+
+
+print("phone" in person)
+
+```
-# Получение всех значений
-values = person.values()
-print(values)
+## Удаление
-# Получение всех пар ключ-значение
-items = person.items()
-print(items)
+Убрать ключ можно оператором `del`. Но если ключа нет, `del` тоже падает с `KeyError`.
-# Объекты dict_keys, dict_values и dict_items являются представлениями словаря
-# Они динамически отражают изменения в словаре
-person["job"] = "developer"
-print(keys)
+```python
+person = {"name": "John", "age": 30, "job": "developer"}
-# Преобразование представлений в списки
-keys_list = list(keys)
-print(keys_list)
+del person["job"]
+print(person)
+
```
-### Копирование словарей
+Метод `pop()` удаляет ключ и заодно возвращает его значение — пригодится, когда удаляемое значение ещё нужно. Со вторым аргументом он не падает на отсутствующем ключе, а возвращает значение по умолчанию.
```python
-import copy
+person = {"name": "John", "age": 30}
-# Словарь с вложенной структурой
-original = {"name": "John", "settings": {"theme": "dark"}}
+age = person.pop("age")
+print(age)
+
-# Поверхностная и глубокая копии
-shallow_copy = original.copy()
-deep_copy = copy.deepcopy(original)
+# Ключа "phone" нет, но второй аргумент спасает от ошибки
+phone = person.pop("phone", "не указан")
+print(phone)
+
+```
-# Изменение простого ключа работает одинаково в обоих случаях
-shallow_copy["name"] = "Peter"
-deep_copy["name"] = "Alice"
-print(f"original['name']: {original['name']}") # Остается John
+## Перебор словаря
-# Различия проявляются при изменении вложенных структур
-shallow_copy["settings"]["theme"] = "light"
-print(f"После shallow copy - original['settings']['theme']: {original['settings']['theme']}") # Меняется!
+По словарю можно пройтись циклом. По умолчанию цикл `for` идёт по ключам, а значение легко достать по ключу.
-# Создадим новый оригинал для демонстрации deep copy
-original = {"name": "John", "settings": {"theme": "dark"}}
-deep_copy = copy.deepcopy(original)
-deep_copy["settings"]["theme"] = "light"
-print(f"После deep copy - original['settings']['theme']: {original['settings']['theme']}") # Не меняется
+```python
+grades = {"John": 90, "Mary": 75, "Kate": 80}
+
+for name in grades:
+ print(name, ":", grades[name])
+
```
-### Объединение словарей
+Если внутри цикла нужны и ключ, и значение, метод `items()` отдаёт их парой сразу, без обращения по ключу.
```python
-# Объединение словарей с update
-dict1 = {"a": 1, "b": 2}
-dict2 = {"b": 3, "c": 4}
+grades = {"John": 90, "Mary": 75, "Kate": 80}
+
+for name, grade in grades.items():
+ print(name, ":", grade)
+
+```
-# Создаем копию dict1 и обновляем её данными из dict2
-merged = dict1.copy()
-merged.update(dict2)
-print(merged)
+Есть и парные методы: `keys()` отдаёт только ключи, `values()` — только значения. Они нужны, когда вторая половина пары в цикле не используется.
-# В Python 3.9+ можно использовать оператор |
-# merged = dict1 | dict2
+```python
+grades = {"John": 90, "Mary": 75, "Kate": 80}
+
+total = 0
+for grade in grades.values():
+ total = total + grade
+print("Сумма баллов:", total)
+
```
-### Установка значения по умолчанию
+## Частая задача: подсчёт
+
+Словарь хорошо подходит, чтобы что-то посчитать: ключ — это объект, значение — счётчик. Посчитаем, сколько раз встречается каждое слово.
+
+В лоб это выглядит так: для каждого слова проверяем, видели ли мы его раньше, и либо увеличиваем счётчик, либо заводим новый.
```python
-# Подсчет частоты слов
-counts = {}
text = "one two one two three"
words = text.split()
-# Способ 1: проверка наличия ключа
+counts = {}
for word in words:
if word in counts:
- counts[word] += 1
+ counts[word] = counts[word] + 1
else:
counts[word] = 1
print(counts)
+
+```
+
+Здесь как раз помогает `get()` со значением по умолчанию: «возьми текущий счётчик, а если слова ещё не было — считай, что был ноль». Условие `if/else` сворачивается в одну строку.
+
+```python
+text = "one two one two three"
+words = text.split()
-# Способ 2: использование get()
counts = {}
for word in words:
counts[word] = counts.get(word, 0) + 1
print(counts)
+
```
-## Итерация по словарям
+## Что годится в ключи
+
+Ключи живут по двум правилам, и оба следуют из того, как словарь устроен внутри.
-Существует несколько способов перебора элементов словаря:
+- **Ключи уникальны.** Записать два одинаковых ключа нельзя: второе присваивание просто перезапишет первое. Это логично — иначе по ключу `"Mary"` было бы непонятно, какое из значений возвращать.
+- **Ключ должен быть неизменяемым.** Подходят строки, числа, кортежи. Список ключом быть не может: Python находит значение по ключу через его неизменяемое содержимое, а список можно поменять уже после того, как он стал ключом, и значение «потеряется».
```python
-person = {"name": "John", "age": 30, "city": "New York"}
+# Список в роли ключа — ошибка
+broken = {[1, 2]: "value"} # TypeError: unhashable type: 'list'
+```
-# Итерация по ключам (способ по умолчанию)
-print("Итерация по ключам:")
-for key in person:
- print(key, ":", person[key])
+Значения же могут быть любыми: числа, строки, списки и даже другие словари.
-# Итерация по значениям
-print("Итерация по значениям:")
-for value in person.values():
- print(value)
+## Проверка понимания
-# Итерация по парам ключ-значение
-print("Итерация по парам ключ-значение:")
-for key, value in person.items():
- print(key, ":", value)
-```
+**Что выведет `print(person["phone"])`, если в словаре `person` нет ключа `"phone"`?**
+
+1. Вернётся None, и программа продолжит работу — None при отсутствии ключа возвращает метод get(). А вот квадратные скобки на несуществующем ключе останавливают программу ошибкой KeyError.
+
+2. **Правильный ответ:** Программа остановится с ошибкой KeyError — Обращение через скобки требует, чтобы ключ существовал. Если он может отсутствовать, безопаснее person.get("phone") — он вернёт None или значение по умолчанию.
+
+3. В словарь добавится ключ "phone" с пустым значением — Чтение по ключу никогда ничего не добавляет в словарь. Новый ключ появляется только при присваивании: person\["phone"] = ....
diff --git a/course/module-1/functions/article.md b/course/module-1/functions/article.md
index 9bb9e3f..8188244 100644
--- a/course/module-1/functions/article.md
+++ b/course/module-1/functions/article.md
@@ -1,25 +1,26 @@
-# Функции в Python
+---
+meta:
+ title: "Функции в Python"
+ description: "Как перестать копировать один и тот же код: объявление функции через def, параметры и аргументы, return и значения по умолчанию."
+---
-Представьте, что вы готовите сложное блюдо. Вместо того чтобы каждый раз заново придумывать рецепт, вы можете использовать уже готовый. Функции в Python работают похожим образом! 🧩
+# Функции в Python
-Функции — это как готовые рецепты для вашего кода. Они позволяют вам создавать блоки кода, которые можно использовать многократно, делая ваши программы более организованными и легкими для понимания.
+Чтобы не копировать один и тот же блок кода в нескольких местах программы, его описывают как функцию и потом вызывают по имени. Функция принимает входные значения, что-то с ними делает и обычно возвращает результат.
## Что такое функции?
-> Функция — это именованный блок кода, который выполняет определенную задачу и может быть вызван из других частей программы.
+> Функция — это именованный блок кода, который выполняет определённую задачу и может быть вызван из других частей программы.
-Функции помогают:
+Удобнее всего представлять функцию как «чёрный ящик»: на вход подаём аргументы, внутри что-то происходит, наружу возвращается результат.
-- Организовать код в логические блоки
-- Избежать повторения кода
-- Сделать программу более читаемой
-- Упростить отладку и поддержку
+
## Создание и вызов функций
### Базовый синтаксис
-Функция в Python создается с помощью ключевого слова `def`:
+Функция в Python создаётся с помощью ключевого слова `def`:
```python
# Простой пример функции
@@ -28,28 +29,29 @@ def greet():
# Вызов функции
greet()
+
```
-В этом примере:
-
-1. `def` указывает, что мы создаем функцию
-2. `greet` — это имя функции
-3. `()` — скобки для параметров (пока пустые)
-4. `:` — двоеточие после определения
-5. Тело функции с отступом
+Здесь `def` объявляет функцию, `greet` — её имя, в скобках пишутся параметры (тут их нет), после двоеточия идёт тело функции с отступом.
### Функции с параметрами
-Функции могут принимать параметры — значения, которые передаются в функцию при её вызове:
+Функции могут принимать данные извне. Имена, которые вы пишете в скобках при объявлении, называются **параметрами**, а конкретные значения, подставленные при вызове, — **аргументами**. У функции ниже параметр один — `name`, а вызываем мы её дважды с разными аргументами: `"Анна"` и `"Петр"`.
```python
-# Функция с параметрами
def greet(name):
print(f"Привет, {name}!")
-# Вызов функции с аргументом
greet("Анна")
+
greet("Петр")
+
```
### Возвращаемые значения
@@ -64,11 +66,46 @@ def add(a, b):
# Использование результата функции
result = add(5, 3)
print(f"5 + 3 = {result}")
+
+```
+
+`return` сразу завершает функцию: код после него не выполнится. Это удобно, чтобы выйти из функции пораньше при особых условиях:
+
+```python
+def describe(value):
+ if value < 0:
+ return "отрицательное"
+ return "неотрицательное"
+
+print(describe(-5))
+
+print(describe(10))
+
+```
+
+Если в функции нет `return`, она всё равно возвращает значение — `None`. Это нормально для функций, которые ничего не вычисляют, а просто что-то делают (например, печатают на экран):
+
+```python
+def say_hi():
+ print("Привет!")
+
+result = say_hi()
+print(result)
+
```
## Параметры функций
-### Позиционные и именованные параметры
+### Позиционные и именованные аргументы
Python поддерживает два способа передачи аргументов в функцию:
@@ -79,9 +116,15 @@ def describe_pet(animal_type, pet_name):
# Позиционные аргументы
describe_pet("собака", "Шарик")
+
# Именованные аргументы
describe_pet(pet_name="Мурка", animal_type="кошка")
+
```
### Параметры по умолчанию
@@ -95,40 +138,47 @@ def describe_pet(pet_name, animal_type="собака"):
# Использование значения по умолчанию
describe_pet("Шарик")
+
# Переопределение значения по умолчанию
describe_pet("Мурка", "кошка")
+
```
### Произвольное количество аргументов
-Иногда вы не знаете заранее, сколько аргументов будет передано в функцию. Для этого используются специальные параметры:
+Иногда вы не знаете заранее, сколько аргументов будет передано в функцию. На помощь приходят специальные параметры: звёздочка перед именем (`*toppings`) собирает все позиционные аргументы вместе, а две звёздочки (`**user_info`) — все именованные.
```python
-# Функция с произвольным количеством позиционных аргументов
+# Произвольное количество позиционных аргументов
def make_pizza(*toppings):
- print("Готовим пиццу со следующими начинками:")
- for topping in toppings:
- print(f"- {topping}")
+ print("Начинки:", toppings)
make_pizza("пепперони")
+
make_pizza("грибы", "зеленый перец", "дополнительный сыр")
-
-# Функция с произвольным количеством именованных аргументов
-def build_profile(first, last, **user_info):
- profile = {}
- profile['first_name'] = first
- profile['last_name'] = last
- for key, value in user_info.items():
- profile[key] = value
- return profile
-
-user_profile = build_profile('Анна', 'Иванова',
- location='Москва',
- field='программирование')
-print(user_profile)
+
+
+# Произвольное количество именованных аргументов
+def build_profile(**user_info):
+ print(user_info)
+
+build_profile(name="Анна", location="Москва", field="программирование")
+
```
+Все переданные значения Python собирает в одну структуру: позиционные — в набор значений, именованные — в пары «имя-значение». Что это за структуры и как перебрать их по одному, разберём в главах про коллекции и циклы.
+
## Проверка понимания
Давайте проверим, насколько хорошо вы усвоили тему функций:
@@ -142,3 +192,27 @@ def mystery(x, y=2):
result = mystery(3)
print(result)
```
+
+1. 3 — Функция умножает x на y, где y по умолчанию равно 2.
+
+2. **Правильный ответ:** 6 — Функция умножает 3 на 2 (значение по умолчанию для y).
+
+3. Ошибка — Код выполнится без ошибок, так как y имеет значение по умолчанию.
+
+4. 9 — Для получения 9 нужно было бы передать 3 и 3 в качестве аргументов.
+
+## Задания для самопроверки
+
+Сразу под этой статьёй есть блок **Задания для самопроверки**. Здесь впервые нужно написать функцию целиком: строка `def` с именем и параметрами уже стоит в редакторе, а тело дописываете вы.
+
+### Откуда берутся значения
+
+Значения, с которыми работает функция, приходят в её параметрах. Придумывать или вводить их не нужно: каждый тест сам вызывает функцию со своими значениями.
+
+
+
+На вкладке теста видно всё сразу: какие значения он подставит, как вызовет функцию и какой результат ждёт. У каждого теста значения свои, и пройти должны все.
+
+### Результат нужно вернуть
+
+Функция должна **возвращать** результат через `return`. Если напечатать его через `print()`, проверка тест не засчитает: она смотрит на возвращённое значение, а не на то, что появилось на экране.
diff --git a/course/module-1/if-statement/article.md b/course/module-1/if-statement/article.md
index 43b156b..2f7f88f 100644
--- a/course/module-1/if-statement/article.md
+++ b/course/module-1/if-statement/article.md
@@ -1,33 +1,35 @@
-# Условная конструкция. Оператор if
-
-В этой статье мы познакомимся с условными конструкциями в Python — одним
-из самых важных инструментов в программировании. Без них наши программы были бы как поезд,
-который может ехать только по прямой, без возможности выбрать путь на развилке.
-
-Условные конструкции позволяют программе "принимать решения" и выбирать разные действия в зависимости от обстоятельств.
+---
+meta:
+ title: "Условная конструкция. Оператор if"
+ description: "Подробное объяснение условных конструкций в Python: синтаксис, варианты использования, вложенные условия и практические примеры."
+---
-## Что такое условная конструкция?
+# Условная конструкция. Оператор if
-Условная конструкция — это инструмент, позволяющий выполнять различные блоки кода
-в зависимости от того, истинно ли (`True`) или ложно (`False`) определённое условие.
+Программа не всегда идёт по прямой. Пускать ли пользователя на страницу «18+», показывать ли скидку, сохранять ли файл или сразу предупредить, что он уже есть, — на каждой такой развилке нужно проверить условие и в зависимости от ответа пойти по одному пути или по другому.
-В Python основной условной конструкцией является оператор `if`.
-Он проверяет, истинно ли условие, и если да — выполняет соответствующий блок кода.
+В Python развилку задаёт оператор `if`: он проверяет условие и выполняет блок кода, только если оно истинно.
## Основные конструкции условного оператора
### Синтаксис оператора if
-Базовый синтаксис условной конструкции if в Python выглядит так:
+Базовый синтаксис условной конструкции `if` в Python выглядит так:
```python
# Простой пример оператора if
age = 18
if age >= 18:
- print("Вы совершеннолетний!")
+ print("Вы совершеннолетний.")
+
print("Эта строка выполнится всегда, независимо от условия.")
+
```
Обратите внимание на несколько важных деталей:
@@ -42,37 +44,43 @@ print("Эта строка выполнится всегда, независим
age = 16
if age >= 18:
- print("Вы совершеннолетний!")
+ print("Вы совершеннолетний.")
print("Эта строка выполнится всегда, независимо от условия.")
+
```
В этом случае условие `age >= 18` не истинно (ложно), поэтому блок кода внутри `if` не выполняется, и мы видим только вторую строку.
-### Использование else — "иначе"
+### Использование else (иначе)
-Часто нам нужно выполнить один блок кода, если условие истинно, и другой — если условие ложно. Для этого используется конструкция `if-else`:
+Часто нам нужно выполнить один блок кода, если условие истинно, и другой — если оно ложно. Для этого используется конструкция `if-else`:
```python
temperature = 15
if temperature > 20:
- print("Сегодня тепло, можно идти в футболке!")
+ print("Сегодня тепло, можно идти в футболке.")
else:
- print("Сегодня прохладно, возьмите куртку!")
+ print("Сегодня прохладно, возьмите куртку.")
+
```
Здесь если температура выше 20, выполнится первый блок. В противном случае выполняется блок после `else`. В нашем примере температура 15, что меньше 20, поэтому выполняется код в блоке `else`.
-### Конструкция elif — "иначе, если"
+### Конструкция elif (иначе если)
-Что если у нас больше двух вариантов выбора? Например, мы хотим разные сообщения для разных диапазонов температур. Для этого используется конструкция `elif` (сокращение от "else if"):
+Что если у нас больше двух вариантов выбора? Например, мы хотим разные сообщения для разных диапазонов температур. Для этого используется конструкция `elif` (сокращение от «else if»):
```python
temperature = 25
if temperature < 0:
- print("Очень холодно! Наденьте тёплую куртку и шапку!")
+ print("Очень холодно. Наденьте тёплую куртку и шапку.")
elif temperature < 10:
print("Холодно. Оденьтесь теплее.")
elif temperature < 20:
@@ -80,12 +88,15 @@ elif temperature < 20:
elif temperature < 30:
print("Тепло. Футболка и шорты подойдут.")
else:
- print("Жарко! Возьмите с собой воду!")
+ print("Жарко. Возьмите с собой воду.")
+
```
В этом примере Python проверяет каждое условие по порядку. Как только находится первое истинное условие, выполняется соответствующий блок кода, а все последующие условия пропускаются. Если ни одно из условий не истинно, выполняется блок `else` (при его наличии).
-> **Важно**: Python проверяет условия сверху вниз и останавливается, как только находит первое истинное условие. Порядок условий имеет значение!
+> **Важно**: Python проверяет условия сверху вниз и останавливается, как только находит первое истинное условие. Порядок условий имеет значение.
## Расширенные возможности условных конструкций
@@ -106,19 +117,23 @@ if age >= 18:
print("Для вождения автомобиля вам необходимо получить права.")
else:
print("Вы несовершеннолетний и не можете водить автомобиль.")
+
```
-Однако не увлекайтесь вложенными условиями — код с большим количеством уровней вложенности становится трудночитаемым. Часто такую логику можно переписать с использованием логических операторов.
+Однако не увлекайтесь вложенными условиями: код с большим количеством уровней вложенности становится трудночитаемым. Часто такую логику можно переписать с использованием логических операторов.
### Логические операторы в условиях
Для создания сложных условий используются логические операторы:
-- `and` — логическое И (условие истинно, если истинны оба выражения)
-- `or` — логическое ИЛИ (условие истинно, если истинно хотя бы одно выражение)
-- `not` — логическое НЕ (инвертирует значение выражения)
+- `and` (логическое И): условие истинно, если истинны оба выражения
+- `or` (логическое ИЛИ): условие истинно, если истинно хотя бы одно выражение
+- `not` (логическое НЕ): инвертирует значение выражения
-Давайте перепишем наш пример с вложенным условием:
+Перепишем наш пример с вложенным условием:
```python
age = 25
@@ -130,24 +145,28 @@ elif age >= 18 and not has_license:
print("Вы совершеннолетний, но для вождения автомобиля вам необходимо получить права.")
else:
print("Вы несовершеннолетний и не можете водить автомобиль.")
+
```
-Такой код более читаемый и его проще поддерживать.
-
-Рассмотрим еще пример с оператором `or`:
+Рассмотрим ещё пример с оператором `or`:
```python
day = "суббота"
if day == "суббота" or day == "воскресенье":
- print("Выходной день! Можно отдохнуть. 😎")
+ print("Выходной день, можно отдохнуть.")
else:
- print("Рабочий день. Нужно идти на работу. 💼")
+ print("Рабочий день, нужно идти на работу.")
+
```
### Тернарный оператор
-Для простых условий можно использовать тернарный оператор — компактную форму записи условия:
+Для простых условий можно использовать тернарный оператор — компактную форму записи:
```python
age = 20
@@ -159,10 +178,16 @@ else:
status = "несовершеннолетний"
print(f"Обычная запись: Вы {status}")
+
# Та же логика, но с использованием тернарного оператора
status = "совершеннолетний" if age >= 18 else "несовершеннолетний"
print(f"Тернарный оператор: Вы {status}")
+
```
Синтаксис тернарного оператора:
@@ -171,40 +196,146 @@ print(f"Тернарный оператор: Вы {status}")
значение_если_истина if условие else значение_если_ложь
```
-Тернарный оператор делает код более компактным, но не злоупотребляйте им — сложные условия лучше записывать в обычном формате для читаемости.
+Не злоупотребляйте им: сложные условия лучше записывать обычным `if-else`, так читаемее.
-## Практические примеры использования условий
+## Pythonic-стиль для условий
+
+В Python есть несколько идиоматических приёмов, которые делают условия короче и читаемее. Стоит привыкать к ним сразу.
+
+### Truthy-проверки
+
+Новички часто пишут условия явным сравнением с `True`, `False`, `0`, пустой строкой:
+
+```python
+if is_active == True:
+if my_list == []:
+if name == "":
+```
+
+Из урока про `bool` мы знаем, что любое значение в условии само по себе обрабатывается как `True` или `False`. Поэтому такие проверки можно упростить:
+
+```python
+if is_active: # уже bool
+if not my_list: # пустой список falsy
+if not name: # пустая строка falsy
+```
+
+Это и короче, и привычнее для глаза любого Python-программиста.
+
+### Сравнение цепочкой
-Условные конструкции широко применяются в повседневном программировании. Рассмотрим несколько практических примеров:
+Когда нужно проверить, что значение попадает в диапазон, в большинстве языков пишут так:
-### Проверка четности числа
+```python
+if x >= 0 and x < 100:
+```
+
+В Python такие сравнения можно записать одной цепочкой, как в школьной математике:
+
+```python
+if 0 <= x < 100:
+```
-Одно из простых и часто используемых применений условий — определение четности числа:
+Поведение то же самое, читается естественнее. Особенно удобно с `elif` для диапазонов:
+
+```python
+if temperature < 0:
+ ...
+elif 0 <= temperature < 10:
+ ...
+elif 10 <= temperature < 20:
+ ...
+```
+
+Теперь каждая ветка самодокументирующаяся: видно, какой диапазон она ловит, не приходится держать в голове, что предыдущая ветка отсекла.
+
+### Оператор in для проверки членства
+
+Когда нужно проверить, что значение совпадает с одним из нескольких вариантов, можно написать через `or`:
+
+```python
+if day == "суббота" or day == "воскресенье":
+ ...
+```
+
+Но Pythonic-вариант с оператором `in` короче и читаемее:
+
+```python
+if day in ("суббота", "воскресенье"):
+ ...
+```
+
+Чем больше вариантов, тем заметнее выигрыш: добавить третий день — одна правка, а не ещё один `or`.
+
+## Практические примеры использования условий
+
+### Проверка чётности числа
+
+Одно из частых применений условий — определение чётности числа:
```python
number = 42
if number % 2 == 0:
- print(f"{number} — четное число")
+ print(f"{number} чётное")
else:
- print(f"{number} — нечетное число")
+ print(f"{number} нечётное")
+
```
### Проверка пароля
-Условия помогают реализовать проверку данных на соответствие определенным критериям:
+Условия помогают проверять данные на соответствие правилам. Здесь `.isalpha()` возвращает `True`, если строка состоит только из букв, а `.isdigit()` — если только из цифр:
```python
password = "password123"
if len(password) < 8:
- print("Пароль слишком короткий!")
+ print("Пароль слишком короткий.")
elif password.isalpha():
- print("Пароль должен содержать не только буквы!")
+ print("Пароль должен содержать не только буквы.")
elif password.isdigit():
- print("Пароль должен содержать не только цифры!")
+ print("Пароль должен содержать не только цифры.")
else:
- print("Пароль подходит!")
+ print("Пароль подходит.")
+
+```
+
+## Типичные ошибки новичков
+
+### Путаница = и ==
+
+В условии нужен `==` (сравнение), а не `=` (присваивание). Это две разные вещи:
+
+```python
+if x == 5: # правильно: проверяем, равен ли x пяти
+ ...
+
+if x = 5: # SyntaxError: в условии присваивать нельзя
+ ...
+```
+
+### Пропущенное двоеточие
+
+После условия в `if`, `elif`, `else` обязательно ставится `:`:
+
+```python
+if x > 5 # SyntaxError: пропущено двоеточие
+ print("больше пяти")
+```
+
+### Сбившийся отступ
+
+Тело условия должно иметь единообразный отступ. Менять количество пробелов внутри одного блока или смешивать табы и пробелы нельзя:
+
+```python
+if x > 5:
+ print("a")
+ print("b") # IndentationError: лишний отступ
```
## Проверка понимания
@@ -224,3 +355,11 @@ elif x == 5:
else:
print("D")
```
+
+1. A — Условие x > 10 не выполняется, так как x равно 5.
+
+2. B — Условие x > 5 не выполняется, так как x равно 5.
+
+3. **Правильный ответ:** C — Условие x == 5 истинно, так как x действительно равно 5.
+
+4. D — Блок else выполняется только если все предыдущие условия ложны, но условие x == 5 истинно.
diff --git a/course/module-1/lists/article.md b/course/module-1/lists/article.md
index ed39a73..e1d7f60 100644
--- a/course/module-1/lists/article.md
+++ b/course/module-1/lists/article.md
@@ -1,40 +1,64 @@
+---
+meta:
+ title: "Списки в Python"
+ description: "Подробное описание списков в Python: создание, методы для работы со списками, операции и практические примеры использования."
+---
+
# Списки в Python
-В этой статье мы подробно рассмотрим списки (list) в Python — один из самых гибких и часто используемых типов данных. Списки позволяют хранить коллекции элементов и манипулировать ими различными способами.
+Допустим, вы ведёте список покупок. Можно завести переменную под каждый товар: `item1`, `item2`, `item3`. Но это быстро превращается в кашу: вставить пункт в середину нельзя, посчитать количество товаров нельзя, пройтись по всем подряд тоже. Нужна одна переменная, которая хранит много значений по порядку. Это и есть список.
-## Что такое список?
+```python
+shopping = ["хлеб", "молоко", "яйца"]
+print(shopping)
+
+```
-Список в Python — это упорядоченная, изменяемая коллекция элементов, которые могут быть разных типов. Вы можете представить список как контейнер, в котором хранятся различные объекты, расположенные в определенном порядке.
+> Список (list) — это упорядоченная изменяемая коллекция элементов. Под одним именем лежит сразу несколько значений, у каждого есть своя позиция, и набор можно менять: добавлять, удалять, переставлять.
-Основные свойства списков:
+Из этого определения вытекают четыре свойства, на которые мы будем опираться дальше:
-- **Упорядоченность**: элементы хранятся в том порядке, в котором они были добавлены
-- **Изменяемость**: можно добавлять, удалять и изменять элементы после создания списка
-- **Индексируемость**: к каждому элементу можно обратиться по его позиции (индексу)
-- **Допускают дубликаты**: один и тот же элемент может встречаться в списке несколько раз
+- **Порядок сохраняется** — элементы лежат в том порядке, в каком вы их добавили
+- **Список изменяемый** — после создания в него можно добавлять и удалять элементы
+- **Доступ по индексу** — к любому элементу можно обратиться по его позиции
+- **Дубликаты разрешены** — одно и то же значение может встречаться несколько раз
## Создание списков
Существует несколько способов создания списков в Python:
-### С помощью квадратных скобок []
+### С помощью квадратных скобок \[]
```python
# Пустой список
empty_list = []
print(empty_list)
+
# Список чисел
numbers = [1, 2, 3, 4, 5]
print(numbers)
+
# Список разных типов данных
mixed = [1, "привет", True, 3.14]
print(mixed)
+
# Вложенные списки (список списков)
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
print(matrix)
+
```
### С помощью конструктора list()
@@ -43,29 +67,23 @@ print(matrix)
# Создание пустого списка
empty_list = list()
print(empty_list)
+
# Создание списка из строки (каждый символ становится элементом)
chars = list("Python")
print(chars)
+
# Создание списка из других итерируемых объектов
tuple_to_list = list((1, 2, 3))
print(tuple_to_list)
-
-set_to_list = list({1, 2, 3})
-print(set_to_list)
-```
-
-### С помощью генераторов списков
-
-```python
-# Создание списка квадратов чисел от 0 до 9
-squares = [x**2 for x in range(10)]
-print(squares)
-
-# Создание списка четных чисел от 0 до 9
-even_numbers = [x for x in range(10) if x % 2 == 0]
-print(even_numbers)
+
```
## Доступ к элементам списка
@@ -80,13 +98,22 @@ fruits = ["яблоко", "банан", "вишня", "груша", "апель
# Получение элементов по индексу
first_fruit = fruits[0]
print(f"Первый фрукт: {first_fruit}")
+
# Отрицательные индексы для доступа с конца списка
last_fruit = fruits[-1]
print(f"Последний фрукт: {last_fruit}")
+
second_last = fruits[-2]
print(f"Предпоследний фрукт: {second_last}")
+
```
### Срезы (slices)
@@ -102,23 +129,75 @@ fruits = ["яблоко", "банан", "вишня", "груша", "апель
# Первые три элемента
first_three = fruits[0:3]
print(f"Первые три фрукта: {first_three}")
+
# То же самое, но начальный индекс можно опустить, если он 0
first_three = fruits[:3]
print(f"Первые три фрукта: {first_three}")
+
# Каждый второй элемент
every_second = fruits[::2]
print(f"Каждый второй фрукт: {every_second}")
+
# Разворот списка
reversed_list = fruits[::-1]
print(f"Список в обратном порядке: {reversed_list}")
+
+```
+
+## Перебор списка
+
+Обращаться по индексу удобно, когда нужен один-два элемента. Но чтобы пройтись по всем, есть путь короче — цикл `for`, который вы уже знаете:
+
+```python
+fruits = ["яблоко", "банан", "вишня"]
+
+for fruit in fruits:
+ print(f"Фрукт: {fruit}")
+
+```
+
+Сколько в списке элементов, показывает функция `len()`:
+
+```python
+fruits = ["яблоко", "банан", "вишня"]
+print(f"Всего фруктов: {len(fruits)}")
+
+```
+
+Иногда вместе с элементом нужен и его индекс. Заводить для этого отдельную переменную-счётчик не нужно — есть `enumerate()`, который выдаёт пару «индекс и значение»:
+
+```python
+fruits = ["яблоко", "банан", "вишня"]
+
+for index, fruit in enumerate(fruits):
+ print(f"Индекс {index}: {fruit}")
+
```
## Изменение элементов списка
-В отличие от строк, списки являются изменяемыми, то есть вы можете изменять, добавлять и удалять элементы:
+Главное отличие списков от строк: **списки изменяемые** (mutable). Строку поменять «на месте» нельзя — приходится создавать новую (вспомните урок про строки). А вот элементы списка можно изменять, добавлять и удалять прямо в существующем объекте:
```python
fruits = ["яблоко", "банан", "вишня"]
@@ -126,16 +205,25 @@ fruits = ["яблоко", "банан", "вишня"]
# Изменение элемента
fruits[0] = "киви"
print(fruits)
+
# Изменение нескольких элементов с помощью среза
numbers = [1, 2, 3, 4, 5]
numbers[1:4] = [20, 30, 40]
print(numbers)
+
# Можно даже заменить несколько элементов на другое количество
numbers = [1, 2, 3, 4, 5]
numbers[1:4] = [20, 30]
print(numbers)
+
```
## Основные методы списков
@@ -150,19 +238,31 @@ fruits = ["яблоко", "банан"]
# Добавление элемента в конец списка
fruits.append("вишня")
print(fruits)
+
# Вставка элемента на определенную позицию
fruits.insert(1, "апельсин")
print(fruits)
+
# Добавление элементов из другого списка
more_fruits = ["груша", "виноград"]
fruits.extend(more_fruits)
print(fruits)
+
# Объединение списков с помощью оператора +
combined = fruits + ["ананас", "манго"]
print(combined)
+
```
### Удаление элементов
@@ -173,28 +273,52 @@ fruits = ["яблоко", "банан", "вишня", "апельсин", "ба
# Удаление элемента по значению (удаляет только первое вхождение)
fruits.remove("банан")
print(fruits)
+
# Удаление элемента по индексу и возврат его значения
removed = fruits.pop(1)
print(f"Удален: {removed}")
+
print(f"Список после удаления: {fruits}")
+
# Если индекс не указан, pop() удаляет и возвращает последний элемент
last = fruits.pop()
print(f"Последний элемент: {last}")
+
print(fruits)
+
# Удаление всех элементов из списка
fruits.clear()
print(f"Пустой список: {fruits}")
+
# Оператор del для удаления элементов по индексу или срезу
numbers = [1, 2, 3, 4, 5]
del numbers[0]
print(numbers)
+
del numbers[1:3]
print(numbers)
+
```
### Поиск и подсчет элементов
@@ -204,15 +328,27 @@ fruits = ["яблоко", "банан", "вишня", "банан", "груша"
# Проверка наличия элемента в списке
print("банан" in fruits)
+
print("арбуз" in fruits)
+
# Поиск индекса первого вхождения элемента
banana_index = fruits.index("банан")
print(f"Индекс первого банана: {banana_index}")
+
# Подсчет количества вхождений элемента
banana_count = fruits.count("банан")
print(f"Количество бананов: {banana_count}")
+
```
### Сортировка и разворот
@@ -222,21 +358,35 @@ print(f"Количество бананов: {banana_count}")
numbers = [3, 1, 4, 1, 5, 9, 2]
numbers.sort()
print(f"Отсортированный список: {numbers}")
+
# Сортировка в обратном порядке
numbers.sort(reverse=True)
print(f"Обратная сортировка: {numbers}")
+
# Если не хотите изменять исходный список, используйте sorted()
original = [3, 1, 4, 1, 5]
sorted_list = sorted(original)
print(f"Оригинал: {original}")
+
print(f"Отсортированная копия: {sorted_list}")
+
```
## Копирование списков
-При работе со списками важно понимать, как работает присваивание и копирование:
+Это одна из самых частых причин багов у новичков: присваивание списка **не создаёт копию**, обе переменные указывают на один и тот же объект в памяти. Если изменить список через одну переменную, изменение увидит и другая.
+
+
```python
# Создаем список
@@ -246,6 +396,9 @@ original = [1, 2, 3]
reference = original
reference.append(4)
print(f"Оригинал после изменения ссылки: {original}")
+
# Правильные способы копирования списка:
# 1. Метод copy()
@@ -260,9 +413,51 @@ copy3 = list(original)
# Проверим, что копии не связаны с оригиналом
copy1.append(5)
print(f"Оригинал: {original}")
+
print(f"Копия 1: {copy1}")
+
+```
+
+## Списковые включения
+
+Очень частая задача: взять один список и сделать из него другой. В лоб это несколько строк — завести пустой список, пройти циклом, добавлять по одному:
+
+```python
+squares = []
+for i in range(10):
+ squares.append(i**2)
+print(squares)
+
+```
+
+Настолько частая, что для неё придумали короткую запись — **списковое включение**. Тот же результат в одну строку:
+
+```python
+squares = [i**2 for i in range(10)]
+print(squares)
+
+```
+
+Читать её удобнее не слева направо: сначала `for i in range(10)` — откуда берём значения, потом `i**2` — что кладём в новый список. В конце можно добавить условие, и тогда попадут не все значения, а только подходящие:
+
+```python
+even_squares = [i**2 for i in range(10) if i % 2 == 0]
+print(even_squares)
+
```
+Соблазн запихнуть в одну строку всё подряд велик, но включение хорошо ровно до тех пор, пока читается с одного взгляда. Если внутрь просятся два условия и вложенный цикл — обычный `for` будет понятнее и вам, и тому, кто откроет код через полгода.
+
## Практический пример использования списка
```python
@@ -274,8 +469,19 @@ grades = [95, 82, 90, 78, 88]
highest_score = max(grades)
top_student_index = grades.index(highest_score)
print(f"Лучший студент: {students[top_student_index]} с оценкой {highest_score}")
+
```
## Проверка понимания
**Какой из следующих способов правильно создаст копию списка, не связанную с оригиналом?**
+
+1. copy_list = original_list — Это не создает копию, а лишь дает другое имя тому же списку. Изменения в copy_list будут отражаться на original_list.
+
+2. **Правильный ответ:** copy_list = original_list.copy() — Метод .copy() создает новую копию списка. Изменения в копии не повлияют на оригинал (однако будьте осторожны с вложенными объектами - это неглубокая копия).
+
+3. **Правильный ответ:** copy_list = original_list\[:] — Использование среза \[:] создает новую копию всего списка. Это один из правильных способов создания копии в Python.
+
+4. **Правильный ответ:** copy_list = list(original_list) — Функция list() создает новый список на основе существующего, что является правильным способом копирования.
diff --git a/course/module-1/loops/article.md b/course/module-1/loops/article.md
index ff78573..3a666db 100644
--- a/course/module-1/loops/article.md
+++ b/course/module-1/loops/article.md
@@ -1,39 +1,39 @@
-# Итерации и циклы
-
-Представьте, что вам нужно повторить какое-то действие много раз. Например, поприветствовать каждого гостя на вечеринке или пересчитать все элементы в списке. Делать это вручную было бы утомительно. И здесь на помощь приходят циклы! 🔄
+---
+meta:
+ title: "Итерации и циклы"
+ description: "Циклы в Python: for и while, перебор строк и диапазонов чисел, break и continue, вложенные циклы и практические примеры."
+---
-Циклы помогают автоматизировать повторяющиеся действия, позволяя вам выполнять один и тот же блок кода несколько раз. Это экономит время и делает ваш код более чистым и эффективным.
-
-## Что такое итерации и циклы?
+# Итерации и циклы
-> Итерация — это однократное выполнение набора инструкций. А цикл — это конструкция, которая позволяет выполнять блок кода многократно, пока выполняется определенное условие.
+Нужно вывести слово по буквам — каждую на своей строке. Без цикла это шесть отдельных `print`: для «P», для «y», для «t» и так далее. Слово подлиннее — больше строк вручную, а если оно станет известно только во время работы программы, заранее их и не напишешь.
-Python предлагает два основных типа циклов:
+Цикл избавляет от этой мороки: вы описываете шаг один раз, а Python сам повторяет его для каждой буквы. Одно такое повторение называют **итерацией**.
-- Цикл `for`: используется для итерации по последовательности (списку, кортежу, строке и т.д.)
-- Цикл `while`: выполняется, пока указанное условие остается истинным
+Циклов в Python два: `for` идёт по готовому набору значений, `while` крутится, пока выполняется условие. Начнём с первого.
## Цикл for
-Цикл `for` используется для перебора элементов последовательности (списка, кортежа, строки и т.д.). Это самый распространенный тип цикла в Python.
+Строка — это последовательность символов, и `for` умеет пройтись по ней символ за символом:
-### Синтаксис цикла for
-
-Базовый синтаксис цикла for выглядит так:
-
-```python-executable
-# Простой пример цикла for
-fruits = ["яблоко", "банан", "вишня"]
+```python
+message = "Python"
-for fruit in fruits:
- print(f"Я люблю {fruit}!")
+for char in message:
+ print(char)
+
```
-В этом примере:
+Читается почти как по-русски: «для каждого символа `char` в строке `message` — напечатай его». Python сам берёт значения по очереди, привязывает каждое к имени `char` и выполняет тело с отступом. Заканчиваются значения — заканчивается цикл, следить за этим не нужно.
-1. `fruit` — это переменная-итератор, которая принимает значение каждого элемента в списке `fruits` по очереди
-2. Тело цикла выполняется для каждого элемента в списке
-3. После `:` следует блок кода с отступом, который выполняется в каждой итерации
+Так же `for` пройдёт по любому набору значений, а не только по строке: списки, кортежи и словари ждут нас в следующих главах, и перебирать их вы будете этим же циклом.
### Итерация по диапазону чисел
@@ -43,9 +43,18 @@ for fruit in fruits:
# Использование range() для создания последовательности чисел
for i in range(5):
print(f"Число: {i}")
+
```
-Функция `range()` создает последовательность чисел и имеет несколько вариантов использования:
+Функция `range()` создает последовательность чисел и имеет несколько вариантов использования.
+
+В примерах ниже используется `print(..., end=" ")`: по умолчанию `print` ставит в конце перенос строки, а параметр `end=" "` заменяет его на пробел, чтобы значения печатались в одну строку.
```python
# Варианты использования range()
@@ -54,39 +63,24 @@ for i in range(5):
for i in range(3):
print(i, end=" ")
print()
+
# range(start, stop): от start до stop-1
for i in range(2, 5):
print(i, end=" ")
print()
+
# range(start, stop, step): от start до stop-1 с шагом step
for i in range(1, 10, 2): # Нечетные числа от 1 до 9
print(i, end=" ")
-```
-
-### Итерация по строкам
-
-Строки в Python также являются последовательностями, поэтому мы можем перебирать их символы:
-
-```python
-# Перебор символов в строке
-message = "Python"
-
-for char in message:
- print(char)
-```
-
-### Итерация с индексами
-
-Иногда нам нужны как элементы, так и их индексы. Для этого используется функция `enumerate()`:
-
-```python
-# Получение индексов и значений
-fruits = ["яблоко", "банан", "вишня"]
-
-for index, fruit in enumerate(fruits):
- print(f"Индекс {index}: {fruit}")
+
```
## Цикл while
@@ -101,15 +95,16 @@ count = 1
while count <= 5:
print(f"Цикл выполняется в {count}-й раз")
count += 1
+
```
-В этом примере:
-
-1. Цикл выполняется, пока `count <= 5`
-2. Внутри цикла мы увеличиваем `count` на 1 в каждой итерации
-3. Когда `count` становится равным 6, условие становится ложным, и цикл завершается
-
-> **Важно**: В цикле `while` всегда нужно изменять переменную, участвующую в условии, иначе цикл может стать бесконечным!
+Цикл выполняется, пока истинно условие `count <= 5`. Внутри обязательно нужно менять переменную из условия (здесь `count += 1`), иначе цикл будет бесконечным.
### Бесконечный цикл
@@ -126,16 +121,26 @@ while True: # Это условие всегда истинно
if counter >= 5: # Условие для выхода из цикла
print("Выходим из цикла!")
break # Прерывает выполнение цикла
+
```
## Управление циклами
-Python предоставляет несколько инструментов для управления выполнением циклов.
+Python предоставляет несколько инструментов для управления выполнением циклов. Два главных — `break` и `continue`.
### Оператор break
Оператор `break` позволяет немедленно выйти из цикла, независимо от условия:
+
+
```python
# Использование break для выхода из цикла
for i in range(10):
@@ -143,12 +148,18 @@ for i in range(10):
if i == 5:
print("\nДостигнуто число 5, выходим из цикла!")
break
+
```
### Оператор continue
Оператор `continue` пропускает текущую итерацию и переходит к следующей:
+
+
```python
# Использование continue для пропуска итераций
for i in range(10):
@@ -156,28 +167,28 @@ for i in range(10):
if i % 2 == 0:
continue
print(i, end=" ")
+
```
### Блок else в циклах
-Циклы `for` и `while` могут иметь блок `else`, который выполняется, когда цикл завершается нормально (не через `break`):
+У циклов `for` и `while` есть блок `else`, который выполняется при нормальном завершении цикла (не через `break`):
```python
-# Использование else с циклом
for i in range(5):
print(i, end=" ")
else:
- print("\nЦикл завершен нормально")
-
-# Пример с break - блок else не выполнится
-for i in range(5):
- print(i, end=" ")
- if i == 2:
- break
-else:
- print("\nЭта строка не будет выведена, так как цикл прерван")
+ print("\nЦикл завершился без break")
+
```
+Фича есть, но в реальном коде её используют редко.
+
## Вложенные циклы
Циклы можно вкладывать друг в друга для обработки многомерных структур данных или решения более сложных задач:
@@ -188,35 +199,15 @@ for i in range(1, 4): # Строки
for j in range(1, 4): # Столбцы
print(f"{i} × {j} = {i*j}", end="\t")
print() # Переход на новую строку после каждой строки таблицы
+
```
> **Совет**: Вложенные циклы могут быть ресурсоемкими, особенно при большом количестве итераций. Используйте их осторожно и ищите более эффективные альтернативы для обработки больших объемов данных.
-## Списковые включения (List Comprehensions)
-
-Списковые включения (или генераторы списков) — это элегантный способ создания списков в одну строку с использованием синтаксиса, похожего на цикл `for`:
-
-```python
-# Создание списка квадратов чисел от 0 до 9
-# Обычный способ
-squares = []
-for i in range(10):
- squares.append(i**2)
-print(f"Обычный способ: {squares}")
-
-# С использованием списковых включений
-squares_comprehension = [i**2 for i in range(10)]
-print(f"Через списковое включение: {squares_comprehension}")
-```
-
-Списковые включения могут также содержать условия:
-
-```python
-# Фильтрация с помощью условия: только четные числа
-even_squares = [i**2 for i in range(10) if i % 2 == 0]
-print(f"Квадраты четных чисел: {even_squares}")
-```
-
## Проверка понимания
Давайте проверим, насколько хорошо вы усвоили тему циклов:
@@ -230,3 +221,11 @@ for i in range(1, 5):
total += i
print(total)
```
+
+1. 0 — В цикле мы суммируем все четные числа от 1 до 4, а это 2 и 4.
+
+2. **Правильный ответ:** 6 — Мы суммируем четные числа от 1 до 4, а это 2 + 4 = 6.
+
+3. 10 — Число 10 было бы результатом суммирования всех чисел от 1 до 4 (1 + 2 + 3 + 4), но в данном коде суммируются только четные числа.
+
+4. 3 — Число 3 было бы результатом суммирования 1 и 2, но в данном коде суммируются только четные числа от 1 до 4, а именно 2 и 4.
diff --git a/course/module-1/numeric-data-types/article.md b/course/module-1/numeric-data-types/article.md
index 4ce3e15..929ac42 100644
--- a/course/module-1/numeric-data-types/article.md
+++ b/course/module-1/numeric-data-types/article.md
@@ -1,14 +1,21 @@
+---
+meta:
+ title: "Числовые типы данных в Python"
+ description: "Целые числа (int) и числа с плавающей точкой (float) в Python, арифметические операции, округление и тонкости работы с дробями."
+---
+
# Числовые типы данных в Python
Python предоставляет различные типы для работы с числами, что делает его мощным инструментом для математических вычислений. В этой статье мы подробно рассмотрим числовые типы данных и операции, которые можно с ними выполнять.
## Основные числовые типы
-В Python есть три основных числовых типа:
+В Python два основных числовых типа:
+
+- **Целые числа** (int) — числа без дробной части
+- **Числа с плавающей точкой** (float) — числа с дробной частью
-1. **Целые числа** (int) — числа без дробной части
-2. **Числа с плавающей точкой** (float) — числа с дробной частью
-3. **Комплексные числа** (complex) — числа с действительной и мнимой частью
+Есть ещё `complex` для мнимых чисел, в обычной разработке практически не встречается.
### Целые числа (int)
@@ -20,199 +27,169 @@ positive = 42
negative = -73
zero = 0
-# Большие числа (в Python нет ограничения на размер целых чисел)
big_number = 1234567890123456789012345678901234567890
-# Двоичные, восьмеричные и шестнадцатеричные числа
-binary = 0b1010 # 10 в десятичной системе
-octal = 0o756 # 494 в десятичной системе
-hexadecimal = 0xFF # 255 в десятичной системе
-
-# Использование разделителя для удобства чтения больших чисел
-million = 1_000_000 # Это то же самое, что и 1000000
+# Подчёркивание-разделитель для удобства чтения больших чисел
+million = 1_000_000 # То же самое, что 1000000
```
-В отличие от многих других языков программирования, в Python целые числа имеют неограниченную точность, то есть они могут быть сколь угодно большими (ограничены только доступной памятью компьютера).
+В отличие от многих других языков, в Python целые числа имеют неограниченную точность: они могут быть сколь угодно большими, ограничены только доступной памятью.
### Числа с плавающей точкой (float)
Числа с плавающей точкой (тип `float`) представляют действительные числа с дробной частью:
```python
-# Числа с десятичной точкой
price = 19.99
pi_approx = 3.14159
negative_float = -0.5
-
-# Экспоненциальная запись (научная нотация)
-avogadro = 6.022e23 # 6.022 * 10^23
-tiny = 1.6e-35 # 1.6 * 10^(-35)
```
-Числа с плавающей точкой имеют ограниченную точность и могут представлять лишь приближенные значения некоторых десятичных дробей. Это может приводить к небольшим погрешностям в вычислениях:
+Главное, что нужно знать про float: их точность ограничена, и в простых на вид вычислениях вылезают погрешности:
```python
-# Пример ограниченной точности float
result = 0.1 + 0.2
-print(result) # Выведет 0.30000000000000004, а не точно 0.3
+print(result)
+
```
-Для финансовых вычислений, где требуется точность, рекомендуется использовать модуль `decimal`.
-
-### Комплексные числа (complex)
+Это не баг Python, а особенность хранения дробных чисел в двоичном представлении: некоторые десятичные дроби в нём не представимы точно. Для финансовых вычислений и других задач, где точность критична, используют модуль `decimal`.
-Комплексные числа (тип `complex`) содержат действительную и мнимую части, где мнимая часть обозначается суффиксом `j` или `J`:
+По той же причине **не сравнивайте float через `==` напрямую** — две казалось бы равные дроби могут отличаться в последнем знаке:
```python
-# Создание комплексных чисел
-z1 = 3 + 4j
-z2 = complex(2, -3) # 2 - 3j
-
-# Получение действительной и мнимой частей
-real_part = z1.real # 3.0
-imag_part = z1.imag # 4.0
+print(0.1 + 0.2 == 0.3)
+
```
-Комплексные числа часто используются в инженерных расчетах, обработке сигналов и математических алгоритмах.
+Если нужно сравнение с допуском, используйте `math.isclose(a, b)`:
-## Арифметические операции
+```python
+import math
+print(math.isclose(0.1 + 0.2, 0.3))
+
+```
-Python поддерживает все стандартные арифметические операции для числовых типов:
+## Арифметические операции
-### Основные операции
+Python поддерживает все стандартные арифметические операции:
```python
a = 10
b = 3
-# Сложение
-sum_result = a + b # 13
-
-# Вычитание
-diff_result = a - b # 7
-
-# Умножение
-mult_result = a * b # 30
+# Сложение, вычитание, умножение
+a + b # 13
+a - b # 7
+a * b # 30
-# Деление (всегда возвращает float)
-div_result = a / b # 3.3333333333333335
+# Деление — всегда возвращает float
+a / b # 3.3333333333333335
-# Целочисленное деление (возвращает целую часть результата деления)
-floor_div = a // b # 3
+# Целочисленное деление — возвращает int (отбрасывает дробь)
+a // b # 3
# Остаток от деления
-remainder = a % b # 1
+a % b # 1
# Возведение в степень
-power = a ** b # 1000
+a ** b # 1000
```
-### Смешанные операции
+### Смешанные типы
-При выполнении операций между различными числовыми типами Python автоматически преобразует типы по следующему правилу:
-`int` → `float` → `complex`
+Если в выражении есть и `int`, и `float`, результат всегда будет `float`:
```python
-# int + float = float
-mixed1 = 5 + 3.14 # 8.14 (float)
-
-# float + complex = complex
-mixed2 = 2.5 + 3j # (2.5+3j) (complex)
+5 + 3.14 # 8.14 (float)
+10 * 2.0 # 20.0 (float)
+4 ** 0.5 # 2.0 (float)
```
-## Округление и управление точностью
+## Округление
-Python предоставляет несколько функций для округления чисел:
+Python даёт несколько способов округлить число:
```python
# Округление до ближайшего целого
-rounded = round(3.14159) # 3
+round(3.14159) # 3
# Округление до указанного количества знаков после запятой
-rounded_pi = round(3.14159, 2) # 3.14
+round(3.14159, 2) # 3.14
-# Округление вниз (к меньшему целому)
import math
-floor_value = math.floor(3.99) # 3
+
+# Округление вниз (к меньшему целому)
+math.floor(3.99) # 3
# Округление вверх (к большему целому)
-ceil_value = math.ceil(3.01) # 4
+math.ceil(3.01) # 4
# Отбрасывание дробной части
-trunc_value = math.trunc(3.99) # 3
+math.trunc(3.99) # 3
```
-## Математические функции
+### Тонкость: round и округление к чётному
-Модуль `math` предоставляет множество полезных математических функций:
+При попадании значения ровно посередине (например, `0.5` или `2.5`) Python округляет не к большему, а к ближайшему **чётному**. Это называется _банковским округлением_ (banker's rounding) и сделано для уменьшения статистической ошибки при больших объёмах округлений:
```python
-import math
-
-# Константы
-pi = math.pi # 3.141592653589793
-e = math.e # 2.718281828459045
-
-# Тригонометрические функции (аргументы в радианах)
-sin_value = math.sin(math.pi / 2) # 1.0
-cos_value = math.cos(math.pi) # -1.0
-tan_value = math.tan(math.pi / 4) # 1.0
-
-# Обратные тригонометрические функции
-asin_value = math.asin(1.0) # 1.5707963267948966 (π/2)
-
-# Логарифмы
-log_value = math.log(10) # натуральный логарифм, ln(10) = 2.302585092994046
-log10_value = math.log10(100) # логарифм по основанию 10, log10(100) = 2.0
-
-# Корни
-sqrt_value = math.sqrt(16) # квадратный корень, 4.0
+print(round(0.5)) # ожидаем 1
+
+print(round(1.5))
+
+print(round(2.5)) # ожидаем 3
+
+print(round(3.5))
+
```
-Для работы с комплексными числами используется модуль `cmath`, который содержит аналогичные функции, но работающие с комплексными аргументами.
+Если нужно «школьное» округление (0.5 всегда вверх), пишите свою функцию или используйте `decimal` с явно заданным режимом.
-## Случайные числа
+## Математические функции
-Для генерации случайных чисел в Python используется модуль `random`:
+Модуль `math` даёт полезные функции — `sqrt` (квадратный корень), константы `pi` и `e`:
```python
-import random
-
-# Случайное число от 0 до 1
-random_float = random.random() # например, 0.7593696548501615
-
-# Случайное целое число в диапазоне
-random_int = random.randint(1, 10) # случайное целое от 1 до 10 включительно
-
-# Случайное число с плавающей точкой в диапазоне
-random_range = random.uniform(1.5, 3.5) # например, 2.4378123553729193
+import math
-# Случайный выбор из последовательности
-random_choice = random.choice([1, 3, 5, 7, 9]) # случайный элемент из списка
+math.sqrt(16) # 4.0
+math.pi # 3.141592653589793
+math.e # 2.718281828459045
```
-## Числовые методы и функции
+Там же лежат тригонометрия, логарифмы, факториал и много чего ещё — полный список в [документации модуля math](https://docs.python.org/3/library/math.html).
-Python предоставляет несколько встроенных функций для работы с числами:
+## Встроенные функции для чисел
+
+Часть числовых функций встроена прямо в Python, без `import`:
```python
# Абсолютное значение
-abs_value = abs(-10) # 10
+abs(-10) # 10
-# Максимальное и минимальное значение
-max_value = max(1, 5, 3, 9, 2) # 9
-min_value = min(1, 5, 3, 9, 2) # 1
+# Максимум и минимум
+max(1, 5, 3, 9, 2) # 9
+min(1, 5, 3, 9, 2) # 1
# Сумма последовательности
-sum_value = sum([1, 2, 3, 4, 5]) # 15
+sum([1, 2, 3, 4, 5]) # 15
# Преобразование других типов в числовые
-int_from_str = int("42") # 42
-float_from_str = float("3.14") # 3.14
+int("42") # 42
+float("3.14") # 3.14
```
-
-## Проверка понимания
-
-Проверим ваше понимание числовых типов данных:
-
-**Что будет результатом выражения `10 / 3` в Python?**
diff --git a/course/module-1/sets/article.md b/course/module-1/sets/article.md
index 6fc733b..a3e86c9 100644
--- a/course/module-1/sets/article.md
+++ b/course/module-1/sets/article.md
@@ -1,21 +1,27 @@
+---
+meta:
+ title: "Множества в Python"
+ description: "Подробное описание множеств в Python: создание, методы для работы с множествами, операции над множествами и практические примеры использования."
+---
+
# Множества в Python
-В этой статье мы рассмотрим множества (set) в Python — коллекции уникальных элементов с особыми свойствами, которые делают их незаменимыми для определенных задач.
+Множество (`set`) в Python решает две конкретные задачи: быстрая проверка членства (есть ли элемент в коллекции) и хранение **только уникальных** значений без дубликатов. Это неупорядоченная коллекция, основанная на математической концепции множества.
## Что такое множество?
Множество в Python — это неупорядоченная коллекция уникальных элементов. Два ключевых свойства множеств:
-1. **Неупорядоченность**: элементы не имеют определенного порядка и не индексируются
+1. **Неупорядоченность**: элементы не имеют определённого порядка и не индексируются — обратиться к «третьему элементу множества» нельзя, можно только перебрать все элементы или проверить, есть ли в нём конкретный
2. **Уникальность**: каждый элемент встречается только один раз
Основные характеристики множеств:
-- **Изменяемость**: можно добавлять и удалять элементы
-- **Хеширование**: элементы должны быть хешируемыми (неизменяемыми) объектами
-- **Эффективность**: оптимизированы для быстрой проверки вхождения элементов
+- **Изменяемость**: можно добавлять и удалять элементы
+- **Неизменяемые элементы**: внутрь множества можно положить только неизменяемые объекты (числа, строки, кортежи)
+- **Эффективность**: оптимизированы для быстрой проверки вхождения элементов
-Множества основаны на математической концепции множеств, что делает их идеальными для операций объединения, пересечения и разности.
+Поскольку множества основаны на математической концепции, у них есть операции объединения, пересечения и разности.
## Создание множеств
@@ -25,14 +31,23 @@
# Множество целых чисел
numbers = {1, 2, 3, 4, 5}
print(numbers)
+
# Автоматическое удаление дубликатов
duplicates = {1, 2, 2, 3, 3, 3, 4, 5, 5}
print(duplicates)
+
-# Множество с разными типами данных
+# В одном множестве могут лежать разные неизменяемые типы
mixed = {1, "привет", (1, 2, 3)}
-print(mixed)
+print(len(mixed)) # число, строка и кортеж — все три поместились
+
```
> **Важно**: Нельзя создать пустое множество с помощью `{}`, так как это будет пустой словарь. Для создания пустого множества используйте `set()`.
@@ -43,22 +58,23 @@ print(mixed)
# Пустое множество
empty_set = set()
print(empty_set)
+
# Создание множества из списка
numbers_set = set([1, 2, 2, 3, 4, 4, 5])
print(numbers_set)
+
-# Создание множества из строки
+# Создание множества из строки — повторяющиеся буквы схлопываются
letters = set("hello")
-print(letters) # 'l' встречается только один раз
-```
-
-### С помощью генераторов множеств
-
-```python
-# Множество квадратов чисел от 0 до 9
-squares = {x**2 for x in range(10)}
-print(squares)
+print(len(letters)) # в "hello" две 'l', в множестве осталась одна — всего 4 буквы
+
```
## Основные операции с множествами
@@ -69,98 +85,138 @@ print(squares)
fruits = {"яблоко", "банан", "вишня"}
print("яблоко" in fruits)
+
print("груша" in fruits)
+
```
### Добавление и удаление элементов
+Порядок элементов в множестве произвольный, поэтому в примерах со строками ниже мы выводим их через `sorted()`: эта функция возвращает отсортированный список, и вывод не скачет от запуска к запуску.
+
```python
fruits = {"яблоко", "банан"}
# Добавление одного элемента
fruits.add("вишня")
-print(fruits)
+print(sorted(fruits))
+
# Добавление нескольких элементов
fruits.update(["груша", "апельсин"])
-print(fruits)
+print(sorted(fruits))
+
# Удаление элемента
fruits.remove("банан") # вызывает KeyError, если элемента нет
-print(fruits)
+print(sorted(fruits))
+
# Безопасное удаление элемента
fruits.discard("вишня") # не вызывает ошибку, если элемента нет
-print(fruits)
-
-# Извлечение произвольного элемента
-random_fruit = fruits.pop()
-print(random_fruit)
-print(fruits)
+print(sorted(fruits))
+
+
+# pop() убирает и возвращает какой-то элемент — какой именно, заранее неизвестно
+removed = fruits.pop()
+print(len(fruits)) # осталось на один меньше
+
# Очистка множества
fruits.clear()
print(fruits)
+
```
-### Итерация по множеству
+### Перебор множества
+
+Множество перебирают циклом `for`. Порядок при этом произвольный и может меняться от запуска к запуску — это и значит «неупорядоченное». Когда нужен предсказуемый порядок, сортируем через `sorted()`:
```python
colors = {"красный", "синий", "зеленый"}
-for color in colors:
+for color in sorted(colors):
print(color)
-
-# Порядок элементов не гарантирован!
+
```
## Математические операции над множествами
+Три основные операции — объединение, пересечение и разность. Удобно визуализировать их диаграммами Венна:
+
+
+
### Объединение (Union)
+Все элементы из обоих множеств:
+
```python
a = {1, 2, 3}
b = {3, 4, 5}
-# С оператором |
union_set = a | b
print(union_set)
-
-# С методом union()
-union_set = a.union(b)
-print(union_set)
+
```
+То же самое можно написать как `a.union(b)`.
+
### Пересечение (Intersection)
+Элементы, которые есть в обоих множествах:
+
```python
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
-# С оператором &
intersection_set = a & b
print(intersection_set)
-
-# С методом intersection()
-intersection_set = a.intersection(b)
-print(intersection_set)
+
```
+То же самое можно написать как `a.intersection(b)`.
+
### Разность (Difference)
+Элементы первого множества, которых нет во втором:
+
```python
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
-# С оператором -
difference_set = a - b
print(difference_set)
-
-# С методом difference()
-difference_set = a.difference(b)
-print(difference_set)
+
```
+То же самое можно написать как `a.difference(b)`.
+
## Операции сравнения множеств
```python
@@ -170,18 +226,36 @@ c = {1, 2, 3}
# Равенство множеств
print(a == c) # Содержат одинаковые элементы
+
# Подмножества
print(a.issubset(b)) # Все элементы a есть в b
+
print(a < b) # a является строгим подмножеством b
+
# Надмножества
print(b.issuperset(a)) # b содержит все элементы a
+
print(b > a) # b является строгим надмножеством a
+
# Проверка на отсутствие общих элементов
d = {6, 7, 8}
print(a.isdisjoint(d)) # Нет общих элементов
+
```
## Неизменяемые множества (frozenset)
@@ -192,16 +266,25 @@ print(a.isdisjoint(d)) # Нет общих элементов
# Создание frozenset
immutable_set = frozenset([1, 2, 3, 4])
print(immutable_set)
+
# Попытка изменить frozenset вызывает ошибку
try:
immutable_set.add(5)
except AttributeError as e:
print(f"Ошибка: {e}")
+
# frozenset можно использовать как ключ словаря или элемент другого множества
normal_set = {frozenset([1, 2]), frozenset([3, 4])}
-print(normal_set)
+print(len(normal_set)) # два frozenset'а поместились внутрь
+
```
## Практические примеры использования множеств
@@ -212,6 +295,9 @@ print(normal_set)
numbers = [1, 2, 2, 3, 3, 3, 4, 5, 5]
unique_numbers = list(set(numbers))
print(unique_numbers)
+
```
### 2. Нахождение общих элементов
@@ -222,15 +308,24 @@ users_group2 = ["Иван", "Ольга", "Елена", "Алексей"]
# Общие элементы (пересечение)
common_users = set(users_group1) & set(users_group2)
-print(f"Пользователи в обеих группах: {common_users}")
+print(f"Пользователи в обеих группах: {sorted(common_users)}")
+
# Элементы только из первой группы (разность)
only_group1 = set(users_group1) - set(users_group2)
-print(f"Только в группе 1: {only_group1}")
+print(f"Только в группе 1: {sorted(only_group1)}")
+
# Все уникальные элементы (объединение)
all_users = set(users_group1) | set(users_group2)
-print(f"Все уникальные пользователи: {all_users}")
+print(f"Все уникальные пользователи: {sorted(all_users)}")
+
```
### 3. Проверка уникальности элементов
@@ -241,7 +336,13 @@ def are_all_unique(items):
return len(set(items)) == len(items)
print(are_all_unique([1, 2, 3, 4, 5]))
+
print(are_all_unique([1, 2, 3, 3, 4]))
+
```
## Ограничения и производительность
@@ -253,55 +354,78 @@ print(are_all_unique([1, 2, 3, 3, 4]))
```python
# Работает с неизменяемыми типами данных
valid_set = {1, "hello", (1, 2, 3)}
-print(valid_set)
+print(len(valid_set)) # число, строка и кортеж хешируемы — все три поместились
+
# Ошибка с изменяемыми типами данных
try:
invalid_set = {1, [2, 3], {"a": 1}}
except TypeError as e:
print(f"Ошибка: {e}")
+
```
Можно добавлять:
-- Числа (int, float, complex)
-- Строки (str)
-- Кортежи (tuple) с хешируемыми элементами
-- Frozenset
+- Числа (int, float, complex)
+- Строки (str)
+- Кортежи (tuple) с хешируемыми элементами
+- Frozenset
Нельзя добавлять:
-- Списки (list)
-- Словари (dict)
-- Множества (set)
+- Списки (list)
+- Словари (dict)
+- Множества (set)
### Производительность
-Множества оптимизированы для быстрых операций:
+Ради быстрого поиска множества и придуманы. Проверим на миллионе чисел: ищем последнее — то есть худший случай для списка, который вынужден просмотреть всё подряд.
```python
import time
-# Сравнение скорости поиска (демонстрация)
-data = list(range(10000))
+data = list(range(1_000_000))
data_set = set(data)
-# Поиск в списке vs. поиск в множестве
start = time.time()
-9999 in data # Медленно: O(n)
+for _ in range(100):
+ 999_999 in data
list_time = time.time() - start
start = time.time()
-9999 in data_set # Быстро: O(1)
+for _ in range(100):
+ 999_999 in data_set
set_time = time.time() - start
-print(f"Поиск в списке: {list_time:.6f} сек")
-print(f"Поиск в множестве: {set_time:.6f} сек")
-print(f"Множество быстрее в {list_time/set_time:.1f} раз")
+print(f"Поиск в списке: {list_time:.3f} сек")
+
+print(f"Поиск в множестве: {set_time:.5f} сек")
+
```
+Конкретные числа у вас получатся свои — они зависят от машины и загруженности, — но разрыв никуда не денется: десятки тысяч раз. Списку приходится перебирать элементы по одному, пока не найдёт нужный. Множество вместо перебора сразу вычисляет, где значение должно лежать, и проверяет только это место — и делает это одинаково быстро хоть на десяти элементах, хоть на миллионе.
+
Операции со сложностью O(1) (константное время):
-- Проверка наличия элемента: `x in set`
-- Добавление элемента: `set.add(x)`
-- Удаление элемента: `set.remove(x)`, `set.discard(x)`
+- Проверка наличия элемента: `x in set`
+- Добавление элемента: `set.add(x)`
+- Удаление элемента: `set.remove(x)`, `set.discard(x)`
+
+## Проверка понимания
+
+**Что выведет `print(set([1, 2, 2, 3, 3, 3]))`?**
+
+1. \{1, 2, 2, 3, 3, 3} — Множество не хранит дубликаты. Повторяющиеся значения схлопываются, и каждое число остаётся в одном экземпляре.
+
+2. **Правильный ответ:** \{1, 2, 3} — Множество хранит только уникальные значения, поэтому повторы 2 и 3 отбрасываются. Именно поэтому set() — частый способ убрать дубликаты из списка.
+
+3. \[1, 2, 3] — Уникальность определена верно, но тип не тот: set() возвращает множество в фигурных скобках, а не список. Чтобы получить список, результат оборачивают в list().
diff --git a/course/module-1/string-data-types/article.md b/course/module-1/string-data-types/article.md
index 0c29600..72bae3b 100644
--- a/course/module-1/string-data-types/article.md
+++ b/course/module-1/string-data-types/article.md
@@ -1,39 +1,53 @@
-# Строковые данные в Python
-
-В этой статье мы познакомимся со строками в Python – одним из самых важных и часто
-используемых типов данных.
+---
+meta:
+ title: "Строковые данные в Python"
+ description: "Подробное описание строковых данных в Python: создание строк, операции и методы для работы со строками, форматирование строк."
+---
-## Что такое строки в Python?
+# Строковые данные в Python
-Строки (тип `str`) — это последовательности символов, которые помогают нам хранить и обрабатывать текстовую информацию.
+Почти всё, что программа показывает человеку и получает от него обратно, — это текст: имя в форме регистрации, сообщение об ошибке, строка из файла. В Python текст хранится в строках — значениях типа `str`.
-Они нужны буквально везде: для вывода сообщений пользователю, хранения имен, адресов, работы с файлами и многого другого!
+Строка — это просто последовательность символов, и с ней можно многое: склеивать, резать на части, искать внутри, подставлять значения переменных. С создания и начнём.
## Как создавать строки в Python
-Python предлагает несколько удобных способов создания строк. Вы можете использовать одинарные, двойные или тройные кавычки:
+Python предлагает несколько способов создания строк. Можно использовать одинарные, двойные или тройные кавычки:
-```python-executable
+```python
# Строки в одинарных кавычках
single_quotes = 'Привет, мир!'
print(single_quotes)
+
# Строки в двойных кавычках
-double_quotes = "Python - это здорово!"
+double_quotes = "Hello, world"
print(double_quotes)
+
# Многострочные строки в тройных кавычках
-multi_line = """Это многострочная строка.
-Она может занимать несколько строк
-и сохраняет все переносы строк."""
+multi_line = """Рецепт кофе:
+1. Вскипятить воду
+2. Залить молотый кофе
+3. Подождать 4 минуты"""
print(multi_line)
+
```
Выбор типа кавычек зависит от ситуации:
-- Одинарные (`'`) и двойные (`"`) кавычки работают одинаково
-- Если в строке уже есть одинарные кавычки, удобнее обернуть её в двойные, и наоборот
-- Тройные кавычки (`"""` или `'''`) отлично подходят для создания многострочного текста
+- Одинарные (`'`) и двойные (`"`) кавычки работают одинаково
+- Если в строке уже есть одинарные кавычки, удобнее обернуть её в двойные, и наоборот
+- Тройные кавычки (`"""` или `'''`) подходят для многострочного текста
### Экранирование символов
@@ -43,46 +57,84 @@ print(multi_line)
# Использование кавычек внутри строк с экранированием
quote_inside = "Он сказал: \"Привет!\""
print(quote_inside)
+
path = "C:\\Program Files\\Python"
print(path)
+
# Часто используемые последовательности экранирования
newline = "Первая строка.\nВторая строка." # \n - перенос строки
print(newline)
+
tab = "Имя:\tИван" # \t - табуляция
print(tab)
+
```
-Вот полезные последовательности экранирования:
+Полезные последовательности экранирования:
-- `\n` — создает новую строку
-- `\t` — добавляет табуляцию (отступ)
-- `\\` — позволяет вставить обратную косую черту
-- `\'` — добавляет одинарную кавычку
-- `\"` — добавляет двойную кавычку
+- `\n` для переноса строки
+- `\t` для табуляции
+- `\\` для обратной косой черты
+- `\'` для одинарной кавычки
+- `\"` для двойной кавычки
-### Raw-строки для путей и регулярных выражений
+### Raw-строки
-Если вы работаете с путями к файлам или регулярными выражениями, постоянное экранирование обратных косых черт может быть утомительным. Python предлагает элегантное решение – "сырые" (raw) строки с префиксом `r`:
+Когда обратных косых черт много — например, в пути к файлу на Windows — экранировать каждую утомительно. Префикс `r` перед строкой отключает экранирование: все символы берутся буквально.
```python
-# Обычная строка требует двойного обратного слеша
-normal_path = "C:\\Users\\Username\\Documents"
-print(normal_path)
-
-# Raw-строка понимает обратные слеши буквально
-raw_path = r"C:\Users\Username\Documents"
+# Обычную строку пришлось бы писать с двойными обратными слешами
+raw_path = r"C:\Users\Documents\file.txt"
print(raw_path)
+
+```
+
+## Неизменяемость строк
+
+Строки в Python являются **неизменяемыми** (immutable). Это значит, что после создания строки её нельзя поменять «на месте»: любая операция, которая выглядит как изменение, на самом деле создаёт новую строку.
+
+Попытка изменить символ по индексу падает с ошибкой:
+```python
+language = "Python"
+
+try:
+ language[0] = "J"
+except TypeError as e:
+ print(f"Ошибка: {e}")
+
```
-Raw-строки значительно упрощают работу с путями в Windows и регулярными выражениями!
+Чтобы получить «изменённую» строку, нужно собрать новую и присвоить её — либо в ту же переменную, либо в другую:
-## Основные операции со строками
+```python
+language = "Python"
+language = "J" + "ython"
+print(language)
+
+```
+
+Это правило касается всех операций над строками: `+`, `.upper()`, `.replace()` и другие возвращают новую строку, а не меняют исходную.
-Теперь, когда мы умеем создавать строки, давайте научимся с ними работать. Python предлагает множество удобных операций для манипуляции строками.
+## Основные операции со строками
### Сложение строк (конкатенация)
@@ -93,9 +145,15 @@ first_name = "Иван"
last_name = "Петров"
full_name = first_name + " " + last_name
print(full_name)
+
greeting = "Привет, " + full_name + "!"
print(greeting)
+
```
### Повторение строк
@@ -103,26 +161,36 @@ print(greeting)
С помощью оператора `*` можно повторить строку несколько раз:
```python
-star_line = "*" * 10
-print(star_line)
-
border = "=" * 20
print(border)
+
+```
-pattern = "+-" * 5
-print(pattern)
+### Длина строки
-# Можно создавать красивые заголовки
-title = "МЕНЮ"
-decorated_title = f"{border}\n{title.center(20)}\n{border}"
-print(decorated_title)
-```
+Количество символов в строке узнаём через встроенную функцию `len()`:
-Это удобно для создания разделителей или визуального оформления текста!
+```python
+word = "Python"
+print(len(word))
+
+
+empty = ""
+print(len(empty))
+
+```
### Доступ к отдельным символам
-Строки в Python – это последовательности символов, и мы можем обращаться к каждому символу по его позиции (индексу). Индексация начинается с 0:
+В строке к каждому символу можно обратиться по его позиции (индексу). Индексация начинается с 0, а отрицательные индексы отсчитываются с конца:
+
+![Индексы строки Python: положительные 0–5 над буквами, отрицательные -6…-1 под ними. Срез word[1:4] выделяет y, t, h](https://python-academy.org/static/guidePage/string-data-types/indexing-ru.webp "Индексы и срезы строки")
```python
word = "Python"
@@ -133,17 +201,26 @@ last_letter = word[5]
print(f"Первая буква: {first_letter}")
print(f"Вторая буква: {second_letter}")
print(f"Последняя буква: {last_letter}")
+
# Можно также использовать отрицательные индексы для отсчета с конца
last_letter = word[-1]
second_last_letter = word[-2]
print(f"Последняя буква (с конца): {last_letter}")
print(f"Предпоследняя буква: {second_last_letter}")
+
```
### Получение части строки (срезы)
-Одна из самых мощных возможностей Python – это срезы (slices). Они позволяют извлекать подстроки, указывая начальный и конечный индексы:
+В Python есть мощный приём для работы со строками: срезы (slices). Они позволяют извлекать подстроки, указывая начальный и конечный индексы:
```python
message = "Python Programming"
@@ -153,124 +230,155 @@ message = "Python Programming"
first_word = message[0:6] # первые 6 символов
print(f"Первое слово: {first_word}")
+
second_word = message[7:] # от 7-го до конца
print(f"Второе слово: {second_word}")
+
prefix = message[:6] # от начала до 6-го (не включая 6-й)
print(f"Префикс: {prefix}")
+
every_second = message[::2] # каждый второй символ
print(f"Каждая вторая буква: {every_second}")
+
reversed_string = message[::-1] # строка задом наперед
print(f"В обратном порядке: {reversed_string}")
+
```
-Срезы очень гибкие:
-
-- Если не указать начальный индекс, срез начнется с начала строки
-- Если не указать конечный индекс, срез продолжится до конца строки
-- Отрицательный шаг позволяет обходить строку в обратном порядке
-
-## Неизменяемость строк: важная особенность!
+Срезы гибкие:
-Важно понимать, что строки в Python являются неизменяемыми (immutable).
-Это значит, что после создания строки её нельзя изменить – нужно создать новую строку:
-
-```python
-language = "Python"
-print(f"Исходная строка: {language}")
-
-# Попытка изменения строки
-try:
- language[0] = "J" # Пытаемся изменить первую букву
-except TypeError as e:
- print(f"Ошибка: {e}")
-
-# Правильный способ: создать новую строку
-new_language = "J" + language[1:]
-print(f"Новая строка: {new_language}")
-```
-
-Эта особенность может показаться ограничением, но на самом деле она делает код более безопасным и предсказуемым!
+- Если не указать начальный индекс, срез начнется с начала строки
+- Если не указать конечный индекс, срез продолжится до конца строки
+- Отрицательный шаг позволяет обходить строку в обратном порядке
## Полезные методы для работы со строками
-Python предоставляет множество встроенных методов, которые делают работу со строками простой и удобной. Рассмотрим самые полезные из них:
+Python предоставляет множество встроенных методов для работы со строками. Рассмотрим самые полезные.
### Изменение регистра текста
```python
-text = "Python Programming"
+text = "Hello PYTHON world"
upper_case = text.upper()
print(f"Верхний регистр: {upper_case}")
+
lower_case = text.lower()
print(f"Нижний регистр: {lower_case}")
+
title_case = text.title()
-print(f"Заглавные первые буквы: {title_case}")
+print(f"Каждое слово с заглавной: {title_case}")
+
-capitalized = "hello world".capitalize()
+capitalized = text.capitalize()
print(f"Только первая буква заглавная: {capitalized}")
+
```
-Эти методы часто используются для стандартизации ввода пользователя или для форматирования текста.
-
### Поиск и замена в строках
```python
-text = "Python - это отличный язык программирования"
+text = "Python это отличный язык программирования"
# Поиск подстроки
position = text.find("отличный")
print(f"Слово 'отличный' начинается с позиции: {position}")
+
count = text.count("о")
print(f"Буква 'о' встречается {count} раза")
+
# Проверка начала и конца строки
starts_with = text.startswith("Python")
print(f"Строка начинается с 'Python': {starts_with}")
+
ends_with = text.endswith("!")
print(f"Строка заканчивается на '!': {ends_with}")
+
# Проверка наличия подстроки
contains = "отличный" in text
print(f"Строка содержит слово 'отличный': {contains}")
+
# Замена подстрок
new_text = text.replace("отличный", "прекрасный")
print(f"Текст после замены: {new_text}")
+
```
### Разделение и объединение строк
-Эти методы очень полезны при работе с данными:
-
```python
# Разделение строки на список слов
-sentence = "Python - это отличный язык программирования"
+sentence = "Python это отличный язык программирования"
words = sentence.split()
print(f"Список слов: {words}")
+
# Разделение по конкретному разделителю
csv_data = "яблоко,банан,вишня"
fruits = csv_data.split(",")
print(f"Список фруктов: {fruits}")
+
# Объединение списка в строку
words_to_join = ["Python", "это", "здорово"]
joined_sentence = " ".join(words_to_join)
print(f"Объединенное предложение: {joined_sentence}")
+
# Объединение с другим разделителем
path_parts = ["C:", "Users", "Username", "Documents"]
path = "\\".join(path_parts)
print(f"Путь к файлу: {path}")
+
```
### Удаление лишних символов
@@ -283,80 +391,84 @@ text_with_spaces = " Python "
# Удаление пробелов с обоих концов
cleaned = text_with_spaces.strip()
print(f"Без пробелов: '{cleaned}'")
+
left_cleaned = text_with_spaces.lstrip()
print(f"Без пробелов слева: '{left_cleaned}'")
+
right_cleaned = text_with_spaces.rstrip()
print(f"Без пробелов справа: '{right_cleaned}'")
+
# Удаление конкретных символов
text_with_dots = "...Python..."
without_dots = text_with_dots.strip('.')
print(f"Без точек: '{without_dots}'")
+
```
-## Форматирование строк: красивый вывод
+### Цепочки методов
-Часто нам нужно вставлять значения переменных в текст. Python предлагает несколько способов форматирования строк. Вот они в порядке от самого современного и рекомендуемого к устаревшему:
+Каждый строковый метод возвращает новую строку (помним про неизменяемость). А значит, их можно вызывать подряд через точку — результат одного сразу передаётся следующему. Это называется **цепочкой методов**.
-### F-строки (самый удобный способ!)
+Например, нужно нормализовать пользовательский ввод: убрать лишние пробелы по краям и привести к нижнему регистру.
-F-строки появились в Python 3.6 и быстро стали самым популярным способом форматирования:
+В три шага через временные переменные:
```python
-name = "Анна"
-age = 25
+raw_input = " Hello "
+without_spaces = raw_input.strip()
+normalized = without_spaces.lower()
+```
-# Просто добавьте 'f' перед строкой и вставляйте переменные в {}
-greeting = f"Привет, меня зовут {name} и мне {age} лет."
-print(greeting)
+Цепочкой в одну строку:
-# Можно вставлять выражения и форматировать вывод
-price = 19.99
-quantity = 3
-total = f"Итого: {price * quantity:.2f} руб."
-print(total)
+```python
+raw_input = " Hello "
+normalized = raw_input.strip().lower()
+print(f"'{normalized}'")
+
```
-F-строки не только удобны, но и работают быстрее других методов форматирования!
+Цепочки работают, потому что после `strip()` мы получаем новую строку, у которой тут же вызывается `lower()`. Это типичный паттерн Python-кода: вместо ведра промежуточных переменных — одна выразительная цепочка.
-### Метод format()
+## Форматирование строк
-Этот метод был основным до появления f-строк:
+Часто нужно вставлять значения переменных в текст. В современном Python для этого используют **f-строки**:
```python
-# Простое форматирование
name = "Анна"
-greeting = "Привет, {}!".format(name)
-print(greeting)
-
-# Несколько подстановок
-template = "Имя: {}, Возраст: {}, Рост: {}"
-info = template.format("Иван", 25, 180)
-print(info)
+age = 25
-# Именованные параметры (более понятный код)
-person_info = "Я {name}, мне {age} лет".format(name="Анна", age=30)
-print(person_info)
+# Просто добавьте 'f' перед строкой и вставляйте переменные в {}
+greeting = f"Привет, меня зовут {name} и мне {age} лет."
+print(greeting)
+
-# Форматирование чисел
-pi = 3.1415
-formatted = "Число Пи: {:.2f}".format(pi)
-print(formatted)
+# Внутри {} можно вставлять любые выражения
+price = 19.99
+quantity = 3
+total = f"Итого: {price * quantity:.2f} руб."
+print(total)
+
```
-### Старый стиль форматирования (%)
-
-Это самый старый способ, который вы можете встретить в старом коде:
-
-```python
-# Старый стиль (не рекомендуется в новом коде)
-name = "Мария"
-age = 28
-message = "Привет, %s! Тебе %d лет." % (name, age)
-print(message)
-```
+Помимо f-строк существуют ещё два способа форматирования, которые встречаются в старом коде: метод `"...".format(...)` и оператор `"..." % (...)`. В новом коде их обычно не используют: f-строки проще и быстрее.
## Проверка понимания
@@ -368,3 +480,11 @@ print(message)
text = "Python"
result = text[1:4]
```
+
+1. "Pyt" — Срез \[1:4] начинается с индекса 1 (второй символ) и заканчивается перед индексом 4.
+
+2. **Правильный ответ:** "yth" — Срез \[1:4] в строке "Python" возвращает символы с индексами 1, 2 и 3, то есть "yth".
+
+3. "ytho" — Срез \[1:4] извлекает символы с индексами от 1 до 3 включительно, не включая символ с индексом 4.
+
+4. "Pyth" — Срез \[1:4] начинается со второго символа (индекс 1), а не с первого.
diff --git a/course/module-1/tuples/article.md b/course/module-1/tuples/article.md
index d3cb6cb..2fbd6d8 100644
--- a/course/module-1/tuples/article.md
+++ b/course/module-1/tuples/article.md
@@ -1,28 +1,30 @@
-# Кортежи в Python
+---
+meta:
+ title: "Кортежи в Python"
+ description: "Подробное описание кортежей в Python: создание, методы для работы с кортежами, сравнение со списками и практические примеры использования."
+---
-В этой статье мы познакомимся с кортежами (tuple) —
-упорядоченными и неизменяемыми коллекциями данных в Python.
+# Кортежи в Python
-Кортежи могут показаться очень похожими на списки, но между ними есть важные различия,
-которые влияют на то, когда и как их следует использовать.
+Когда у данных **фиксированная форма** (координаты, RGB-цвет, возвращаемое значение функции из нескольких частей), это случай для кортежа, а не списка.
## Что такое кортеж?
-Кортеж в Python — это упорядоченная, неизменяемая коллекция элементов, которые могут быть разных типов. Проще говоря, это как список, но который нельзя изменить после создания.
+Кортеж (`tuple`) в Python — это упорядоченная, неизменяемая коллекция элементов, которые могут быть разных типов. Проще говоря, это как список, только изменить его после создания нельзя.
Основные свойства кортежей:
-- **Упорядоченность**: элементы хранятся в определенном порядке
-- **Неизменяемость**: после создания нельзя добавить, удалить или изменить элементы
-- **Индексируемость**: к элементам можно обращаться по индексам
-- **Допускают дубликаты**: могут содержать повторяющиеся значения
-- **Могут содержать разные типы данных**: целые числа, строки, списки и т.д.
+- **Упорядоченность**: элементы хранятся в определённом порядке
+- **Неизменяемость**: после создания нельзя добавить, удалить или изменить элементы
+- **Индексируемость**: к элементам можно обращаться по индексам
+- **Допускают дубликаты**: могут содержать повторяющиеся значения
+- **Могут содержать разные типы данных**: целые числа, строки, списки и т.д.
## Создание кортежей
Существует несколько способов создания кортежей в Python:
-### 1. С помощью круглых скобок ()
+### С помощью круглых скобок ()
```python
# Пустой кортеж
@@ -30,11 +32,17 @@ empty_tuple = ()
# Кортеж с одним элементом (обязательно с запятой!)
single_item = (42,)
-type(single_item)
+print(type(single_item))
+
# Без запятой будет просто число: (42) == 42
single_item_num = (42)
-type(single_item_num)
+print(type(single_item_num))
+
# Кортеж чисел
numbers = (1, 2, 3, 4, 5)
@@ -46,21 +54,20 @@ mixed = (1, "hello", True, 3.14)
nested = ((1, 2), ("a", "b"), (True, False))
```
-### 2. Без скобок (разделение запятыми)
+### Без скобок (разделение запятыми)
Python позволяет создавать кортежи даже без скобок, просто перечисляя элементы через запятую:
```python
# Создание кортежа без скобок
coordinates = 10.5, 20.7, 30.9
-type(coordinates)
-
-# Распаковка кортежа
-x, y, z = coordinates
-print(x, y, z)
+print(type(coordinates))
+
```
-### 3. С помощью конструктора tuple()
+### С помощью конструктора tuple()
```python
# Создание пустого кортежа
@@ -69,14 +76,16 @@ empty_tuple = tuple()
# Преобразование списка в кортеж
list_to_tuple = tuple([1, 2, 3])
print(list_to_tuple)
+
# Преобразование строки в кортеж (каждый символ становится элементом)
string_to_tuple = tuple("Python")
print(string_to_tuple)
-
-# Преобразование множества в кортеж
-set_to_tuple = tuple({1, 2, 3})
-print(set_to_tuple)
+
```
## Доступ к элементам кортежа
@@ -91,10 +100,16 @@ fruits = ("apple", "banana", "cherry", "date", "elderberry")
# Получение элементов по индексу
first_fruit = fruits[0]
print(first_fruit)
+
# Отрицательные индексы для доступа с конца кортежа
last_fruit = fruits[-1]
print(last_fruit)
+
```
### Срезы (slices)
@@ -105,14 +120,23 @@ fruits = ("apple", "banana", "cherry", "date", "elderberry")
# Первые три элемента
first_three = fruits[:3]
print(first_three)
+
# От второго до четвертого
middle = fruits[1:4]
print(middle)
+
# Разворот кортежа
reversed_tuple = fruits[::-1]
print(reversed_tuple)
+
```
## Методы кортежей
@@ -125,10 +149,16 @@ fruits = ("apple", "banana", "cherry", "banana", "date")
# Подсчет количества вхождений элемента
banana_count = fruits.count("banana")
print(banana_count)
+
# Поиск индекса первого вхождения элемента
banana_index = fruits.index("banana")
print(banana_index)
+
```
## Операции с кортежами
@@ -137,32 +167,79 @@ print(banana_index)
# Узнать длину
fruits = ("apple", "banana", "cherry")
print(len(fruits))
+
# Проверить наличие элемента
print("apple" in fruits)
+
print("mango" in fruits)
+
# Конкатенация (объединение) кортежей
more_fruits = ("pear", "orange")
all_fruits = fruits + more_fruits
print(all_fruits)
+
# Повторение
repeated = fruits * 2
print(repeated)
+
# Распаковка
a, b, c = fruits
print(a, b, c)
+
+```
+
+## Сравнение кортежей
+
+Кортежи сравниваются **поэлементно**, слева направо, до первого отличия:
+
+```python
+print((1, 2) < (1, 3))
+
+print((2, 0) < (1, 9))
+
```
+В первом случае первые элементы равны (`1 == 1`), Python переходит ко вторым: `2 < 3`, значит весь кортеж меньше. Во втором сразу `2 > 1`, дальше можно не смотреть.
+
+Это позволяет одной командой отсортировать список кортежей по нескольким полям сразу:
+
+```python
+people = [("Bob", 30), ("Anna", 25), ("Anna", 30)]
+print(sorted(people))
+
+```
+
+Сортировка идёт сначала по первому элементу (имени), а при равенстве — по второму (возрасту).
+
## Неизменяемость кортежей
Важно понимать, что неизменяемость кортежей означает, что после создания кортежа вы не можете:
-- Изменить существующие элементы
-- Добавить новые элементы
-- Удалить элементы
+- Изменить существующие элементы
+- Добавить новые элементы
+- Удалить элементы
```python
# Создаем кортеж
@@ -177,6 +254,9 @@ coordinates = (10.5, 20.7, 30.9)
# Но можно создать новый кортеж на основе существующего
new_coordinates = (15.0,) + coordinates[1:]
print(new_coordinates)
+
```
### Важное замечание о вложенных изменяемых объектах
@@ -188,6 +268,9 @@ tuple_with_list = (1, 2, [3, 4])
# Это работает, потому что мы изменяем список внутри кортежа
tuple_with_list[2][0] = 30
print(tuple_with_list)
+
# Но это вызовет ошибку - нельзя изменить сам кортеж
# tuple_with_list[2] = [5, 6] # TypeError
@@ -207,6 +290,9 @@ def get_user_info():
# Распаковка результата
user_name, user_age, user_is_admin = get_user_info()
print(f"Name: {user_name}, Age: {user_age}, Admin: {user_is_admin}")
+
```
### 2. Фиксированные данные
@@ -218,10 +304,20 @@ DAYS_OF_WEEK = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturda
# Использование
today_index = 4 # Friday
print(f"Today is {DAYS_OF_WEEK[today_index]}")
+
```
## Сравнение списков и кортежей
+Технически кортежи и списки часто пересекаются по возможностям, но в Python-коде у них **разные роли**:
+
+- **Кортеж это «запись»**: позиции имеют конкретный смысл. `(name, age, email)` это не «список из трёх произвольных вещей», а структура, в которой первая позиция всегда имя, вторая всегда возраст, третья всегда email.
+- **Список это однородная коллекция**: все элементы примерно одного «типа задачи», их количество может расти и сокращаться по ходу программы.
+
+Эта разница важнее технических деталей: даже когда оба варианта работают, выбор влияет на читаемость и намерение кода.
+
| Характеристика | Список (list) | Кортеж (tuple) |
| ------------------------- | --------------------------------- | ------------------- |
| Синтаксис | `[1, 2, 3]` | `(1, 2, 3)` |
@@ -240,10 +336,16 @@ print(f"Today is {DAYS_OF_WEEK[today_index]}")
# Неправильно (это не кортеж, а просто число в скобках)
not_a_tuple = (42)
print(type(not_a_tuple))
+
# Правильно
single_item_tuple = (42,)
print(type(single_item_tuple))
+
```
2. **Попытка изменить кортеж**
@@ -256,8 +358,21 @@ print(f"Today is {DAYS_OF_WEEK[today_index]}")
coordinates[0] = 15.0
except TypeError as e:
print(f"Error: {e}")
+
```
## Проверка понимания
**Какое из следующих утверждений о кортежах в Python верно?**
+
+1. **Правильный ответ:** Кортежи могут использоваться в качестве ключей словаря — Кортежи, в отличие от списков, неизменяемы, поэтому они могут использоваться в качестве ключей словарей.
+
+2. Кортежи можно изменять после создания — Кортежи являются неизменяемыми (immutable) в Python. После создания кортежа вы не можете добавить, удалить или изменить его элементы.
+
+3. Кортеж с одним элементом записывается как (42) — Кортеж с одним элементом должен содержать запятую после элемента: (42,). Без запятой (42) будет рассматриваться как просто число в скобках.
+
+4. Кортежи могут содержать только элементы одного типа — Кортежи, как и списки, могут содержать элементы разных типов данных: числа, строки, списки, другие кортежи и т.д.
+
+В следующем уроке посмотрим [множества](https://python-academy.org/ru/guide/sets) (`set`) — коллекции, в которых каждый элемент уникален.
diff --git a/course/module-1/variable-scope/article.md b/course/module-1/variable-scope/article.md
index 82b8c58..3b43e9b 100644
--- a/course/module-1/variable-scope/article.md
+++ b/course/module-1/variable-scope/article.md
@@ -1,190 +1,213 @@
-# Область видимости переменных в Python
+---
+meta:
+ title: "Область видимости переменных в Python"
+ description: "Локальные и глобальные переменные в Python, ключевое слово global, типичная ошибка UnboundLocalError и как правильно передавать данные между функциями."
+---
-Сегодня мы разберемся с важной концепцией
-в программировании — областью видимости переменных.
+# Область видимости переменных в Python
-Если переменные — это "коробочки" для хранения данных, то область видимости определяет,
-где эти коробочки доступны и могут использоваться.
-Это как разные комнаты в доме — некоторые вещи доступны только в определенной комнате,
-а другие — во всем доме!
+Программы на Python почти никогда не пишут одной сплошной простынёй. Их собирают из **функций** — отдельных кусочков кода, у каждого своё имя и своя задача. Объявляют функцию словом `def`, а `return` говорит, какой результат она отдаёт наружу. Разберём их подробно в отдельной главе, а пока достаточно уметь читать вот такое:
-## Что такое область видимости?
+```python
+def area_circle(radius):
+ result = 3.14 * radius * radius
+ return result
-> Область видимости (scope) переменной — это часть программы, в которой переменная доступна
-> для использования. Она определяет, где и как переменная может быть использована в коде.
+def area_square(side):
+ result = side * side
+ return result
+```
-Понимание области видимости переменных — **важный навык**, который помогает вам предотвращать ошибки и писать более структурированный код. В Python переменные могут иметь разные области видимости, что влияет на их доступность и время жизни.
+Здесь две функции, и в обеих внутри используется переменная `result`. Друг другу они не мешают: каждая функция работает со своим `result`, и одно никак не влияет на другое.
-## Локальные и глобальные переменные
+## Что такое область видимости?
-В Python существует два основных типа переменных с точки зрения области видимости:
+Правило, по которому Python решает, какая переменная имеется в виду в каждом месте кода, называется **областью видимости** (scope). В ней два уровня:
-1. **Глобальные переменные** - создаются в основном теле программы и доступны во всех частях программы
-2. **Локальные переменные** - создаются внутри функций и доступны только внутри этих функций
+- **глобальная** область: всё, что объявлено в основном теле программы; видно из любой функции
+- **локальная** область: переменные, объявленные внутри функции; живут только пока функция выполняется
-Представьте себе школу: глобальные переменные — это как объявления на главной доске объявлений, которые видны всем, а локальные переменные — это как записи на досках в отдельных классах, видные только ученикам конкретного класса.
+Удобно представлять это как комнаты в квартире. Вещи на твоём личном столе доступны только тебе (это локальная переменная функции). А вот общее объявление на холодильнике видит вся семья (это глобальная переменная).
-
+
-### Глобальные переменные
+## Глобальные переменные
-Глобальные переменные определяются вне функций и могут использоваться как внутри функций, так и вне их:
+Глобальные переменные определяются вне функций и доступны как изнутри функций, так и снаружи:
```python
-# Создаем глобальную переменную
+# Создаём глобальную переменную
message = "Привет, мир!"
def show_message():
# Используем глобальную переменную внутри функции
print(message)
-show_message() # Вызываем функцию
+show_message()
+
print(f"Переменная вне функции: {message}")
+
```
-Глобальные переменные удобны, когда данные должны быть доступны во всех частях программы, но их чрезмерное использование может затруднить отладку и поддержку кода.
+Чем больше глобалов, тем сложнее отлаживать код: чтобы понять, что делает функция, недостаточно смотреть на её аргументы — нужно ещё помнить, какие переменные снаружи она читает.
-### Локальные переменные
+## Локальные переменные
-Локальные переменные создаются внутри функций и существуют только во время выполнения этой функции. После завершения работы функции они удаляются из памяти:
+Локальные переменные создаются внутри функций и существуют только пока функция выполняется. После её завершения они исчезают из памяти:
```python
def calculate_sum():
- # Локальные переменные
a = 10
b = 20
result = a + b
print(f"Сумма внутри функции: {result}")
calculate_sum()
+
# Попытка обратиться к локальной переменной вне функции
try:
- print(f"Пытаемся получить доступ к result: {result}")
+ print(f"result: {result}")
except NameError as e:
print(f"Ошибка: {e}")
+
```
-Как видите, переменная `result` существует только внутри функции `calculate_sum()` и недоступна за её пределами. Это как если бы вы оставили свои вещи в гостиничном номере — они доступны, только пока вы находитесь в номере! 🏨
+Переменная `result` существует только внутри `calculate_sum()` и наружу не выходит.
-### Приоритет локальных переменных
+## Когда имена совпадают
-Когда внутри функции создается переменная с тем же именем, что и глобальная переменная, Python отдает приоритет локальной версии:
+Если внутри функции создаётся переменная с тем же именем, что и глобальная, локальная имеет приоритет. Это называется **shadowing**: локальная «заслоняет» глобальную, не трогая её:
```python
x = "глобальная"
def test_scope():
- x = "локальная" # Локальная переменная с тем же именем
+ x = "локальная"
print(f"Внутри функции: x = {x}")
test_scope()
+
print(f"Вне функции: x = {x}")
+
```
-В этом примере создаются две разные переменные `x` (локальная и глобальная),
-несмотря на одинаковое имя. Локальная переменная не влияет на глобальную и наоборот — они как полные тёзки, которые живут в разных городах! 👥
-
-Это очень важно понимать, чтобы избежать путаницы в вашем коде!
+Это две разные переменные с одинаковым именем. Они как полные тёзки, живущие в разных городах: имя совпадает, но это разные люди.
-## Модификация глобальных переменных
+## Изменение глобальной переменной из функции
-А что если вам нужно изменить глобальную переменную внутри функции? По умолчанию Python интерпретирует присваивание значения переменной внутри функции как создание новой локальной переменной:
+По умолчанию любое присваивание внутри функции Python считает созданием новой локальной переменной:
```python
x = "глобальная"
def modify_global():
- # Python создаст новую локальную переменную x
- x = "новая локальная" # Эта переменная не меняет глобальную
+ x = "новая локальная" # Это локальная, глобальная не меняется
print(f"Внутри функции: x = {x}")
modify_global()
+
print(f"Вне функции: x = {x}")
+
```
-Как видим, функция не изменила глобальную переменную. Для прямого изменения глобальной переменной внутри функции используется ключевое слово `global`.
-
-### Ключевое слово global
-
-Ключевое слово `global` указывает Python, что переменная должна быть взята из глобальной области видимости, а не создана локально:
+Чтобы функция реально изменила глобальную переменную, нужно явно сказать `global x` в начале функции:
```python
x = "глобальная"
def modify_global():
- global x # Указываем, что используем глобальную переменную
+ global x
x = "глобальная изменена"
print(f"Внутри функции: x = {x}")
modify_global()
+
print(f"Вне функции: x = {x}")
+
```
-Теперь функция успешно изменила глобальную переменную! Это как иметь пульт дистанционного управления, который позволяет изменять вещи в другой комнате! 📱
+Ключевое слово `global` сигналит читателю: «эта функция не просто работает со своими аргументами, она лезет наружу». Поэтому им стоит пользоваться, только когда другого пути нет.
-Ключевое слово `global` также позволяет создавать новые глобальные переменные внутри функций:
+## Самая частая ловушка: UnboundLocalError
-```python
-def create_global_var():
- global new_var # Создаем глобальную переменную
- new_var = "Эта переменная будет доступна вне функции"
- print("Переменная создана внутри функции")
+В одной функции читаем глобальную переменную и тут же её переприсваиваем:
-create_global_var()
+```python
+x = 10
-# Теперь переменная доступна вне функции
-print(f"Переменная вне функции: {new_var}")
+def show():
+ print(x) # читаем
+ x = x + 1 # и тут же присваиваем
```
-Но помните — с большой силой приходит большая ответственность! Используйте `global` только когда это действительно необходимо.
+Кажется, что функция должна напечатать `10` и увеличить `x` до `11`. Но запуск падает с ошибкой `UnboundLocalError: local variable 'x' referenced before assignment`.
+
+В чём дело? Python смотрит на функцию **целиком** ещё до её выполнения и определяет, какие имена локальные. Видит `x = ...` внутри — значит, `x` объявлена в этой функции, она локальная. И в верхней строке `print(x)` Python пытается прочитать **локальную** `x`, которой ещё не присвоено значение, и падает.
-## Лучшие практики работы с областью видимости
+Чтобы такая функция работала с глобальной `x`, нужен `global x` в начале функции. Но обычно лучше другой путь: не трогать глобал, а принять `x` параметром и вернуть новое значение:
-При работе с областью видимости переменных в Python рекомендуется следовать этим практикам:
+```python
+def increment(value):
+ return value + 1
-1. **Минимизируйте использование глобальных переменных** — они усложняют понимание кода и могут привести к непредсказуемым результатам.
+x = 10
+x = increment(x)
+print(x)
+
+```
-2. **Передавайте данные через параметры функций** — это делает код более явным и модульным:
+## Когда global нужен, а когда нет
+
+Самый предсказуемый способ работать с данными — не модифицировать глобалы, а передавать значения через параметры и возвращать через `return`:
```python
def add(a, b):
- result = a + b
- return result
+ return a + b
x = 5
y = 10
-sum_result = add(x, y) # Передаем переменные как аргументы
+sum_result = add(x, y)
print(f"{x} + {y} = {sum_result}")
+
```
-3. **Используйте возвращаемые значения вместо модификации глобальных переменных**:
+Такая функция честная: её результат полностью определяется тем, что в неё пришло. Ничего «закулисного». Это упрощает и тестирование, и чтение кода — посмотрел на сигнатуру, и сразу всё понятно.
-```python
-def increment(value):
- return value + 1
-
-count = 0
-print(f"Начальное значение: {count}")
-
-# Обновляем значение через возвращаемое значение
-count = increment(count)
-print(f"После инкремента: {count}")
-
-# Можем вызывать функцию несколько раз
-count = increment(increment(count))
-print(f"После двойного инкремента: {count}")
-```
+`global` оправдан, когда переменная действительно должна жить на уровне всей программы — например, конфигурация или общий счётчик. Но в большинстве учебных задач он не нужен.
## Проверка понимания
-Давайте проверим, насколько хорошо вы поняли концепцию области видимости переменных:
+Давайте проверим, как вы поняли концепцию области видимости переменных.
-
+Интерактивная демонстрация доступна [в уроке на Python Academy](https://python-academy.org/ru/guide/variable-scope).
-Теперь вы понимаете, как работает область видимости переменных в Python.
-Это важная концепция, которая поможет вам писать более структурированный, понятный и предсказуемый код.
+В следующем уроке разберём базовые типы данных в Python: что прячется за `int`, `str`, `float`, `bool`, и какие операции с ними определены.
diff --git a/course/module-1/variables/article.md b/course/module-1/variables/article.md
index 6880571..e42278e 100644
--- a/course/module-1/variables/article.md
+++ b/course/module-1/variables/article.md
@@ -1,23 +1,35 @@
+---
+meta:
+ title: "Переменные в Python"
+ description: "Что такое переменные в Python. Создание переменных, множественное присваивание, правила и лучшие практики именования переменных."
+---
+
# Переменные в Python
-Привет! Сегодня мы познакомимся с одним из самых важных понятий в программировании — переменными. Представьте, что это подписанные «коробочки» (именованные области памяти), в которые вы можете складывать разные вещи: числа, тексты, списки и многое другое.
+Допустим, мы пишем программу-приветствие. Имя пользователя мы не знаем заранее: оно появится только когда программа запустится. Значит, в коде нам нужно как-то ссылаться на это будущее имя, не зная его. Именно для этого существуют переменные.
-## Что такое переменная?
+```python
+name = "Иван"
+print("Привет, " + name + "!")
+```
-> Переменная в Python — это именованная ссылка на объект в памяти, тип которой определяется динамически и может изменяться.
+В этом коде `name` — это **ярлык**, который Python повесил на строку `"Иван"` в памяти. Когда дальше встречается `name`, Python смотрит, на что этот ярлык указывает, и подставляет значение.
-В Python работа с переменными максимально проста:
+
-- Не нужно объявлять переменные перед использованием
-- Не нужно указывать тип — Python сам определит его
-- Можно изменить тип переменной в любой момент
+## Что такое переменная?
-Создание переменной происходит при первом присваивании, и дальше вы можете использовать её где угодно в программе.
+> Переменная в Python — это имя, привязанное к объекту в памяти. Тип объекта Python определяет сам, и эту привязку можно поменять в любой момент.
+
+Из этой формулировки следуют три удобства:
+
+- не нужно объявлять переменную заранее: она появляется при первом присваивании
+- не нужно указывать тип: Python видит, что справа, и запоминает
+- можно перепривязать имя к другому значению в любой момент
## Создание и использование переменных
-Для создания переменной используется оператор `=`.
-Пример:
+Переменная создаётся через оператор `=`. Слева пишется имя, справа — значение, на которое это имя будет указывать:
```python
# Создание переменных различных типов
@@ -28,83 +40,85 @@ is_student = True # Логическая переменная
courses = ["Python", "SQL", "JavaScript"] # Переменная-список
```
-После этого при выполнении кода `print()` или других операций на место имени переменной будет подставлено её значение:
+После создания имя можно подставлять везде, где нужно его значение:
```python
name = "Иван"
-# Используем переменную в строке с приветствием
-print("Привет, " + name + "! 👋")
+print("Привет, " + name + "!")
+
age = 25
-# Используем переменную в вычислении
next_year_age = age + 1
-print(f"В следующем году тебе будет {next_year_age} лет! 🎂")
+print(f"В следующем году тебе будет {next_year_age} лет")
+
```
-## Множественное присваивание
-
-Python позволяет присваивать значения нескольким переменным в одной строке:
-
-```python
-# Присваиваем одно значение нескольким переменным
-x = y = z = 0
-print(f"x = {x}, y = {y}, z = {z}")
+Во втором примере перед кавычками стоит буква `f`. Она означает: внутри строки можно писать имена переменных в фигурных скобках, и Python подставит туда их значения. Такие строки называют f-строками, и дальше в курсе они встретятся часто — подробно разберём их в главе про строки.
-# Присваиваем разные значения нескольким переменным
-a, b, c = 1, 2, 3
-print(f"a = {a}, b = {b}, c = {c}")
-```
+## Множественное присваивание
-Также можно «распаковать» коллекцию в несколько переменных:
+Чаще всего такое присваивание применяют для **распаковки**: справа стоит коллекция, слева сразу несколько имён. Они получают значения по порядку:
```python
coordinates = (10, 20, 30)
x, y, z = coordinates
-print(f"Координаты: x={x}, y={y}, z={z}")
+print(f"x={x}, y={y}, z={z}")
+
```
-Для обмена значениями двух переменных без промежуточной используй простое присваивание:
+Из той же логики растёт элегантный способ обменять значения двух переменных без временной:
```python
-# Обмен значениями переменных
a = 5
b = 10
-print(f"До обмена: a = {a}, b = {b}")
+a, b = b, a
+print(f"a = {a}, b = {b}")
+
+```
+
+Справа сначала собирается кортеж `(10, 5)`, потом он распаковывается слева. Никакой третьей переменной не нужно.
-a, b = b, a # Обмен значениями
-print(f"После обмена: a = {a}, b = {b}")
+Можно также присвоить одно значение сразу нескольким именам через цепочку `=`:
+
+```python
+x = y = z = 0
```
+В реальном коде такое встречается редко, обычно проще написать три строки. К тому же с изменяемыми объектами цепочка создаст одну и ту же ссылку: `a = b = []` сделает `a` и `b` одним и тем же списком, что почти всегда не то, что хотелось.
+
## Динамическая типизация
-Python — язык с динамической типизацией. Тип переменной определяется во время выполнения и может меняться при присваивании нового значения:
+Python — язык с динамической типизацией. Тип переменной определяется во время выполнения и может меняться при присваивании нового значения. Посмотреть текущий тип можно функцией `type()` — она показывает, какого типа значение:
```python
-# Демонстрация изменения типа переменной
x = 10 # x имеет тип int (целое число)
print(f"x = {x}, тип: {type(x)}")
+
x = "десять" # теперь x имеет тип str (строка)
print(f"x = {x}, тип: {type(x)}")
+
x = [1, 2, 3] # теперь x имеет тип list (список)
print(f"x = {x}, тип: {type(x)}")
+
```
-Для определения текущего типа данных переменной используется встроенная функция `type()`:
-
-```python
-name = "Иван"
-age = 25
-height = 1.85
-is_student = True
-
-# Определение типов переменных
-print(f"{name}: {type(name)}")
-print(f"{age}: {type(age)}")
-print(f"{height}: {type(height)}")
-print(f"{is_student}: {type(is_student)}")
-```
+Сами типы — что такое `str`, `int`, `float` и `bool` и чем они отличаются друг от друга — разберём позже, в уроке про базовые типы данных. Пока достаточно того, что тип у значения есть и он может меняться.
## Именование переменных в Python
@@ -116,7 +130,7 @@ print(f"{is_student}: {type(is_student)}")
2. Имя должно начинаться с буквы или символа подчёркивания.
3. Нельзя использовать зарезервированные слова Python (например, `if`, `for`, `class`).
-```python
+```python-interactive
# Допустимые имена
name = "Иван"
age_in_years = 25
@@ -130,68 +144,65 @@ _private_variable = "Непубличная информация"
### Лучшие практики для именования переменных
-В сообществе Python существует официальное руководство по стилю кода — [PEP 8](https://peps.python.org/pep-0008/), которое содержит рекомендации по именованию переменных и другим аспектам написания чистого, понятного Python-кода.
-
-Следование этим рекомендациям считается хорошей практикой и помогает писать код, который будет легко поддерживать:
+В сообществе Python есть официальное руководство по стилю, PEP 8. Это не закон, но почти весь Python-код, который вы встретите, ему следует. И читать ваш код будет проще, когда он выглядит так же.
-1. **Используйте описательные имена** — пусть имя переменной говорит о её назначении
+1. **Используйте описательные имена**: пусть имя говорит о назначении
```python
# Лучше так ✅
user_age = 25
-print(f"Возраст пользователя: {user_age}")
# Чем так ❌
a = 25
-print(f"Значение переменной a: {a}") # О чём эта переменная?
```
-2. **Используйте стиль snake_case** для переменных (слова в нижнем регистре, разделенные подчеркиваниями)
+Через месяц к коду вернётесь вы сами, и `a` уже ни о чём не скажет. `user_age` сразу подсказывает читателю, что это.
+
+2. **Используйте snake_case** (слова в нижнем регистре через подчёркивание)
```python
# Python-стиль ✅
first_name = "Иван"
-print(f"Имя: {first_name}")
# Не в стиле Python ❌
firstName = "Иван"
-print(f"Имя: {firstName}")
```
-3. **Добавляйте префикс `is_` или `has_`** для логических переменных (тип bool)
+Технически оба варианта работают. Но весь Python-код вокруг написан в snake_case, и переключение между стилями в одном проекте утомляет глаз.
+
+3. **Префикс `is_` или `has_`** для логических переменных
```python
-# Сразу понятно, что это логические переменные ✅
is_adult = True
has_permission = False
-
-print(f"Совершеннолетний: {is_adult}")
-print(f"Имеет разрешение: {has_permission}")
```
-4. **Используйте ВЕРХНИЙ_РЕГИСТР** для констант (значений, которые не должны меняться)
+Когда вы видите `if is_adult:`, сразу понятно, что справа булево значение, а не, скажем, число лет или объект пользователя. Имя `adult` без префикса такой подсказки не даёт.
+
+4. **ВЕРХНИЙ_РЕГИСТР** для констант
+
+```python
+MAX_ATTEMPTS = 3
+PI = 3.14159
+```
- ```python
- # Константы в верхнем регистре ✅
- MAX_ATTEMPTS = 3
- PI = 3.14159
- ```
+В Python нет настоящих констант: `MAX_ATTEMPTS` можно переприсвоить, и язык не возразит. Но соглашение «всё в верхнем регистре трогать нельзя» общепринятое, и его все соблюдают.
-5. **Избегайте слишком коротких и непонятных имен** — экономия на символах приводит к потере времени при чтении кода
+5. **Избегайте слишком коротких имён**
- ```python
- # Плохие имена переменных ❌
- n = "Иван" # Слишком короткое
- x = 25 # Непонятное
- flag = True # Неясное назначение
- str1 = "Строка" # Неинформативное
+```python
+# Плохо ❌
+n = "Иван"
+flag = True
+str1 = "Строка"
+
+# Хорошо ✅
+user_name = "Иван"
+is_verified = True
+welcome_message = "Добро пожаловать!"
+```
- # Хорошие имена переменных ✅
- user_name = "Иван"
- age_in_years = 25
- is_verified = True
- welcome_message = "Добро пожаловать!"
- ```
+Несколько лишних символов при написании экономят минуты при чтении. А читают код всегда чаще, чем пишут.
### Чувствительность к регистру
@@ -203,21 +214,45 @@ Name = "Пётр"
NAME = "Алексей"
print(name)
+
print(Name)
+
print(NAME)
+
```
Три разные переменные: изменение регистра в имени переменной приводит к самостоятельной новой переменной.
-## Проверка понимания
+## Задания для самопроверки
+
+Сразу под этой статьёй есть блок **Задания для самопроверки**. В нём можно сразу попробовать применить то, что прочитали.
+
+### Что уже написано в редакторе
+
+В редакторе справа лежит заготовка: имена переменных из условия и знак `=`. Осталось вписать значения.
+
+
+
+Слева находится **условие**: что и какими значениями назвать. Плашки в условии (например, `John`, `25`, `True`) означают ровно те значения, которые ждут в коде.
+
+Кнопка **«Проверить»** запускает ваш код и сверяет результат с тестами. Нажимать её можно сколько угодно раз.
+
+### Что именно проверяется
-Давайте проверим, насколько хорошо вы поняли особенности работы с переменными в Python:
+Под редактором есть вкладки: «Результат» и рядом с ним «Тест #1», «Тест #2» и так далее. Задание засчитывается, только когда проходят все тесты.
-**Какое из следующих утверждений наиболее точно описывает переменные в Python?**
+Каждая вкладка с тестом показывает, что именно будет проверено: строку кода и значение, которое должно получиться. Например, «Тест #2» читает `user_age` и ждёт, что там окажется `25`. Открыть тесты можно в любой момент, в том числе до первой проверки.
+
-## Заключение
+После проверки на каждой вкладке появляется отметка: зелёная галочка у пройденных тестов, красный крестик у непройденных. Непройденный тест открывается сам, и под его кодом появляется блок «Полученный результат» со значением, которое оказалось на самом деле. Сравните его с ожидаемым, и расхождение сразу станет видно.
-Отлично! Теперь вы знаете, что такое переменные в Python и как с ними работать. Это фундаментальная концепция, которая будет основой для всего вашего дальнейшего путешествия в мире программирования.
+На вкладке «Результат» лежит общий итог (сколько тестов прошло) и всё, что вы напечатали через `print()`. Это удобно, когда хочется посмотреть промежуточные значения по ходу решения.
-Помните, что правильное именование переменных и понимание их динамической природы в Python сделает ваш код более читаемым и поддерживаемым. Экспериментируйте, практикуйтесь, и у вас обязательно получится! 💪
+В следующем уроке соберём код в **функции**: как дать куску кода имя, передать ему значения и получить результат обратно.
diff --git a/course/module-2/attributes/article.md b/course/module-2/attributes/article.md
index c789bc1..6f470bb 100644
--- a/course/module-2/attributes/article.md
+++ b/course/module-2/attributes/article.md
@@ -1,84 +1,77 @@
-# Атрибуты и методы в Python
-
-В прошлом уроке мы видели атрибуты (`self.name`) и методы (`def greet(self)`). Сейчас копнём глубже: бывают ещё **атрибуты класса** (общие для всех экземпляров), методы делятся на три вида (обычные, классовые, статические), а в `__init__` живёт классическая ловушка с изменяемыми значениями по умолчанию, которая ловит даже опытных разработчиков.
+---
+meta:
+ title: "Атрибуты и специальные методы"
+ description: "Чем атрибуты класса отличаются от атрибутов экземпляра, как обойти ловушку изменяемых значений по умолчанию и зачем нужны __str__ и __eq__."
+---
-## Типы атрибутов
+# Атрибуты и специальные методы
-1. **Атрибуты экземпляра**: уникальны для каждого объекта.
-2. **Атрибуты класса**: общие для всех экземпляров.
+В прошлом уроке все данные объекта жили в атрибутах вроде `self.name` — у каждого объекта свои. Сегодня разберём данные, общие для всех объектов класса, обезвредим классическую ловушку в `__init__` и научим объекты печататься и сравниваться по-человечески.
-### Атрибуты экземпляра
+## Атрибуты экземпляра и атрибуты класса
-> Атрибуты экземпляра: переменные, которые хранят данные конкретного объекта. Обычно создаются в `__init__` через `self.имя = ...`.
+Атрибуты, которые создаёт `__init__` через `self.имя = ...`, называются **атрибутами экземпляра**: у каждого объекта своё значение. Но бывают данные, одинаковые для всех. Пусть все студенты учатся в одной школе — хранить её название в каждом объекте нет смысла: значение одно на всех, а поменяется — придётся обновлять в каждом объекте.
-```python-executable
-class Person:
- def __init__(self, name, age):
- # Атрибуты экземпляра
- self.name = name
- self.age = age
+Такие данные объявляют прямо в теле класса, вне методов. Это **атрибут класса**:
- def birthday(self):
- self.age += 1
- return f"{self.name} теперь {self.age} лет!"
+```python
+class Student:
+ school = "Школа №1" # атрибут класса — один на всех
-# Создаём два разных объекта
-person1 = Person("Анна", 25)
-person2 = Person("Иван", 30)
+ def __init__(self, name):
+ self.name = name # атрибут экземпляра — у каждого свой
-# У каждого объекта свои значения атрибутов
-print(f"{person1.name}: {person1.age} лет")
-# Вывод: Анна: 25 лет
+student1 = Student("Алексей")
+student2 = Student("Екатерина")
-# Изменение атрибута одного объекта не влияет на другой
-print(person1.birthday())
-# Вывод: Анна теперь 26 лет!
-print(f"{person2.name}: {person2.age} лет") # Возраст не изменился
-# Вывод: Иван: 30 лет
+print(f"{student1.name}, {student1.school}")
+
+print(f"{student2.name}, {student2.school}")
+
```
-### Атрибуты класса
-
-> Атрибуты класса: переменные, объявленные прямо в теле класса (вне методов). Они общие для всех экземпляров.
+Заметьте: `student1.school` читается, хотя в самом объекте такого атрибута нет. Не найдя имя в экземпляре, Python ищет его в классе. Поэтому достаточно поменять атрибут класса — и изменение увидят сразу все объекты:
-```python-executable
+```python
class Student:
- # Атрибут класса: общий для всех экземпляров
school = "Школа №1"
def __init__(self, name):
- # Атрибут экземпляра: у каждого студента свой
self.name = name
-# Создаём студентов
student1 = Student("Алексей")
student2 = Student("Екатерина")
-# Имя у каждого своё, школа одна
-print(f"{student1.name}, {student1.school}")
-# Вывод: Алексей, Школа №1
+Student.school = "Школа №5"
-# Меняем атрибут класса: это видят сразу все экземпляры
-Student.school = "Гимназия №5"
print(f"{student1.name}, {student1.school}")
-# Вывод: Алексей, Гимназия №5
+
print(f"{student2.name}, {student2.school}")
-# Вывод: Екатерина, Гимназия №5
+
```
-### Когда какой использовать
+`class Student`: `school = "Школа №1"`
-- **Атрибуты экземпляра**: для данных, которые различаются между объектами (имя, возраст, id).
-- **Атрибуты класса**: для:
- - Констант и значений по умолчанию;
- - Данных, общих для всех экземпляров (например, имя школы для всех студентов);
- - Счётчиков уровня класса (например, сколько объектов уже создано).
+- `student1`: `name = "Алексей"`, `school` — из класса
+- `student2`: `name = "Екатерина"`, `school` — из класса
+
+`name` — у каждого объекта свой, а `school` хранится в классе: один на всех.
+
+Правило выбора простое: то, что различается между объектами, — в атрибуты экземпляра (имя, возраст), а то, что одно на всех, — в атрибуты класса (константы, общие настройки, значения по умолчанию).
## Ловушка с изменяемыми значениями по умолчанию
-Классическая засада, которая ловит даже опытных Python-разработчиков. Допустим, мы хотим, чтобы у студента по умолчанию был пустой список оценок. Кажется логичным написать так:
+Эта ловушка в `__init__` ловит даже опытных разработчиков. Допустим, мы хотим, чтобы у студента по умолчанию был пустой список оценок. Кажется логичным написать так:
-```python-executable
+```python
class Student:
def __init__(self, name, grades=[]): # выглядит безобидно
self.name = name
@@ -87,186 +80,141 @@ class Student:
s1 = Student("Анна")
s1.grades.append(5)
print(s1.grades)
-# Вывод: [5]
+
s2 = Student("Иван")
print(s2.grades) # ожидаем []
-# Вывод: [5]
+
```
-Иван унаследовал оценку Анны, хотя мы ничего ему не добавляли. Почему?
+У Ивана оказалась оценка Анны, хотя мы ничего ему не добавляли. Почему?
Значения параметров по умолчанию вычисляются **один раз**, в момент определения функции, а не при каждом вызове. Список `[]` создан один раз и переиспользуется всеми вызовами `Student(...)` без аргумента `grades`. То есть `s1.grades` и `s2.grades` указывают на **один и тот же** список в памяти.
-Правильный паттерн: ставить `None` по умолчанию, а реальный список создавать внутри:
+Правильный паттерн: по умолчанию ставить `None` — знакомый по прошлому уроку приём, — а реальный список создавать внутри:
-```python-executable
+```python
class Student:
def __init__(self, name, grades=None):
self.name = name
- self.grades = grades if grades is not None else []
+ if grades is None:
+ grades = []
+ self.grades = grades
s1 = Student("Анна")
s1.grades.append(5)
print(s1.grades)
-# Вывод: [5]
+
s2 = Student("Иван")
print(s2.grades)
-# Вывод: []
+
```
-Теперь каждый объект получает свой собственный пустой список. Тот же приём работает для словарей, множеств и любых других мутабельных значений.
-
-## Типы методов
-
-В Python есть несколько видов методов:
-
-1. **Обычные методы** (методы экземпляра): работают с конкретным объектом через `self`.
-2. **Методы класса**: работают с классом в целом, получают `cls`.
-3. **Статические методы**: не получают ни `self`, ни `cls`.
-4. **Специальные методы**: имеют особое значение для Python (например, `__init__`, `__str__`).
+Теперь каждый объект получает свой собственный пустой список. Тот же приём работает для словарей, множеств и любых других изменяемых значений.
-### Обычные методы
+## Специальные методы
-> Обычные методы: функции внутри класса, принимающие `self` первым параметром. Через `self` обращаются к атрибутам объекта и вызывают другие методы.
+Данные разложены по атрибутам — посмотрим, как объект ведёт себя в привычных операциях. Напечатаем его и сравним два одинаковых:
-```python-executable
-class Rectangle:
- def __init__(self, width, height):
- self.width = width
- self.height = height
-
- def area(self):
- return self.width * self.height
+```python
+class Vector:
+ def __init__(self, x, y):
+ self.x = x
+ self.y = y
- def perimeter(self):
- return 2 * (self.width + self.height)
+v1 = Vector(3, 4)
+v3 = Vector(3, 4)
-# Создаём прямоугольник и вызываем методы
-rect = Rectangle(5, 3)
-print(f"Площадь: {rect.area()}")
-# Вывод: Площадь: 15
-print(f"Периметр: {rect.perimeter()}")
-# Вывод: Периметр: 16
+print(v1)
+
+print(v1 == v3)
+
```
-### Методы класса
-
-> Методы класса: получают сам класс первым параметром (обычно называется `cls`), а не экземпляр. Декорируются через `@classmethod`. Часто используются как альтернативные конструкторы.
+Печать выдала адрес объекта в памяти вместо содержимого, а сравнение ответило `False` при совпадающих координатах: без нашей помощи Python сравнивает объекты по тому, один ли это объект в памяти, а не по содержимому.
-```python-executable
-class Date:
- def __init__(self, day, month, year):
- self.day = day
- self.month = month
- self.year = year
+Обе проблемы решаются **специальными методами** — методами с двойными подчёркиваниями в имени, которые Python вызывает сам в нужный момент:
- def display(self):
- return f"{self.day:02d}.{self.month:02d}.{self.year}"
+- `__init__` вы уже пишете в каждом классе: он срабатывает при `Student(...)`;
+- `__str__` срабатывает, когда объект нужно превратить в строку — например, внутри `print()`;
+- `__eq__` — при сравнении через `==`;
+- `__add__` — при сложении через `+`.
- # Метод класса: альтернативный конструктор
- @classmethod
- def from_string(cls, date_string):
- day, month, year = map(int, date_string.split('.'))
- return cls(day, month, year)
-
-# Создаём объект стандартным способом
-date1 = Date(15, 6, 2023)
-print(date1.display())
-# Вывод: 15.06.2023
-
-# Создаём объект, используя метод класса
-date2 = Date.from_string("25.12.2023")
-print(date2.display())
-# Вывод: 25.12.2023
-```
+Определим три новых метода в `Vector`:
-### Статические методы
-
-> Статические методы: не получают ни `self`, ни `cls`. Декорируются через `@staticmethod`. Используются для вспомогательных функций, логически связанных с классом, но не требующих доступа к его состоянию.
-
-```python-executable
-class MathUtils:
- @staticmethod
- def is_prime(number):
- """Проверяет, является ли число простым"""
- if number < 2:
- return False
- for i in range(2, int(number**0.5) + 1):
- if number % i == 0:
- return False
- return True
-
-# Вызываем статический метод через имя класса
-print(f"Является ли 7 простым числом: {MathUtils.is_prime(7)}")
-# Вывод: Является ли 7 простым числом: True
-print(f"Является ли 10 простым числом: {MathUtils.is_prime(10)}")
-# Вывод: Является ли 10 простым числом: False
-```
-
-### Специальные методы
-
-> Специальные методы: методы с двойными подчёркиваниями в имени (`__init__`, `__str__`, `__add__`, `__eq__`). Python вызывает их сам при определённых операциях: создание объекта, печать, сложение, сравнение.
-
-```python-executable
+```python
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
- # Строковое представление: вызывается через print() и str()
def __str__(self):
return f"Vector({self.x}, {self.y})"
- # Перегрузка оператора +
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
- # Перегрузка оператора ==
def __eq__(self, other):
return self.x == other.x and self.y == other.y
-# Создаём векторы и используем перегруженные операторы
v1 = Vector(3, 4)
v2 = Vector(1, 2)
v3 = Vector(3, 4)
-print(f"v1 = {v1}") # вызывает __str__
-# Вывод: v1 = Vector(3, 4)
-print(f"v1 + v2 = {v1 + v2}") # вызывает __add__
-# Вывод: v1 + v2 = Vector(4, 6)
-print(v1 == v2) # вызывает __eq__
-# Вывод: False
-print(v1 == v3) # вызывает __eq__
-# Вывод: True
+print(v1)
+
+print(v1 + v2)
+
+print(v1 == v3)
+
```
-Без `__eq__` сравнение `v1 == v3` вернуло бы `False`, даже если координаты совпадают: Python по умолчанию сравнивает объекты по идентичности (один ли это объект в памяти), а не по содержимому. Определив `__eq__`, мы говорим: «считай эти объекты равными, если совпадают координаты».
+Обратите внимание на `__add__`: он не меняет исходные векторы, а собирает из их координат новый объект и возвращает его.
+
+Спецметодов в Python десятки: `__len__` научит объект отвечать на `len(...)`, `__repr__` — показывать себя в отладке. Учить их списком не нужно — они будут встречаться по курсу по мере надобности.
-## Часто используемые специальные методы
+## Проверка понимания
-Вот некоторые из наиболее часто используемых специальных методов в Python:
+**Программа создала двух студентов. Что напечатает последняя строка?**
-| Метод | Описание | Пример использования |
-| ------------------------------- | ---------------------------------------- | -------------------- |
-| `__init__(self, ...)` | Конструктор | `obj = MyClass()` |
-| `__str__(self)` | Строковое представление для пользователя | `print(obj)` |
-| `__repr__(self)` | Строковое представление для разработчика | `repr(obj)` |
-| `__len__(self)` | Длина объекта | `len(obj)` |
-| `__getitem__(self, key)` | Доступ по ключу/индексу | `obj[key]` |
-| `__setitem__(self, key, value)` | Присваивание по ключу/индексу | `obj[key] = value` |
-| `__call__(self, ...)` | Вызов объекта как функции | `obj()` |
-| `__add__(self, other)` | Оператор + | `obj + other` |
-| `__sub__(self, other)` | Оператор - | `obj - other` |
-| `__eq__(self, other)` | Оператор == | `obj == other` |
-| `__lt__(self, other)` | Оператор `<` | `obj < other` |
+```python
+class Student:
+ def __init__(self, name, grades=[]):
+ self.name = name
+ self.grades = grades
-## Что дальше?
+s1 = Student("Анна")
+s1.grades.append(5)
-В следующем уроке возьмём четыре принципа ООП по одному. Начнём с наследования: как один класс продолжает другой, переиспользуя его атрибуты и методы.
+s2 = Student("Иван")
+print(s2.grades)
+```
----
+1. \[] — Список из значения по умолчанию создаётся не при каждом вызове, а один раз: s2 получил тот же список, куда уже добавили 5.
+
+2. **Правильный ответ:** \[5] — Значение по умолчанию вычислено один раз при определении \_\_init\_\_, поэтому s1.grades и s2.grades — один и тот же список.
+
+3. Ошибка: у s2 нет атрибута grades. — Атрибут есть: \_\_init\_\_ сохранил в self.grades список из значения по умолчанию.
-**Какой декоратор используется для создания методов класса в Python?**
+4. \[5, 5] — Пятёрку добавляли один раз, так что в списке одно значение — хоть список и общий.
+В следующем уроке разберём наследование: как один класс продолжает другой, переиспользуя его атрибуты и методы.
diff --git a/course/module-2/built-in-libraries/article.md b/course/module-2/built-in-libraries/article.md
index 0caa09e..a082afc 100644
--- a/course/module-2/built-in-libraries/article.md
+++ b/course/module-2/built-in-libraries/article.md
@@ -1,26 +1,22 @@
-# Встроенные библиотеки Python
+---
+meta:
+ title: "Встроенные библиотеки Python"
+ description: "Обзор наиболее полезных встроенных библиотек Python, их функционал и практические примеры использования."
+---
-Представьте, что вы получили новый смартфон. Сразу после покупки на нём уже установлены календарь, калькулятор, камера и другие полезные приложения. Встроенные библиотеки Python работают похожим образом! 🧰
+# Встроенные библиотеки Python
-Встроенные библиотеки — это модули, которые поставляются вместе с Python и доступны сразу после его установки. Они предоставляют готовые решения для самых распространённых задач программирования.
+Посчитать факториал, узнать сегодняшнюю дату, перемешать список, прочитать JSON — для всего этого не нужно ничего устанавливать. Оно уже есть в Python из коробки: десятки готовых модулей доступны сразу после `import`, без `pip install`. Это и есть стандартная (встроенная) библиотека.
## Что такое встроенные библиотеки?
> Встроенные библиотеки (стандартная библиотека) — это набор модулей, которые включены в дистрибутив Python и могут использоваться без дополнительной установки.
-Стандартная библиотека Python включает:
-
-- Модули для работы с системой
-- Инструменты для обработки данных
-- Утилиты для работы с сетью
-- Средства для создания пользовательских интерфейсов
-- И многое другое
-
## Основные встроенные библиотеки
-Давайте рассмотрим несколько наиболее полезных встроенных библиотек Python.
+Пройдёмся по нескольким самым полезным встроенным библиотекам Python.
-### math — математические функции
+### math: математические функции
Модуль `math` предоставляет доступ к математическим функциям, определенным в стандарте языка C:
@@ -29,19 +25,37 @@ import math
# Константы
print(f"Число π: {math.pi}")
+
print(f"Число e: {math.e}")
+
# Тригонометрические функции
angle = math.pi / 4 # 45 градусов в радианах
print(f"Синус 45°: {math.sin(angle):.4f}")
+
print(f"Косинус 45°: {math.cos(angle):.4f}")
+
# Другие функции
print(f"Факториал 5: {math.factorial(5)}")
+
print(f"Наибольший общий делитель 12 и 18: {math.gcd(12, 18)}")
+
```
-### random — генерация случайных чисел
+### random: генерация случайных чисел
Модуль `random` предоставляет функции для генерации случайных чисел и выбора случайных элементов:
@@ -50,124 +64,92 @@ import random
# Генерация случайного целого числа в диапазоне
print(f"Случайное число от 1 до 10: {random.randint(1, 10)}")
+
# Случайное число с плавающей точкой от 0 до 1
print(f"Случайное число от 0 до 1: {random.random():.4f}")
+
# Выбор случайного элемента из последовательности
fruits = ["яблоко", "банан", "апельсин", "груша"]
print(f"Случайный фрукт: {random.choice(fruits)}")
+
# Перемешивание последовательности
numbers = [1, 2, 3, 4, 5]
random.shuffle(numbers)
print(f"Перемешанные числа: {numbers}")
+
```
-### datetime — работа с датами и временем
+### datetime: даты и время
-Модуль `datetime` предоставляет классы для работы с датами и временем:
+Модуль `datetime` умеет разбирать даты из строк, складывать их с интервалами и форматировать обратно. Этого набора хватает для типовых операций с датами:
```python
-import datetime
-
-# Текущая дата и время
-now = datetime.datetime.now()
-print(f"Текущая дата и время: {now}")
+from datetime import datetime, timedelta
-# Создание конкретной даты
-specific_date = datetime.date(2023, 12, 31)
-print(f"Заданная дата: {specific_date}")
+# разобрать строку в дату по шаблону "день.месяц.год"
+d = datetime.strptime("31.12.2022", "%d.%m.%Y")
-# Разница между датами
-today = datetime.date.today()
-new_year = datetime.date(today.year + 1, 1, 1)
-days_until_new_year = (new_year - today).days
-print(f"Дней до Нового года: {days_until_new_year}")
+# прибавить интервал
+new_d = d + timedelta(days=5)
-# Форматирование даты
-formatted_date = now.strftime("%d.%m.%Y %H:%M")
-print(f"Отформатированная дата: {formatted_date}")
+# отформатировать дату обратно в строку
+print(new_d.strftime("%d.%m.%Y"))
+
```
-### os — взаимодействие с операционной системой
+Это лишь верхушка айсберга — у `datetime` куда больше возможностей, и им посвящена отдельная глава «Работа с датой и временем» дальше в модуле.
-Модуль `os` предоставляет функции для взаимодействия с операционной системой:
+Ещё две большие темы стандартной библиотеки вынесены в свои главы, поэтому здесь только назовём модули:
-```python
-import os
-
-# Получение текущей рабочей директории
-print(f"Текущая директория: {os.getcwd()}")
+| Что нужно | Модуль | Где разберём |
+| ------------ | --------------- | -------------------- |
+| Файлы и пути | `os`, `pathlib` | «Работа с файлами» |
+| JSON и CSV | `json`, `csv` | «Форматы JSON и CSV» |
-# Список файлов и папок в директории
-files = os.listdir('.')
-print(f"Первые 3 файла в текущей директории: {files[:3]}")
+А вот `collections` в отдельную главу не попал, поэтому посмотрим его прямо сейчас.
-# Информация о системе
-print(f"Имя операционной системы: {os.name}")
+### collections: специализированные типы данных
-# Существует ли файл или директория
-file_exists = os.path.exists('example.txt')
-print(f"Файл example.txt существует: {file_exists}")
-```
+Модуль `collections` предоставляет несколько удобных структур данных поверх базовых. Один из самых полезных — `Counter` для подсчёта элементов:
-### json — работа с форматом JSON
+```python
+from collections import Counter
-Модуль `json` предоставляет функции для работы с данными в формате JSON:
+orders = ["яблоко", "банан", "яблоко", "вишня", "яблоко", "банан"]
+counts = Counter(orders)
-```python
-import json
-
-# Словарь Python
-person = {
- "name": "Иван",
- "age": 30,
- "city": "Москва",
- "languages": ["Python", "JavaScript", "SQL"]
-}
-
-# Преобразование словаря в строку JSON
-person_json = json.dumps(person, ensure_ascii=False, indent=4)
-print("JSON строка:")
-print(person_json)
-
-# Преобразование строки JSON в объект Python
-json_string = '{"name": "Мария", "age": 25, "city": "Санкт-Петербург"}'
-person_dict = json.loads(json_string)
-print(f"Имя: {person_dict['name']}, Возраст: {person_dict['age']}")
+# most_common() сортирует по убыванию количества
+print(counts.most_common())
+
```
-### collections — специализированные типы данных
+В модуле также есть `defaultdict`, `namedtuple`, `deque` и другие — продвинутые штуки, которые пригодятся позже.
-Модуль `collections` предоставляет альтернативные структуры данных для Python:
+## Проверка понимания
-```python
-from collections import Counter, defaultdict, namedtuple
-
-# Counter - подсчет элементов
-text = "Программирование на Python это интересно и приятно"
-character_count = Counter(text.lower())
-print("Три самых частых символа:")
-for char, count in character_count.most_common(3):
- print(f"'{char}': {count}")
-
-# defaultdict - словарь со значением по умолчанию
-fruit_categories = defaultdict(list)
-fruit_categories["желтые"].append("банан")
-fruit_categories["красные"].append("яблоко")
-fruit_categories["красные"].append("клубника")
-print(f"Желтые фрукты: {fruit_categories['желтые']}")
-print(f"Зеленые фрукты: {fruit_categories['зеленые']}") # Пустой список
-
-# namedtuple - именованные кортежи
-Person = namedtuple('Person', ['name', 'age', 'job'])
-alice = Person('Алиса', 30, 'инженер')
-print(f"{alice.name}, {alice.age} лет, работает как {alice.job}")
-```
+**Какая библиотека лучше всего подходит для работы с датами в Python?**
-## Проверка понимания
+1. math — Библиотека math предназначена для математических вычислений, а не для работы с датами.
-Давайте проверим, насколько хорошо вы усвоили тему встроенных библиотек:
+2. **Правильный ответ:** datetime — Библиотека datetime специально разработана для работы с датами и временем.
-**Какая библиотека лучше всего подходит для работы с датами в Python?**
+3. random — Библиотека random используется для генерации случайных чисел, а не для работы с датами.
+
+4. json — Библиотека json предназначена для работы с форматом JSON, а не для работы с датами.
+
+В следующем уроке посмотрим [сторонние библиотеки](https://python-academy.org/ru/guide/third-party-libraries) — те, что устанавливаются через `pip install` и расширяют возможности Python за пределы стандартной поставки.
diff --git a/course/module-2/classes/article.md b/course/module-2/classes/article.md
index 36888a0..622803a 100644
--- a/course/module-2/classes/article.md
+++ b/course/module-2/classes/article.md
@@ -1,14 +1,82 @@
+---
+meta:
+ title: "Классы и объекты"
+ description: "Собираем первый класс по шагам: __init__, self и методы. Затем глубже: как работает self, изменяемость объектов и динамические атрибуты."
+---
+
# Классы и объекты
-В предыдущем уроке мы разобрали базу: класс это шаблон, объект — заполненный экземпляр, методы получают `self` первым параметром. В этой статье копнём глубже три темы: как **на самом деле** работает `self`, что объекты в Python **изменяемы**, и почему атрибуты можно добавлять **на лету** (и почему этим лучше не злоупотреблять).
+В прошлой главе мы смотрели на класс издалека: форма, шаблон, объекты. Теперь соберём такой класс сами, строчка за строчкой.
+
+## Собираем класс по шагам
+
+Самый короткий класс в Python выглядит так:
+
+```python
+class Person:
+ pass
+
+person = Person()
+print(type(person))
+
+```
+
+`class Person:` объявляет класс, а `Person()` создаёт по нему объект. Правда, пока пустой: никаких данных внутри нет.
+
+Данные закладывают в `__init__` — специальном методе, который Python сам вызывает при каждом `Person(...)`. Аргументы вызова попадают в его параметры:
+
+```python
+class Person:
+ def __init__(self, name, age):
+ self.name = name
+ self.age = age
+
+person = Person("Анна", 25)
+print(person.name)
+
+print(person.age)
+
+```
+
+Строка `self.name = name` читается так: «возьми параметр `name` и сохрани его в объекте под именем `name`». Слева — атрибут объекта, справа — параметр метода.
+
+Это и есть заполнение шаблона со схемы из прошлой главы: вызов `Person("Анна", 25)` заполнил пропуски.
+
+Данные есть, теперь добавим действие. Метод объявляют как обычную функцию, только внутри класса, и первым параметром он всегда принимает `self`:
+
+```python
+class Person:
+ def __init__(self, name, age):
+ self.name = name
+ self.age = age
+
+ def greet(self):
+ return f"Привет, меня зовут {self.name}, мне {self.age} лет."
+
+person = Person("Анна", 25)
+print(person.greet())
+
+```
+
+Заметили странность? В `def greet(self)` параметр объявлен, а в `person.greet()` мы ничего не передаём. Откуда же `self` берётся?
## Как работает self
-Когда вы пишете `person.greet()`, Python внутри превращает это в `Person.greet(person)`. То есть объект слева от точки автоматически становится первым аргументом метода — тем самым `self`.
+Метод `greet` записан в классе один раз, а объектов может быть сколько угодно. Значит, при вызове методу нужно как-то узнать, **чей** `name` печатать. Эту работу делает точка: вызов `person.greet()` Python выполняет как `Person.greet(person)` — объект слева от точки сам становится первым аргументом. Он и приходит в параметр `self`.
+
+`person.greet()` — Python выполняет как → `Person.greet(person)`; объект слева от точки становится первым аргументом и приходит в `self`.
-Через `self` метод видит собственные данные:
+Проверим, что это буквально так:
-```python-executable
+```python
class Person:
def __init__(self, name, age):
self.name = name
@@ -21,16 +89,20 @@ person = Person("Анна", 25)
# Эти два вызова делают одно и то же:
print(person.greet())
-# Вывод: Привет, меня зовут Анна, мне 25 лет.
+
print(Person.greet(person))
-# Вывод: Привет, меня зовут Анна, мне 25 лет.
+
```
-`self` это не keyword и не магия. Это просто имя первого параметра метода по соглашению. Технически можно назвать как угодно (`def greet(this):` тоже сработает), но Python-сообщество ожидает `self`, и линтеры будут ругаться на другие имена.
+Раз `self` — обычный первый параметр, имя ему можно дать любое: `def greet(this):` тоже сработает. Но всё Python-сообщество пишет `self`, и линтеры ругаются на другие имена.
Через `self` методы могут вызывать другие методы того же объекта:
-```python-executable
+```python
class Person:
def __init__(self, name, age):
self.name = name
@@ -45,14 +117,16 @@ class Person:
person = Person("Анна", 25)
print(person.describe())
-# Вывод: Анна: взрослый
+
```
## Объекты в Python изменяемы
-После создания объекта его состояние можно менять: вызывать методы, которые модифицируют атрибуты, или присваивать атрибутам новые значения напрямую.
+До сих пор методы только читали данные: `is_adult` смотрел на `self.age` и ничего не менял. Но состояние объекта можно и менять: методом, как `add_grade` ниже, или присваиванием атрибуту напрямую.
-```python-executable
+```python
class Student:
def __init__(self, name):
self.name = name
@@ -65,30 +139,40 @@ class Student:
def average_grade(self):
if not self.grades:
return "Нет оценок"
- return sum(self.grades) / len(self.grades)
+ return round(sum(self.grades) / len(self.grades), 1)
student = Student("Мария")
print(f"Средний балл: {student.average_grade()}")
-# Вывод: Средний балл: Нет оценок
+
print(student.add_grade(5))
-# Вывод: Добавлена оценка: 5
+
print(student.add_grade(4))
-# Вывод: Добавлена оценка: 4
+
print(student.add_grade(5))
-# Вывод: Добавлена оценка: 5
+
print(f"Средний балл: {student.average_grade()}")
-# Вывод: Средний балл: 4.666666666666667
+
```
-Метод `add_grade` модифицирует `self.grades` — список, хранящийся в объекте. Изменения происходят **на месте**: следующий вызов `student.average_grade()` видит обновлённое состояние. Это не «верни новый список», а «измени существующий».
+Метод `add_grade` меняет `self.grades` — список, хранящийся в объекте. Изменения происходят **на месте**: следующий вызов `student.average_grade()` видит обновлённое состояние. Это не «верни новый список», а «измени существующий».
## Динамические атрибуты
-В Python объекту можно добавить **любой** атрибут в любой момент, даже если он не объявлен в `__init__`:
+Менять существующие атрибуты — обычное дело. Python разрешает больше: объекту можно добавить **любой** новый атрибут в любой момент, даже не объявленный в `__init__`. Проверим на упрощённом `Student`, у которого есть только имя:
-```python-executable
+```python
class Student:
def __init__(self, name):
self.name = name
@@ -98,18 +182,22 @@ student.age = 19 # добавили новый атрибут на л
student.favorite_color = "синий"
print(student.age)
-# Вывод: 19
+
print(student.favorite_color)
-# Вывод: синий
+
```
-Технически это работает, но в реальном коде так почти не пишут. Несколько причин:
+Технически это работает, но в реальном коде так почти не пишут, и вот почему:
-- **Состояние объекта становится непредсказуемым.** Глядя на класс `Student`, нельзя понять, какие атрибуты в действительности есть у объекта.
-- **IDE и линтеры не помогут** с автодополнением: они знают только то, что объявлено в `__init__`.
-- **При опечатке создастся новый атрибут** вместо понятной ошибки. Если вы напишете `student.aeg = 19` вместо `student.age = 19`, Python молча создаст новое поле `aeg`, и баг сложно найти.
+- **Состояние объекта становится непредсказуемым.** Глядя на класс `Student`, нельзя понять, какие атрибуты в действительности есть у объекта.
+- **IDE и линтеры не помогут** с автодополнением: они знают только то, что объявлено в `__init__`.
+- **При опечатке создастся новый атрибут** вместо понятной ошибки. Если вы напишете `student.aeg = 19` вместо `student.age = 19`, Python молча создаст новое поле `aeg`, и баг сложно найти.
-Правило простое: все атрибуты объекта объявляйте в `__init__`, даже со значением `None`, если они появятся позже:
+Поэтому все атрибуты объекта объявляйте в `__init__` — даже со значением `None`, если они появятся позже:
```python
class Student:
@@ -121,11 +209,16 @@ class Student:
Так класс честно описывает, какие поля есть у объекта, и опечатки сразу превращаются в `AttributeError`.
-## Что дальше?
+## Проверка понимания
-В следующей статье разберём атрибуты подробнее: разницу между атрибутами экземпляра и класса, и классическую ловушку с mutable-defaults в `__init__`.
+**Что произойдёт при выполнении `Person.greet(person)`, если `greet` определён с `self` как первый параметр?**
----
+1. Ошибка: нельзя вызывать метод через имя класса. — Вызывать метод через имя класса можно. Просто нужно передать объект первым аргументом вручную.
-**Что произойдёт при выполнении `Person.greet(person)`, если `greet` определён с `self` как первый параметр?**
+2. **Правильный ответ:** То же самое, что и person.greet() — person станет self. — Запись person.greet() Python выполняет как Person.greet(person): объект слева от точки сам передаётся первым аргументом и приходит в self.
+
+3. Метод вызовется, но self будет None. — self получит ровно то, что передано первым аргументом — в данном случае объект person.
+
+4. Создастся новый объект Person. — Объект создаётся только через Person() (вызов конструктора), а не через вызов метода.
+В следующей статье разберём атрибуты подробнее: чем атрибуты экземпляра отличаются от атрибутов класса и как не наступить на классическую ловушку с изменяемыми значениями по умолчанию в `__init__`.
diff --git a/course/module-2/datetime-handling/article.md b/course/module-2/datetime-handling/article.md
index d42bffa..dfe1042 100644
--- a/course/module-2/datetime-handling/article.md
+++ b/course/module-2/datetime-handling/article.md
@@ -1,3 +1,9 @@
+---
+meta:
+ title: "Работа с датой и временем в Python"
+ description: "Модуль datetime: три класса, арифметика через timedelta, strftime и strptime, ISO 8601 и часовые пояса zoneinfo."
+---
+
# Работа с датой и временем в Python
Допустим, у нас лог-файл со строками вида `2026-05-20 14:30:42 ERROR ...`, и нужно посчитать, сколько ошибок произошло за последние сутки. Для этого нужно: разобрать строку с датой в объект, посчитать разницу с «сейчас», и (если показывать пользователю) вернуть в его часовом поясе и привычном формате.
@@ -6,48 +12,62 @@
## Три класса: datetime, date, time
-В модуле `datetime` есть три основных класса, и важно понимать когда какой использовать:
+В модуле `datetime` есть три основных класса, и важно понимать, когда какой использовать:
-- **`datetime`**: конкретный момент времени с точностью до микросекунд. Используется в 90% случаев.
-- **`date`**: только дата, без времени. День рождения, дедлайн, дата заказа.
-- **`time`**: только время суток, без даты. Время встречи в календаре.
+- **`datetime`**: конкретный момент времени с точностью до микросекунд. Используется в 90% случаев.
+- **`date`**: только дата, без времени. День рождения, дедлайн, дата заказа.
+- **`time`**: только время суток, без даты. Время встречи в календаре.
-```python-executable
+```python
from datetime import datetime, date, time
# Момент времени
now = datetime.now()
print(now)
-# Вывод: 2026-05-20 14:30:25.123456
+
# Только дата
birthday = date(1990, 5, 15)
print(birthday)
-# Вывод: 1990-05-15
+
# Только время
meeting = time(14, 30)
print(meeting)
-# Вывод: 14:30:00
+
```
-У всех трёх классов есть отдельные атрибуты компонентов: `.year`, `.month`, `.day`, `.hour`, `.minute`, `.second`, `.microsecond`.
+У каждого класса свои атрибуты компонентов:
-```python-executable
+- у `date` — `.year`, `.month`, `.day`;
+- у `time` — `.hour`, `.minute`, `.second`, `.microsecond`;
+- у `datetime`, который объединяет дату и время, — все сразу.
+
+```python
from datetime import datetime
now = datetime(2026, 5, 20, 14, 30)
print(now.year, now.month, now.day)
-# Вывод: 2026 5 20
+
print(now.weekday()) # 0 = понедельник, 6 = воскресенье
-# Вывод: 2
+
```
## Арифметика дат: timedelta
-Прибавлять и вычитать даты можно напрямую: результат это `timedelta` (промежуток времени), либо новый `datetime`:
+Прибавлять и вычитать даты можно напрямую: результат — `timedelta` (промежуток времени) либо новый `datetime`:
-```python-executable
+```python
from datetime import datetime, timedelta
now = datetime(2026, 5, 20, 14, 30)
@@ -55,64 +75,96 @@ now = datetime(2026, 5, 20, 14, 30)
# Прибавить интервал
week_later = now + timedelta(days=7)
print(week_later)
-# Вывод: 2026-05-27 14:30:00
+
-# Разница между моментами это timedelta
+# Разница между моментами — timedelta
deadline = datetime(2026, 6, 1)
delta = deadline - now
print(delta)
-# Вывод: 11 days, 9:30:00
+
print(delta.days, delta.total_seconds())
-# Вывод: 11 985800.0
+
```
`timedelta` принимает `days`, `hours`, `minutes`, `seconds`, `weeks`, но **не** `months` и `years` (потому что они переменной длины: в феврале 28 или 29 дней, в году 365 или 366). Для прибавления месяцев есть библиотека `dateutil` (внешняя).
## strftime и strptime: между объектом и строкой
-В реальном коде даты постоянно проходят через строки — API, логи, базы данных. Запомнить какой метод что делает помогает мнемоника:
+В реальном коде даты постоянно проходят через строки — API, логи, базы данных. Запомнить, какой метод что делает, помогает мнемоника:
+
+- **`strftime`** — **f**ormat: объект → строка
+- **`strptime`** — **p**arse: строка → объект
-- **`strftime`** — **f**ormat: объект → строка
-- **`strptime`** — **p**arse: строка → объект
+`"2026-05-20 14:30"` → `strptime` → datetime-объект → `strftime` → `"20.05.2026 14:30"`.
-```python-executable
+```python
from datetime import datetime
# Объект → строка
now = datetime(2026, 5, 20, 14, 30)
print(now.strftime("%d.%m.%Y %H:%M"))
-# Вывод: 20.05.2026 14:30
+
print(now.strftime("%A, %d %B %Y"))
-# Вывод: Wednesday, 20 May 2026
+
# Строка → объект
parsed = datetime.strptime("20.05.2026 14:30", "%d.%m.%Y %H:%M")
print(parsed)
-# Вывод: 2026-05-20 14:30:00
+
```
Формат описывается строкой с директивами вида `%Y`, `%m`, `%d`:
-| Директива | Описание | Пример |
-| --------- | ------------------------------ | ------- |
-| `%Y` | Год, 4 цифры | 2026 |
-| `%y` | Год, 2 цифры | 26 |
-| `%m` | Месяц (01-12) | 05 |
-| `%B` | Полное название месяца | January |
-| `%b` | Сокращённое название месяца | Jan |
-| `%d` | День месяца (01-31) | 20 |
-| `%A` | Полное название дня недели | Wednesday|
-| `%a` | Сокращённое название дня | Wed |
-| `%H` | Час (00-23) | 14 |
-| `%I` | Час (01-12) | 02 |
-| `%M` | Минуты (00-59) | 30 |
-| `%S` | Секунды (00-59) | 25 |
+| Директива | Описание | Пример |
+| --------- | -------------------------- | --------- |
+| `%Y` | Год, 4 цифры | 2026 |
+| `%m` | Месяц (01-12) | 05 |
+| `%B` | Полное название месяца | May |
+| `%d` | День месяца (01-31) | 20 |
+| `%A` | Полное название дня недели | Wednesday |
+| `%H` | Час (00-23) | 14 |
+| `%M` | Минуты (00-59) | 30 |
+| `%S` | Секунды (00-59) | 42 |
+
+Соберём пример из вступления: берём строку лога, превращаем дату в объект и проверяем, случилась ли ошибка за последние сутки:
+
+```python
+from datetime import datetime, timedelta
+
+line = "2026-05-20 14:30:42 ERROR disk full"
+
+logged_at = datetime.strptime(line[:19], "%Y-%m-%d %H:%M:%S")
+now = datetime(2026, 5, 21, 10, 0)
+
+print(now - logged_at)
+
+print(now - logged_at < timedelta(days=1))
+
+```
+
+Срез `line[:19]` отрезает первые 19 символов — ровно дату со временем, без хвоста сообщения.
## ISO 8601: стандарт обмена датами
Если дату нужно передать между системами (API, JSON, базы данных), используется **ISO 8601**: `2026-05-20T14:30:00`. У `datetime` есть готовые методы для этого формата, и они быстрее и надёжнее, чем `strftime`/`strptime`:
-```python-executable
+```python
from datetime import datetime
now = datetime(2026, 5, 20, 14, 30)
@@ -120,23 +172,27 @@ now = datetime(2026, 5, 20, 14, 30)
# В ISO-строку
iso_string = now.isoformat()
print(iso_string)
-# Вывод: 2026-05-20T14:30:00
+
# Обратно
parsed = datetime.fromisoformat("2026-05-20T14:30:00")
print(parsed)
-# Вывод: 2026-05-20 14:30:00
+
```
Правило: внутри программы держите даты как `datetime`-объекты, при выводе наружу (в JSON, в базу) — `.isoformat()`, при чтении снаружи — `fromisoformat()`. Свой формат с `strftime` нужен только когда мы показываем дату пользователю.
## Часовые пояса: naive vs aware
-`datetime.now()` без аргумента возвращает «наивный» (naive) datetime — у него нет информации о часовом поясе. Это распространённая грабля: программа работает «дома», а на сервере в другой стране внезапно показывает время на 7 часов раньше.
+`datetime.now()` без аргумента возвращает «наивный» (naive) datetime — у него нет информации о часовом поясе. Это частая ловушка: программа работает «дома», а на сервере в другой стране внезапно показывает время на 7 часов раньше.
-Правильно работать с **aware** datetime, у которого часовой пояс есть. С Python 3.9 для этого есть модуль `zoneinfo`, который умеет в реальные часовые пояса с учётом перехода на летнее время:
+Правильно работать с **aware** («знающим» о поясе) datetime, у которого часовой пояс есть. С Python 3.9 для этого есть модуль `zoneinfo`, который поддерживает настоящие часовые пояса с учётом перехода на летнее время:
-```python-executable
+```python
from datetime import datetime
from zoneinfo import ZoneInfo
@@ -145,9 +201,13 @@ utc_now = datetime(2026, 5, 20, 14, 30, tzinfo=ZoneInfo("UTC"))
moscow_now = utc_now.astimezone(ZoneInfo("Europe/Moscow"))
print(utc_now)
-# Вывод: 2026-05-20 14:30:00+00:00
+
print(moscow_now)
-# Вывод: 2026-05-20 17:30:00+03:00
+
```
Названия зон стандартизированы (IANA tz database): `"Europe/Moscow"`, `"America/New_York"`, `"Asia/Tokyo"`. Когда вы храните `datetime` в базе данных, типичная практика: всегда хранить в UTC, конвертировать в локальную зону пользователя только при выводе.
@@ -158,29 +218,39 @@ print(moscow_now)
Помимо `datetime`, есть низкоуровневый модуль `time`. Два его метода встречаются часто:
-```python-executable
+```python
import time
# Текущий момент как Unix timestamp (секунд с 1 января 1970)
print(time.time())
-# Вывод: 1779373825.123456
+
# Пауза на N секунд
print("Начало")
time.sleep(0.1)
print("Прошло 0.1 секунды")
-# Вывод:
-# Начало
-# Прошло 0.1 секунды
+
```
-`time.time()` возвращает Unix timestamp — это формат, в котором часто хранят время в базах и логах: одно число, не зависит от часового пояса. Перевести в `datetime` можно через `datetime.fromtimestamp(ts, tz=ZoneInfo("UTC"))`.
+`time.time()` возвращает Unix timestamp — это формат, в котором часто хранят время в базах и логах: одно число, не зависит от часового пояса. Перевести его обратно в `datetime` умеет метод `fromtimestamp` — ему передают это число и часовой пояс.
-## Что дальше?
+## Проверка понимания
-В дальнейшем `datetime` будет встречаться часто: при работе с API (даты приходят строкой ISO 8601), с базами данных (хранение моментов событий), при логировании. Главное правило: внутри программы держите даты как объекты, превращайте в строки только на границе с внешним миром.
+**Какой класс из модуля datetime используется для представления промежутка времени?**
----
+1. datetime — Класс datetime представляет конкретный момент времени, а не промежуток.
-**Какой класс из модуля datetime используется для представления промежутка времени?**
+2. date — Класс date представляет только дату без времени.
+
+3. time — Класс time представляет только время без даты.
+
+4. **Правильный ответ:** timedelta — timedelta представляет промежуток времени и используется для арифметики с датами: разница между датами это timedelta, и timedelta можно прибавить к datetime, чтобы получить новый момент времени.
+
+В дальнейшем `datetime` будет встречаться часто: при работе с API (даты приходят строкой ISO 8601), с базами данных (хранение моментов событий), при логировании. Главное правило: внутри программы держите даты как объекты, превращайте в строки только на границе с внешним миром.
+В следующем уроке — последняя глава модуля: аннотации типов, подсказки о том, какие значения функция принимает и возвращает.
diff --git a/course/module-2/decorators/article.md b/course/module-2/decorators/article.md
index a70c1e4..2a2a5d0 100644
--- a/course/module-2/decorators/article.md
+++ b/course/module-2/decorators/article.md
@@ -1,3 +1,9 @@
+---
+meta:
+ title: "Декораторы в Python"
+ description: "Как устроены декораторы: обёртка вокруг функции, *args и **kwargs, functools.wraps и декораторы с параметрами."
+---
+
# Декораторы в Python
Допустим, у нас есть несколько функций и мы хотим во всех замерить время выполнения. Можно скопировать код тайминга в каждую:
@@ -7,24 +13,41 @@ def slow_function():
start = time.time()
# ... основная работа
elapsed = time.time() - start
- print(f"slow_function: {elapsed:.4f} c")
+ print(f"slow_function: {round(elapsed, 4)} c")
def another_function():
start = time.time()
# ... основная работа
elapsed = time.time() - start
- print(f"another_function: {elapsed:.4f} c")
+ print(f"another_function: {round(elapsed, 4)} c")
```
Работает, но через неделю мы захотим поменять формат логирования, и придётся редактировать **все** функции по очереди. Плюс основная логика теряется в служебном коде.
Декораторы решают именно это: вы пишете обёртку один раз, и навешиваете её на любую функцию короткой строкой `@обёртка` над определением.
+## Функция — это значение
+
+Сначала одна ступенька, без которой декораторы не понять. Функция в Python — такое же значение, как число или строка: её можно положить в переменную, передать в другую функцию и вернуть из функции.
+
+```python
+def say_hello():
+ print("Привет!")
+
+greet = say_hello # без скобок: берём саму функцию, а не её результат
+greet()
+
+```
+
+`say_hello` без скобок — сама функция, со скобками — её вызов. Передавать функцию вы уже пробовали в прошлом уроке: `sorted(key=lambda ...)` — это функция, отданная в другую функцию. Осталось третье: функцию можно объявить внутри другой и вернуть наружу — именно так декоратор и устроен.
+
## Базовый синтаксис
Декоратор — функция, которая берёт другую функцию и возвращает её «обёрнутую» версию (с добавленным поведением):
-```python-executable
+```python
def my_decorator(func):
def wrapper():
print("До вызова функции")
@@ -37,25 +60,34 @@ def say_hello():
print("Привет, мир!")
say_hello()
-# Вывод:
-# До вызова функции
-# Привет, мир!
-# После вызова функции
+
```
-Запись `@my_decorator` над `say_hello` это сахар над таким эквивалентом:
+Запись `@my_decorator` над `say_hello` — это сокращение для такой строки:
```python
say_hello = my_decorator(say_hello)
```
-То есть мы переопределяем `say_hello` на новую функцию (которую вернул декоратор). Никакой магии, обычное переприсваивание.
+То есть имя `say_hello` теперь указывает на новую функцию, которую вернул декоратор, — обычное переприсваивание.
+
+`say_hello()` → `@my_decorator` → `wrapper()`:
+
+1. `print("До вызова")`
+2. `say_hello()`
+3. `print("После вызова")`
+
+Декоратор вернул новую функцию: внутри — исходная плюс код до и после
## Декоратор с аргументами функции
Если оборачиваемая функция принимает аргументы, обёртка должна их пробрасывать. Универсальный приём: `*args, **kwargs`:
-```python-executable
+```python
def my_decorator(func):
def wrapper(*args, **kwargs):
print("До вызова")
@@ -69,19 +101,22 @@ def add(a, b):
return a + b
print(add(5, 3))
-# Вывод:
-# До вызова
-# После вызова
-# 8
+
```
`*args, **kwargs` означает «приму любые позиционные и именованные аргументы», и `func(*args, **kwargs)` пробрасывает их дальше. Этим приёмом декоратор становится универсальным — работает с любой функцией.
+Заметьте порядок вывода: `8` печатается последним, потому что внешний `print` ждёт, пока обёртка отработает целиком и вернёт результат.
+
## Практический декоратор: тайминг
-Тот самый тайминг, ради которого мы всё начали:
+Тот самый тайминг, ради которого мы всё начали. Одна деталь: у каждой функции есть атрибут `__name__` с её именем — обёртка подпишет им замер:
-```python-executable
+```python
import time
def timing(func):
@@ -89,7 +124,7 @@ def timing(func):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
- print(f"{func.__name__}: {elapsed:.4f} c")
+ print(f"{func.__name__}: {round(elapsed, 4)} c")
return result
return wrapper
@@ -98,16 +133,18 @@ def calculate_sum(n):
return sum(range(n))
calculate_sum(1_000_000)
-# Вывод: calculate_sum: 0.0462 c
+
```
-Теперь добавить тайминг к любой функции это одна строка `@timing` сверху. Захотим поменять формат вывода: правим одну функцию `timing`, а не каждую функцию в проекте.
+Теперь добавить тайминг к любой функции — одна строка `@timing` сверху. Захотим поменять формат вывода: правим одну функцию `timing`, а не каждую функцию в проекте.
-## functools.wraps: сохранение имени и docstring
+## functools.wraps: сохранение имени функции
-У наивного декоратора есть незаметный побочный эффект: обёрнутая функция «теряет» своё имя и документацию, потому что снаружи вы видите уже `wrapper`, а не оригинал:
+У наивного декоратора есть незаметный побочный эффект: обёрнутая функция «теряет» своё имя, потому что снаружи вы видите уже `wrapper`, а не оригинал:
-```python-executable
+```python
def timing(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
@@ -115,18 +152,17 @@ def timing(func):
@timing
def calculate_sum(n):
- """Считает сумму чисел от 0 до n."""
return sum(range(n))
print(calculate_sum.__name__)
-# Вывод: wrapper
-print(calculate_sum.__doc__)
-# Вывод: None
+
```
-В реальном коде это ломает отладку, логирование и работу IDE. Лечится одной строкой — декоратором `@functools.wraps(func)` на `wrapper`:
+В реальном коде это ломает отладку и сообщения об ошибках. Лечится одной строкой — декоратором `@functools.wraps(func)` на `wrapper`:
-```python-executable
+```python
from functools import wraps
def timing(func):
@@ -137,22 +173,21 @@ def timing(func):
@timing
def calculate_sum(n):
- """Считает сумму чисел от 0 до n."""
return sum(range(n))
print(calculate_sum.__name__)
-# Вывод: calculate_sum
-print(calculate_sum.__doc__)
-# Вывод: Считает сумму чисел от 0 до n.
+
```
-Правило: пишете свой декоратор, всегда оборачивайте внутреннюю функцию через `@wraps(func)`. Это бесплатно и сохраняет интроспекцию.
+Правило: пишете свой декоратор — всегда оборачивайте внутреннюю функцию через `@wraps(func)`.
## Декоратор с параметрами
Иногда хочется передать настройки самому декоратору, например «повтори вызов N раз». Это требует ещё одного уровня: внешняя функция принимает параметр, внутри возвращает «настоящий» декоратор:
-```python-executable
+```python
from functools import wraps
def repeat(n=1):
@@ -171,10 +206,11 @@ def say_hi(name):
print(f"Привет, {name}!")
say_hi("Анна")
-# Вывод:
-# Привет, Анна!
-# Привет, Анна!
-# Привет, Анна!
+
```
Три уровня вложенности кажутся пугающими, но логика простая:
@@ -183,41 +219,21 @@ say_hi("Анна")
2. `decorator(func)` принимает функцию и возвращает обёртку.
3. `wrapper(*args, **kwargs)` обрабатывает реальный вызов.
-## Цепочка декораторов
-
-Декораторы можно применять несколько. Они применяются **снизу вверх**: ближайший к функции идёт первым:
-
-```python-executable
-def bold(func):
- def wrapper(*args, **kwargs):
- return f"{func(*args, **kwargs)}"
- return wrapper
-
-def italic(func):
- def wrapper(*args, **kwargs):
- return f"{func(*args, **kwargs)}"
- return wrapper
+В развёрнутом виде — тот же приём, что и раньше, только с одним лишним вызовом:
-@bold
-@italic
-def format_text(text):
- return text
-
-print(format_text("Привет, мир!"))
-# Вывод: Привет, мир!
+```python
+say_hi = repeat(n=3)(say_hi)
```
-`@italic` применяется к `format_text` первым, получается «italic-функция». Потом `@bold` оборачивает её снаружи, получается «bold(italic(format_text))». Поэтому сначала закрывается ``, потом ``.
+Сначала выполняется обычный вызов `repeat(n=3)` — он возвращает настоящий декоратор, и тот сразу применяется к `say_hi`.
-## Вы уже встречали декораторы
-
-Когда мы разбирали инкапсуляцию, мы использовали `@property` и `@balance.setter` — это и есть декораторы из стандартной библиотеки. `@property` берёт функцию-геттер и превращает её в «вычисляемый атрибут». Никакой магии: тот же механизм, что мы сейчас разбираем, только применённый в контексте классов.
+Имя `_` в цикле — соглашение: переменная нужна только чтобы цикл крутился, её значение не используется.
## Где декораторы живут в реальности
Несколько мест, где вы их встретите чаще всего:
-**Web-фреймворки (Flask, FastAPI, Django).** Привязка URL к функции-обработчику:
+**Веб-фреймворки.** Привязка адреса страницы к функции-обработчику — например, во Flask или FastAPI:
```python
@app.route('/home')
@@ -225,41 +241,24 @@ def home():
return "Главная страница"
```
-`@app.route` регистрирует функцию в роутере фреймворка — без декоратора пришлось бы вручную писать `app.add_url_rule(...)` для каждого эндпоинта.
-
-**Кэширование.** Сохранение результатов чтобы не пересчитывать одно и то же:
+`@app.route` регистрирует функцию в маршрутизаторе фреймворка: запросил браузер `/home` — фреймворк вызовет `home()`.
-```python-executable
-from functools import wraps
-
-def memoize(func):
- cache = {}
- @wraps(func)
- def wrapper(*args):
- if args not in cache:
- cache[args] = func(*args)
- return cache[args]
- return wrapper
+**Кэширование.** Декоратор `@lru_cache` из `functools` запоминает результаты функции: повторный вызов с теми же аргументами ничего не пересчитывает, а сразу возвращает готовый ответ.
-@memoize
-def fib(n):
- if n < 2:
- return n
- return fib(n - 1) + fib(n - 2)
+**Тесты.** В pytest декораторы `@pytest.fixture` и `@pytest.mark.parametrize` превращают обычную функцию в заготовку данных или целую серию тестов — встретите их в третьем модуле курса.
-print(fib(30))
-# Вывод: 832040
-```
+## Проверка понимания
-Без `@memoize` `fib(30)` пересчитывал бы одно и то же миллион раз и подвисал. С кэшем работает мгновенно. В стандартной библиотеке такой декоратор уже есть: `from functools import lru_cache`.
+**В чём основное назначение декораторов в Python?**
-**Тесты.** В pytest `@pytest.fixture`, `@pytest.mark.parametrize` — это декораторы, которые превращают обычную функцию в фикстуру или параметризованный тест.
+1. **Правильный ответ:** Модифицировать или расширять функции без изменения их исходного кода — Декоратор оборачивает функцию, добавляя поведение до, после или вокруг её вызова, не трогая исходный код самой функции.
-## Что дальше?
+2. Создавать новые переменные и структуры данных — Декораторы работают с функциями, а не с созданием переменных или структур данных.
-Декораторы это рабочая лошадка Python: библиотеки и фреймворки строятся на них (Flask-роуты, pytest-фикстуры, dataclasses, type-checkers). Когда вы понимаете, что `@что-то` это просто `func = что-то(func)`, никакой магии в этих библиотеках больше нет — только композиция функций.
+3. Оптимизировать код Python для более быстрого выполнения — Декораторы могут использоваться для оптимизации (например, кэширование через @lru_cache), но это не их основное назначение.
----
+4. Документировать код и генерировать API-документацию — Декораторы иногда используются для добавления метаданных, но их основная задача функциональная, а не документационная.
-**В чём основное назначение декораторов в Python?**
+Декораторы — рабочая лошадка Python: на них строятся веб-фреймворки, тесты и уже знакомый вам `@property`. Когда вы знаете, что `@что-то` — это просто `func = что-то(func)`, читать такой код становится сильно проще.
+В следующем уроке — работа с датами и временем: модуль `datetime`, разница дат и их форматирование.
diff --git a/course/module-2/encapsulation/article.md b/course/module-2/encapsulation/article.md
index 2c65355..8753538 100644
--- a/course/module-2/encapsulation/article.md
+++ b/course/module-2/encapsulation/article.md
@@ -1,26 +1,46 @@
+---
+meta:
+ title: "Инкапсуляция"
+ description: "Как класс защищает свои данные: соглашение об одном подчёркивании, свойства @property и атрибуты только для чтения."
+---
+
# Инкапсуляция в Python
-Допустим, мы пишем класс банковского счёта:
+Заведём класс-копилку: `deposit` пополняет баланс и не пропускает отрицательную сумму. Проверка на месте — но обойти её проще простого:
```python
class BankAccount:
def __init__(self, balance):
self.balance = balance
+ def deposit(self, amount):
+ if amount > 0:
+ self.balance += amount
+
account = BankAccount(1000)
-print(account.balance) # 1000
-account.balance = -1_000_000 # упс
+
+account.deposit(-500) # проверка не пропустит
+print(account.balance)
+
+
+account.balance = -1_000_000 # а так — пожалуйста
+print(account.balance)
+
```
-Класс не возражает: `balance` — публичный атрибут, и присвоить ему можно что угодно, включая такое, чего не должно быть никогда. Класс не гарантирует свои **инварианты** (правила, которые должны выполняться всегда, например «баланс не отрицательный»).
+Проверка в `deposit` никуда не делась — мы её просто обошли: `balance` — обычный атрибут, и присвоить ему можно что угодно. Класс не может гарантировать свои правила — например, «баланс не бывает отрицательным».
-Инкапсуляция — это идея «у объекта есть **публичный интерфейс** (через что с ним общаются снаружи) и **внутреннее состояние** (внутрь не лазать)». Внешний код вызывает методы, а класс внутри сам следит, чтобы данные оставались валидными.
+Инкапсуляция — ответ на эту проблему: у объекта есть **публичный интерфейс** (то, через что с ним общаются снаружи) и **внутреннее состояние** (внутрь не лазать). Внешний код вызывает методы, а класс внутри сам следит, чтобы данные оставались корректными.
## Соглашение об одном подчёркивании
-В Python нет ключевого слова `private`. Вместо него используется соглашение: атрибут или метод, который начинается с подчёркивания, считается «внутренним», то есть снаружи трогать не следует:
+Как отличить внутреннее от публичного? В Python для этого есть соглашение: атрибут или метод, чьё имя начинается с подчёркивания, считается «внутренним» — снаружи его трогать не следует:
-```python-executable
+```python
class BankAccount:
def __init__(self, balance):
self._balance = balance # подчёркивание = «внутреннее»
@@ -40,18 +60,24 @@ account = BankAccount(1000)
account.deposit(500)
account.withdraw(2000) # запрошено больше баланса, игнорируется
print(account.get_balance())
-# Вывод: 1500
+
```
Снаружи доступны только `deposit`, `withdraw` и `get_balance`: всё, что нужно для работы со счётом. `_balance` менять снаружи **технически** можно (Python не запрещает), но соглашение говорит: не надо, иначе вы обходите проверки класса.
Эту философию в Python-сообществе формулируют так: **«мы все взрослые здесь»** (we're all consenting adults here). Язык не запрещает — он сигналит, что трогать не стоит. Ответственность на программисте.
+внешний код → через методы → `deposit()`, `withdraw()`, `get_balance()`; напрямую → `_balance`. Со счётом работают через методы, а `_balance` класс меняет только сам.
+
## Свойства: атрибут снаружи, метод внутри
-Часто хочется, чтобы снаружи `account.balance` **выглядело** как обычный атрибут, но при этом внутри был метод (например, с проверкой). В Java для этого пишут `getBalance()` и `setBalance()`. В Python для этого есть `@property`:
+Часто хочется, чтобы снаружи `account.balance` **выглядело** как обычный атрибут, но при этом внутри был метод с проверкой. Для этого в Python есть `@property`.
-```python-executable
+Строка с `@` над методом — это **декоратор**: приём, который меняет поведение метода. Декораторам посвящён отдельный урок дальше в курсе; пока достаточно знать, что делают эти два:
+
+```python
class Account:
def __init__(self, balance):
self._balance = balance
@@ -70,81 +96,77 @@ account = Account(1000)
# Используется как обычный атрибут:
print(account.balance)
-# Вывод: 1000
+
account.balance = 500 # вызовет setter с проверкой
print(account.balance)
-# Вывод: 500
+
# Попытка установить отрицательное значение:
try:
account.balance = -100
except ValueError as e:
print(f"Ошибка: {e}")
-# Вывод: Ошибка: Баланс не может быть отрицательным
+
```
-Декоратор `@property` превращает метод в «вычисляемый атрибут». `@balance.setter` определяет, что произойдёт при присваивании. Снаружи всё выглядит как `account.balance = 500`, но внутри класса срабатывает проверка.
+`@property` превращает метод в «вычисляемый атрибут»: снаружи `account.balance` читается без скобок. `@balance.setter` определяет, что произойдёт при присваивании: снаружи всё выглядит как обычное присваивание, но внутри класса срабатывает проверка.
-> В Java и C++ часто пишут `get_x()` и `set_x()` методы. В Python так делать не принято: используйте `@property`.
+В примере встречаются `raise` и `try/except` — это механизм ошибок: `raise` прерывает операцию и сообщает, что значение недопустимо, а `try/except` перехватывает это в вызывающем коде. Им посвящён отдельный урок дальше в курсе; здесь достаточно понимать, что проверка в сеттере не даёт записать некорректное значение.
## Свойство только для чтения
Если у `@property` нет сеттера, атрибут получается доступным только для чтения. Это удобно для **вычисляемых** значений, которые не имеет смысла «устанавливать» извне:
-```python-executable
+```python
import math
class Circle:
def __init__(self, radius):
- self._radius = radius
-
- @property
- def radius(self):
- return self._radius
-
- @radius.setter
- def radius(self, value):
- if value <= 0:
- raise ValueError("Радиус должен быть положительным")
- self._radius = value
+ self.radius = radius
@property
def area(self):
- return math.pi * self._radius ** 2
+ return math.pi * self.radius ** 2
circle = Circle(5)
-print(f"Радиус: {circle.radius}, площадь: {circle.area:.2f}")
-# Вывод: Радиус: 5, площадь: 78.54
-
-# Меняем радиус, площадь пересчитывается автоматически
-circle.radius = 7
-print(f"Радиус: {circle.radius}, площадь: {circle.area:.2f}")
-# Вывод: Радиус: 7, площадь: 153.94
+print(f"Радиус: {circle.radius}, площадь: {round(circle.area, 2)}")
+
# area нельзя присвоить (нет сеттера)
try:
circle.area = 100
except AttributeError as e:
print(f"Ошибка: {e}")
-# Вывод: Ошибка: property 'area' of 'Circle' object has no setter
+
```
-`area` всегда возвращает актуальное значение, и присвоить ему ничего нельзя: «установки площади» не должно быть, она вытекает из радиуса.
+`area` всегда возвращает актуальное значение, а присвоить ему ничего нельзя — и правильно: площадь не хранится отдельно, она вытекает из радиуса.
-## А что насчёт двух подчёркиваний?
+## Что даёт инкапсуляция
-Иногда можно встретить атрибуты с двумя подчёркиваниями в начале: `self.__balance`. Это запускает в Python механизм **name mangling**: атрибут переименовывается внутри объекта в `_ClassName__balance`. Это нужно в редких случаях (например, чтобы атрибут точно не пересёкся с одноимённым атрибутом в дочернем классе) и в обычном коде почти не встречается. По умолчанию используйте одно подчёркивание.
+Главное — класс становится **ответственным за свои данные**. Снаружи невозможно (по соглашению) обойти его проверки и оставить объект с некорректными данными. Если потом нужно поменять способ хранения (например, `_balance` стал словарём с историей операций), внешний код не сломается, потому что он общается с классом через тот же публичный интерфейс — `deposit`, `withdraw`, `balance`.
-## Что даёт инкапсуляция
+## Проверка понимания
-Главное — класс становится **ответственным за свои данные**. Снаружи невозможно (по соглашению) обойти его проверки и оставить объект в невалидном состоянии. Если потом нужно поменять способ хранения (например, `_balance` стал словарём с историей операций), внешний код не сломается, потому что он общается с классом через тот же интерфейс — `deposit`, `withdraw`, `balance`.
+**Как оформить атрибут, который снаружи можно читать, но нельзя менять?**
-## Что дальше?
+1. **Правильный ответ:** @property без сеттера — Снаружи такой атрибут читается как обычный, а при попытке присвоить значение Python выбросит AttributeError.
-В следующем уроке возьмём третий принцип ООП — полиморфизм: как через единый интерфейс работать с объектами разных классов и почему это делает код проще.
+2. Обычный атрибут без подчёркивания — Обычный атрибут может изменить любой код в программе — защиты от записи нет.
----
+3. Атрибут с одним подчёркиванием — Одно подчёркивание — только сигнал «внутреннее, не трогать»: технически менять его снаружи по-прежнему можно.
-**Какой уровень доступа наиболее подходит для атрибута, который должен быть доступен для чтения, но не должен изменяться вне класса?**
+4. Метод get_x() и атрибут \_x — Читать придётся через вызов метода со скобками, а сам \_x останется изменяемым. В Python вместо этого используют @property.
+В следующем уроке возьмём третий принцип ООП — полиморфизм: как через единый интерфейс работать с объектами разных классов и почему это делает код проще.
diff --git a/course/module-2/exception-handling/article.md b/course/module-2/exception-handling/article.md
index 4e6d815..0eb5a6d 100644
--- a/course/module-2/exception-handling/article.md
+++ b/course/module-2/exception-handling/article.md
@@ -1,264 +1,184 @@
-# Обработка исключений в Python
-
-Путешествие по неизвестной территории требует тщательной подготовки — опытный исследователь заранее продумывает возможные опасности, проверяет карту, готовит снаряжение и планирует маршрут. В программировании обработка исключений работает похожим образом — это надёжный инструмент, который позволяет программе грамотно реагировать на непредвиденные ситуации и продолжать работу, вместо того чтобы аварийно завершаться при первой же ошибке. 🧭
-
-## Что такое исключения?
+---
+meta:
+ title: "Обработка исключений в Python"
+ description: "Как перехватывать ошибки через try/except, зачем нужны else и finally и почему не стоит ловить все исключения подряд."
+---
-> Исключения — это события, нарушающие нормальный поток выполнения программы, которые могут быть обработаны специальными конструкциями языка.
+# Обработка исключений в Python
-## Зачем нужна обработка исключений?
+Программа спрашивает у пользователя возраст и прибавляет год:
-Обработка исключений позволяет:
+```python
+age = int(input("Сколько вам лет? "))
+print("Через год будет", age + 1)
+```
-1. **Предотвращать аварийное завершение программы** — ваша программа продолжит работу, даже если что-то пошло не так
-2. **Отделять код обработки ошибок от основной логики** — делает код более читаемым и понятным
-3. **Централизованно обрабатывать разные типы ошибок** — упрощает структуру программы
-4. **Делать программу более устойчивой** — она сможет корректно работать даже в неожиданных ситуациях
+Пока вводят число, всё работает. Но стоит пользователю набрать «сорок» вместо `40`, и `int()` не сможет превратить это в число: программа аварийно завершится с ошибкой `ValueError`, а строка с `print` уже не выполнится. Один неверный ввод роняет всю программу.
-## Блоки try-except
+Обработка исключений нужна как раз для таких моментов: перехватить ошибку, среагировать и продолжить работу, вместо того чтобы падать при первой же неожиданности. Мы дважды откладывали этот разговор — в уроках про текстовые файлы и инкапсуляцию. Пришло время.
-Основной механизм обработки исключений в Python — блок `try-except`. Он позволяет:
+> Исключение — это ошибка, возникшая во время выполнения программы. По умолчанию оно прерывает работу, но его можно перехватить и обработать.
-- Изолировать потенциально опасный код
-- Перехватывать ошибки
-- Выполнять альтернативные действия
-- Продолжать выполнение программы
+## Блок try-except
-### Синтаксис блока try-except
+Рискованный код кладут в `try`, реакцию на ошибку — в `except`:
```python
try:
- # Рискованный код, который может вызвать исключение
+ # код, который может упасть
except ТипИсключения:
- # Код, который выполнится при возникновении указанного исключения
+ # что делать, если упал
```
-Вот простой пример:
+Вот как это выглядит на настоящей ошибке:
```python
try:
- # Код, который может вызвать исключение
- result = 10 / 0 # Деление на ноль вызовет ZeroDivisionError
+ result = 10 / 0
except ZeroDivisionError:
- # Код, который выполнится при возникновении исключения
print("Ошибка: деление на ноль!")
+
```
-### Полная структура блока try-except
+Программа не упала: Python дошёл до деления, увидел ошибку, перепрыгнул в `except` и продолжил работу.
-Полная структура блока try-except включает дополнительные секции:
+## Разные ошибки — разные реакции
-```python
-try:
- # Рискованный код
-except ТипИсключения1:
- # Обработка исключения типа 1
-except ТипИсключения2:
- # Обработка исключения типа 2
-else:
- # Выполняется, если в блоке try не возникло исключений
-finally:
- # Выполняется всегда, независимо от наличия исключений
-```
-
-## Обработка исключений разных типов
-
-### Последовательная обработка разных исключений
-
-Можно обрабатывать разные типы исключений по-разному:
+В одном `try` может падать разное, и на каждый тип исключения заводится свой `except`. Python проверяет их по порядку и выполняет первый подходящий:
```python
+file_name = "data.txt"
+
try:
- # Несколько операций, которые могут вызвать разные исключения
- file_name = input("Введите имя файла: ") # Допустим, введено "data.txt"
file = open(file_name, "r")
- line = file.readline()
- number = int(line.strip())
- result = 100 / number
- print(f"Результат: {result}")
+ number = int(file.readline().strip())
+ print(f"Результат: {100 / number}")
except FileNotFoundError:
print(f"Файл {file_name} не найден")
except ValueError:
- print("Невозможно преобразовать данные в число")
+ print("В файле не число")
except ZeroDivisionError:
- print("Ошибка: деление на ноль")
-except Exception as e:
- print(f"Произошла неизвестная ошибка: {e}")
+ print("В файле ноль — делить нельзя")
+
```
-Важно: Python проверяет блоки `except` в порядке их объявления и выполняет первый подходящий блок.
-
-### Обработка нескольких исключений одним блоком
-
-Если для разных исключений нужна одинаковая обработка:
+Если реакция на разные ошибки одна и та же, типы перечисляют в скобках через запятую:
```python
try:
value = int("abc")
- result = 10 / 0
except (ValueError, ZeroDivisionError):
print("Произошла ошибка в вычислениях")
+
```
## Блоки else и finally
-### Блок else
-
-Блок `else` выполняется только если в блоке `try` не возникло исключений:
+`else` выполняется, только если в `try` ошибок не было:
```python
try:
- number = int("42") # Это выполнится успешно
+ number = int("42")
except ValueError:
print("Это не число")
else:
- # Этот блок выполнится, так как исключения не было
print(f"Успешно! Число: {number}")
- print(f"Квадрат числа: {number ** 2}")
+
```
-Блок `else` полезен, когда нужно выполнить код только после успешного выполнения рискованной операции.
-
-### Блок finally
-
-Блок `finally` выполняется всегда, независимо от того, произошло исключение или нет:
+`finally` выполняется всегда — была ошибка или нет:
```python
+f = open("example.txt", "w")
try:
- f = open("example.txt", "w")
f.write("Привет, мир!")
- # Предположим, здесь может произойти исключение
-except IOError:
- print("Произошла ошибка ввода-вывода")
finally:
- print("Закрытие файла")
- f.close() # Файл будет закрыт в любом случае
+ f.close() # выполнится, даже если запись упадёт
+ print("Файл закрыт")
+
```
-Блок `finally` обычно используется для:
+Обратите внимание: `open()` стоит до `try`. Если поместить его внутрь и файл не откроется, переменной `f` просто не будет, и `f.close()` в `finally` сам упадёт. Именно эту возню автоматизирует оператор `with` из главы про файлы — он закрывает файл за вас.
-- Освобождения ресурсов (закрытие файлов, соединений с базой данных)
-- Очистки временных данных
-- Логирования завершения операции
+В `finally` кладут то, что должно случиться в любом случае, — чаще всего освобождение ресурсов вроде открытых файлов.
-## Получение информации об исключении
+## Информация об ошибке: as
-Используйте `as` для получения объекта исключения:
+Через `as` можно получить сам объект исключения — например, чтобы показать его сообщение:
```python
try:
result = 10 / 0
except ZeroDivisionError as e:
- print(f"Тип исключения: {type(e).__name__}")
print(f"Сообщение: {e}")
+
```
-## Вызов исключений
+## Свои ошибки: raise
-Вы можете сами вызывать исключения с помощью оператора `raise`:
+Оператор `raise` поднимает исключение вручную. Вы уже видели его в уроке про инкапсуляцию: там сеттер отказывался сохранять отрицательный баланс. Устроено это так:
```python
def check_age(age):
if age < 0:
raise ValueError("Возраст не может быть отрицательным")
- if age < 18:
- print("Вы несовершеннолетний")
- else:
- print("Вы совершеннолетний")
+ print(f"Возраст {age} принят")
try:
check_age(-5)
except ValueError as e:
print(f"Ошибка: {e}")
+
```
-## Создание собственных исключений
+## Не ловите всё подряд
-Можно создавать собственные классы исключений, наследуя их от стандартных:
+Голый `except:` без типа перехватывает любые ошибки — в том числе те, о которых вы не подозревали, например опечатку в имени переменной. Программа «работает», а настоящая проблема молчит:
```python
-# Создаем собственный класс исключения
-class InvalidEmailError(Exception):
- """Вызывается, когда email не соответствует формату"""
- pass
-
-def validate_email(email):
- if "@" not in email:
- raise InvalidEmailError("Email должен содержать символ @")
- print(f"Email {email} корректен")
-
-try:
- validate_email("user.example.com")
-except InvalidEmailError as e:
- print(f"Ошибка валидации: {e}")
-```
-
-## Практические рекомендации
-
-### 1. Определяйте конкретные исключения
-
-```python
-# Плохо:
+# Плохо: непонятно, что именно случилось
try:
number = int("abc")
-except: # Перехватывает все исключения
- print("Ошибка")
+except:
+ print("Какая-то ошибка")
+
-# Хорошо:
+# Хорошо: ловим ровно то, что ожидаем
try:
number = int("abc")
except ValueError:
print("Неверный формат числа")
-```
-
-### 2. Минимизируйте код в блоке try
-
-```python
-# Хорошо:
-try:
- file = open("data.txt", "r")
-except FileNotFoundError:
- print("Файл не найден")
- file = None
-
-if file:
- try:
- content = file.read()
- except:
- print("Ошибка при чтении файла")
- finally:
- file.close()
-```
-
-### 3. Используйте else и finally правильно
-
-```python
-def get_value_from_list(my_list, index):
- try:
- value = my_list[index]
- except IndexError:
- print(f"Индекс {index} вне диапазона")
- return None
- else:
- # Этот код выполнится только если исключения не было
- print(f"Значение успешно получено")
- return value
- finally:
- # Этот код выполнится всегда
- print("Операция доступа к списку завершена")
-
-result = get_value_from_list([1, 2, 3], 1)
-print(f"Результат: {result}")
-
-result = get_value_from_list([1, 2, 3], 10)
-print(f"Результат: {result}")
+
```
## Проверка понимания
**Какой блок выполнится всегда, независимо от того, произошло исключение или нет?**
+1. try — Блок try содержит код, который может вызвать исключение, но не гарантирует выполнение всего кода внутри при возникновении исключения.
+
+2. except — Блок except выполняется только в случае возникновения исключения соответствующего типа.
+
+3. else — Блок else выполняется только если в блоке try не возникло исключений.
-## Заключение
+4. **Правильный ответ:** finally — Блок finally выполняется всегда, произошло исключение или нет. Поэтому в него кладут освобождение ресурсов: закрытие файлов, соединений с базой — то, что должно случиться в любом случае.
-Обработка исключений — это ваш надежный защитник от хаоса в коде. Она превращает потенциальные катастрофы в контролируемые ситуации, позволяя вашим программам изящно справляться с неожиданностями. Правильно используя try-except, else и finally, вы создаете более надежный и устойчивый код. 🛡️
+В следующем уроке — декораторы. Вы уже встречали их дважды: `@property` в инкапсуляции и `@abstractmethod` в полиморфизме. Пришло время разобрать, как строка с `@` устроена изнутри.
diff --git a/course/module-2/files/article.md b/course/module-2/files/article.md
index 496b9a1..374e2d0 100644
--- a/course/module-2/files/article.md
+++ b/course/module-2/files/article.md
@@ -1,22 +1,12 @@
-# Работа с файлами в Python
-
-Сейчас мы затронем одну из самых фундаментальных возможностей программирования — работу
-с файлами.
-Умение корректно считывать данные из файлов и записывать их обратно
-пригодится вам практически в любом проекте. 📁
-
-Файлы — это контейнеры для хранения данных на диске.
-Python предоставляет мощные и удобные инструменты для работы с ними. Давайте разберемся, как использовать эти инструменты!
-
-## Основные операции с файлами
+---
+meta:
+ title: "Работа с файлами в Python"
+ description: "Как открывать, читать, записывать и закрывать файлы в Python. Основные концепции и методы работы с файловой системой."
+---
-В Python работа с файлами обычно включает следующие шаги:
-
-1. **Открытие файла** — указываем файл и режим работы с ним
-2. **Чтение или запись** — выполняем нужные операции с файлом
-3. **Закрытие файла** — освобождаем системные ресурсы
+# Работа с файлами в Python
-Рассмотрим каждый из этих шагов подробнее.
+Любая переменная живёт только пока запущена программа: закрыли её — и список задач, набранный счёт, сохранённые настройки исчезли. Чтобы данные пережили перезапуск, их записывают в файл на диске, а при следующем запуске читают обратно. На этом держатся заметки, сохранения в играх, экспорт отчётов — почти любое приложение.
## Открытие файлов
@@ -25,94 +15,159 @@ Python предоставляет мощные и удобные инструм
> Функция `open(file, mode)` возвращает объект файла, который можно использовать для чтения, записи или других операций с файлом.
-Вот основные режимы открытия файлов:
+Режим — это короткая строка, которая отвечает на вопрос «что мы собираемся делать с файлом». Выбрать нужно одно из трёх:
-| Режим | Описание |
-| ----- | ------------------------------------------------------------ |
-| `'r'` | Чтение (режим по умолчанию) |
-| `'w'` | Запись (создает новый файл или перезаписывает существующий) |
-| `'a'` | Добавление (append) — дописывает данные в конец файла |
-| `'b'` | Бинарный режим (например, `'rb'` для чтения бинарного файла) |
-| `'t'` | Текстовый режим (режим по умолчанию) |
-| `'+'` | Обновление (чтение и запись) |
+| Буква | Что делает |
+| ----- | ------------------------------------------------------- |
+| `'r'` | читать; файл должен существовать (режим по умолчанию) |
+| `'w'` | писать с нуля: создаёт файл, а существующий очищает |
+| `'a'` | дописывать в конец; старое содержимое остаётся на месте |
-Примеры открытия файлов в разных режимах:
+Посмотрим на два самых частых режима в деле:
```python
-# Открытие файла для чтения (режим по умолчанию - 'r')
-file = open('example.txt', 'r')
-print(f"Файл открыт в режиме: {file.mode}")
+# 'w' создаёт файл (или перезаписывает) и открывает его для записи
+file = open('notes.txt', 'w')
+file.write("Купить молоко")
file.close()
-# Открытие файла для записи
-file = open('new_file.txt', 'w')
-print(f"Файл открыт в режиме: {file.mode}")
+# 'r' открывает уже существующий файл для чтения
+file = open('notes.txt', 'r')
+print(file.read())
+
file.close()
+```
-# Открытие бинарного файла для чтения
-file = open('image.jpg', 'rb')
-print(f"Файл открыт в режиме: {file.mode}")
+Про `'w'` стоит помнить отдельно: он стирает старое содержимое молча, без предупреждения. Открыли файл с важными данными в `'w'` вместо `'a'` — и данных больше нет. А `'r'`, наоборот, требует, чтобы файл уже существовал: если его нет, Python остановит программу ошибкой `FileNotFoundError`.
+
+## Контекстный менеджер with
+
+В предыдущем примере после работы с файлом вызывался `close()`. Разберёмся, зачем он нужен и почему в реальном коде его почти не пишут.
+
+Открытый файл нужно закрывать: пока он открыт, он держит системные ресурсы, а записанные данные могут не попасть на диск до закрытия. Закрыть можно вручную:
+
+```python
+file = open('example.txt', 'w')
+file.write("Пример текста")
file.close()
```
+Загвоздка в том, что про `close()` легко забыть. А если между открытием и закрытием что-то пойдёт не так и программа прервётся, до `close()` дело вообще не дойдёт, и файл останется открытым.
+
+Конструкция `with` снимает эту заботу: она сама закрывает файл, когда блок заканчивается, при любом исходе.
+
+```python
+with open('example.txt', 'w') as file:
+ file.write("Пример текста")
+# здесь файл уже закрыт автоматически
+print("Файл закрыт автоматически после блока with")
+
+```
+
+> Контекстный менеджер `with` гарантирует, что файл закроется, даже если внутри блока возникнет ошибка. Это стандартный способ работы с файлами в Python, и дальше используется именно он.
+
## Чтение из файла
-После открытия файла его содержимое можно прочитать несколькими способами:
+После открытия файла его содержимое можно прочитать несколькими способами.
+
+Во всех примерах ниже рядом с программой лежит файл `sample.txt` из трёх строк.
### Чтение всего файла
-```python
-# Создадим тестовый файл
-with open('sample.txt', 'w') as f:
- f.write("Первая строка\nВторая строка\nТретья строка")
+**sample.txt**
+
+```text
+Первая строка
+Вторая строка
+Третья строка
+```
+
+**main.py**
-# Чтение всего файла целиком
+```python
with open('sample.txt', 'r') as file:
content = file.read()
- print("Содержимое файла:")
- print(content)
+
+print(content)
+
+```
+
+Запустим `main.py` — в выводе:
+
+```text
+Первая строка
+Вторая строка
+Третья строка
```
### Чтение файла построчно
-```python
-# Чтение файла построчно с использованием цикла
-with open('sample.txt', 'r') as file:
- print("Чтение построчно:")
- for line in file:
- print(f" Строка: {line.strip()}")
+**sample.txt**
+
+```text
+Первая строка
+Вторая строка
+Третья строка
```
-### Чтение определенного количества символов
+**main.py**
```python
-# Чтение определенного количества символов
with open('sample.txt', 'r') as file:
- first_10_chars = file.read(10)
- print(f"Первые 10 символов: {first_10_chars}")
+ for line in file:
+ print(f"Строка: {line.strip()}")
+
+```
- # Чтение следующих 10 символов
- next_10_chars = file.read(10)
- print(f"Следующие 10 символов: {next_10_chars}")
+Запустим `main.py` — в выводе:
+
+```text
+Строка: Первая строка
+Строка: Вторая строка
+Строка: Третья строка
```
### Чтение всех строк в список
+Цикл `for line in file` берёт строки по одной и не держит файл в памяти целиком. Это способ по умолчанию, особенно для больших файлов. Но иногда строки нужны все сразу: посчитать их количество, обратиться к пятой по счёту, отсортировать. Тогда пригодится `readlines()`, который возвращает обычный список:
+
+**sample.txt**
+
+```text
+Первая строка
+Вторая строка
+Третья строка
+```
+
+**main.py**
+
```python
-# Чтение всех строк в список
with open('sample.txt', 'r') as file:
lines = file.readlines()
- print(f"Список строк: {lines}")
+
+print(lines)
+
+```
+
+Запустим `main.py` — в выводе:
+
+```text
+['Первая строка\n', 'Вторая строка\n', 'Третья строка']
```
+Заметьте `\n` на концах элементов: `readlines()` режет файл по переносам строк, но сами переносы не выбрасывает. Отсюда и `line.strip()` в примере выше: `print` добавляет свой перенос к тому, что уже пришёл из файла, и без `strip()` между строками появились бы пустые.
+
## Запись в файл
-Запись данных в файл также может осуществляться разными методами:
+Записывать данные в файл можно несколькими способами:
### Запись строки
```python
-# Запись строки в файл
with open('output.txt', 'w') as file:
file.write("Привет, мир!\n")
file.write("Python - отличный язык программирования.")
@@ -121,7 +176,14 @@ with open('output.txt', 'w') as file:
with open('output.txt', 'r') as file:
content = file.read()
print("Содержимое файла после записи:")
+
print(content)
+
```
### Запись нескольких строк
@@ -134,71 +196,58 @@ with open('lines.txt', 'w') as file:
for line in lines:
file.write(line + '\n')
-# Альтернативный способ: использование writelines()
-with open('lines2.txt', 'w') as file:
- # Не забудьте добавить символы переноса строки
- file.writelines([line + '\n' for line in lines])
-
-# Проверим второй файл
-with open('lines2.txt', 'r') as file:
+# Проверим, что записалось
+with open('lines.txt', 'r') as file:
content = file.read()
- print("Содержимое файла lines2.txt:")
+ print("Содержимое файла lines.txt:")
+
print(content)
+
```
### Добавление данных в конец файла
-```python
-# Добавление данных в конец файла (режим 'a')
-with open('output.txt', 'a') as file:
- file.write("\nЭта строка добавлена позже.")
+Рядом лежит `output.txt` с двумя строками — режим `'a'` допишет в его конец, ничего не стирая:
-# Проверим результат
-with open('output.txt', 'r') as file:
- content = file.read()
- print("Содержимое файла после добавления:")
- print(content)
+**output.txt**
+
+```text
+Привет, мир!
+Python - отличный язык программирования.
```
-## Контекстный менеджер with
+**main.py**
-Вы могли заметить, что во всех примерах выше мы использовали конструкцию `with open(...) as file:`.
-Это контекстный менеджер, который автоматически закрывает файл после завершения работы
-с ним.
+```python
+with open('output.txt', 'a') as file:
+ file.write("
+Эта строка добавлена позже.")
-> Контекстный менеджер `with` — это безопасный способ работы с файлами, который гарантирует закрытие файла даже при возникновении ошибок.
+with open('output.txt', 'r') as file:
+ print(file.read())
-Сравним два подхода:
+```
-```python
-# Традиционный подход (требует явного закрытия файла)
-file = open('example.txt', 'w')
-try:
- file.write("Пример текста")
-finally:
- file.close() # Файл нужно закрыть явно
- print("Файл закрыт вручную")
+Запустим `main.py` — в выводе:
-# Подход с использованием with (более безопасный)
-with open('example.txt', 'w') as file:
- file.write("Пример текста")
-# Файл автоматически закрывается после выхода из блока with
-print("Файл закрыт автоматически после блока with")
+```text
+Привет, мир!
+Python - отличный язык программирования.
+Эта строка добавлена позже.
```
-Настоятельно рекомендуется всегда использовать контекстный менеджер `with` при работе
-с файлами в Python! 👍
-
## Обработка исключений при работе с файлами
-При работе с файлами могут возникать различные ошибки:
-
-- Файл не существует
-- Недостаточно прав для доступа к файлу
-- Диск заполнен
-- И т.д.
+При работе с файлами могут возникать ошибки: файла нет на месте, не хватает прав на доступ, на диске кончилось место.
-Хорошей практикой является обработка этих ошибок:
+Перехватывать такие ошибки и реагировать на них умеет конструкция `try/except` (ей посвящён отдельный урок дальше в курсе). Здесь достаточно увидеть её в действии на файлах: блок `try` пробует выполнить операцию, а `except` ловит конкретную ошибку, если она случилась.
```python
# Обработка возможных ошибок при открытии файла
@@ -207,98 +256,85 @@ try:
content = file.read()
except FileNotFoundError:
print("Ошибка: Файл не найден!")
-except PermissionError:
- print("Ошибка: Недостаточно прав для доступа к файлу!")
-except Exception as e:
- print(f"Произошла ошибка: {e}")
+
```
-## Дополнительные операции с файлами
+## Работа с путями к файлам
-Python предоставляет множество дополнительных возможностей для работы с файлами:
+До сих пор мы писали имя файла как есть: `open('notes.txt')`. Python ищет такой файл в текущем каталоге — там, откуда запущена программа.
-### Перемещение указателя
+Если файл лежит в соседней папке, нужен путь. Склеивать его строками рискованно: на macOS и Linux части разделяет `/`, а на Windows `\`. Написанный вручную путь сломается при переезде на другую систему.
-```python
-# Перемещение указателя в файле
-with open('sample.txt', 'r') as file:
- # Прочитаем первые 6 символов
- print(file.read(6))
+Поэтому пути собирают модулем `pathlib` из стандартной библиотеки.
- # Переместим указатель на начало файла
- file.seek(0)
- print("После file.seek(0):", file.read(6))
+Путь в `pathlib` — не строка, а объект: части соединяются оператором `/`, а имя, расширение и проверка существования доступны прямо у объекта. Читается такой код близко к тому, как путь выглядит.
- # Переместим указатель на 8-ю позицию
- file.seek(8)
- print("После file.seek(8):", file.read(6))
-```
-
-### Получение текущей позиции
+В песочнице текущий каталог — `/home/pyodide`; на вашем компьютере путь будет другим.
```python
-# Получение текущей позиции указателя
-with open('sample.txt', 'r') as file:
- print(f"Начальная позиция: {file.tell()}")
- file.read(10)
- print(f"Позиция после чтения 10 символов: {file.tell()}")
- file.readline()
- print(f"Позиция после чтения строки: {file.tell()}")
-```
-
-## Работа с путями к файлам
+from pathlib import Path
-При работе с файлами важно правильно указывать пути. Python предоставляет модуль `os.path` и модуль `pathlib` для облегчения работы с путями:
+# Текущий каталог
+current_path = Path.cwd()
+print(f"Текущий каталог: {current_path}")
+
-```python
-import os
+# Создание пути
+data_file = current_path / 'data' / 'info.txt'
+print(f"Путь к файлу: {data_file}")
+
-# Текущий рабочий каталог
-current_dir = os.getcwd()
-print(f"Текущий каталог: {current_dir}")
+# Получение имени файла и расширения
+document_path = Path("path/to/document.pdf")
+print(f"Имя файла: {document_path.stem}, расширение: {document_path.suffix}")
+
+```
-# Объединение путей (корректная работа с разделителями)
-data_file = os.path.join(current_dir, 'data', 'info.txt')
-print(f"Путь к файлу: {data_file}")
+Проверка существования — метод `exists()`: он отвечает, есть ли по пути настоящий файл. Ниже `sample.txt` лежит рядом с программой, а `missing.txt` — нет:
-# Проверка существования файла
-sample_exists = os.path.exists('sample.txt')
-print(f"Файл sample.txt существует: {sample_exists}")
+**sample.txt**
-# Получение имени файла и расширения
-filename = "path/to/document.pdf"
-basename = os.path.basename(filename)
-name, ext = os.path.splitext(basename)
-print(f"Имя файла: {name}, расширение: {ext}")
+```text
+Первая строка
+Вторая строка
+Третья строка
```
-Более современный подход с использованием `pathlib`:
+**main.py**
```python
from pathlib import Path
-# Текущий каталог
-current_path = Path.cwd()
-print(f"Текущий каталог: {current_path}")
+print(Path('sample.txt').exists())
+print(Path('missing.txt').exists())
-# Создание пути
-data_file = current_path / 'data' / 'info.txt'
-print(f"Путь к файлу: {data_file}")
+```
-# Проверка существования файла
-sample_path = Path('sample.txt')
-print(f"Файл sample.txt существует: {sample_path.exists()}")
+Запустим `main.py` — в выводе:
-# Получение имени файла и расширения
-document_path = Path("path/to/document.pdf")
-print(f"Имя файла: {document_path.stem}, расширение: {document_path.suffix}")
+```text
+True
+False
```
## Проверка понимания
-Давайте проверим, насколько хорошо вы усвоили тему работы с файлами:
+**Что случится с содержимым, если открыть существующий файл в режиме `'w'`?**
+
+1. **Правильный ответ:** Старое содержимое сотрётся, файл начнётся с нуля — Режим "w" очищает файл молча, без предупреждения, и делает это в момент открытия — ещё до первого write(). Чтобы дописать в конец, нужен режим "a".
+
+2. Новые данные допишутся в конец, старые останутся — Так ведёт себя режим "a". А "w" начинает файл с чистого листа.
-**Какой код корректно открывает файл для записи и добавляет в конец файла строку?**
+3. Python откажется открывать файл, раз он уже существует — Существующий файл "w" открывает спокойно. Наоборот ругается режим "r": если файла нет, он поднимет FileNotFoundError.
+4. Ничего не изменится, пока не вызовут write() — Файл очищается уже при открытии. Даже если сразу закрыть его, не записав ни строки, прежнее содержимое не вернуть.
В следующих уроках мы углубимся в работу с конкретными типами файлов, такими как текстовые файлы, CSV, JSON и другие.
diff --git a/course/module-2/inheritance/article.md b/course/module-2/inheritance/article.md
index 0fb8e30..3408caf 100644
--- a/course/module-2/inheritance/article.md
+++ b/course/module-2/inheritance/article.md
@@ -1,6 +1,12 @@
+---
+meta:
+ title: "Наследование в Python"
+ description: "Как один класс продолжает другой: дочерние классы, super(), переопределение методов и проверка типа через isinstance."
+---
+
# Наследование в Python
-В прошлых уроках мы написали класс `Person` — имя, возраст, метод `greet()`. Теперь нужен класс `Student`. У студента есть имя и возраст (то же, что у человека), есть `greet()` (студент тоже умеет здороваться), но дополнительно есть школа и оценки, а здороваться он умеет «по-своему» — с упоминанием школы.
+В прошлых уроках мы написали класс `Person` — имя, возраст, метод `greet()`. Теперь нужен класс `Student`. У студента есть имя и возраст (то же, что у человека), есть `greet()` (студент тоже умеет здороваться), но дополнительно есть школа, а здороваться он умеет «по-своему» — с упоминанием школы.
Можно скопировать весь код `Person` в `Student` и дописать новое. Но если потом мы что-то поправим в `Person`, в копии это не обновится. Дублирование кода — это всегда мина с задержкой.
@@ -10,7 +16,7 @@
Чтобы один класс наследовался от другого, имя родителя пишется в скобках после имени дочернего класса:
-```python-executable
+```python
class Person:
def __init__(self, name, age):
self.name = name
@@ -22,23 +28,30 @@ class Person:
class Student(Person): # Student наследуется от Person
pass # пока ничего нового не добавляем
-# Создаём студента
student = Student("Анна", 20)
# greet() и атрибуты унаследованы от Person
print(student.name)
-# Вывод: Анна
+
print(student.greet())
-# Вывод: Привет, меня зовут Анна, мне 20 лет.
+
```
Мы ни строчки не написали внутри `Student`, но он уже работает — потому что получил `__init__` и `greet()` от `Person`. Это и есть базовое наследование.
+`class Student(Person)` — наследует: `name`, `age`, `greet()` — из Person.
+
+school объявлен в Student, а name, age и greet() достались от Person
+
## Добавление новых атрибутов и super()
-Теперь добавим студенту школу и оценки. Нужно расширить `__init__`: принять и старые параметры (`name`, `age`), и новые (`school`, `grades`). Чтобы не дублировать установку `self.name` и `self.age`, вызовем родительский `__init__` через `super()`:
+Теперь добавим студенту школу. Нужно расширить `__init__`: принять и старые параметры (`name`, `age`), и новый (`school`). Чтобы не дублировать установку `self.name` и `self.age`, вызовем родительский `__init__` через `super()`:
-```python-executable
+```python
class Person:
def __init__(self, name, age):
self.name = name
@@ -51,28 +64,26 @@ class Student(Person):
def __init__(self, name, age, school):
super().__init__(name, age) # пусть Person сам установит name и age
self.school = school
- self.grades = []
-
- def add_grade(self, grade):
- self.grades.append(grade)
student = Student("Анна", 20, "МГУ")
-student.add_grade(5)
-student.add_grade(4)
print(student.greet()) # унаследованный метод
-# Вывод: Привет, меня зовут Анна, мне 20 лет.
-print(student.school, student.grades)
-# Вывод: МГУ [5, 4]
+
+print(student.school)
+
```
-`super()` это ссылка на «родителя текущего класса». `super().__init__(name, age)` означает «вызови `__init__` от `Person`, передай ему `name` и `age`». Так мы переиспользуем логику родителя вместо того, чтобы копировать её.
+`super()` — это обращение к родителю текущего класса. Вызов в примере читается так: «попроси `Person` выполнить его `__init__`, передав `name` и `age`». Так мы переиспользуем логику родителя вместо того, чтобы копировать её.
## Переопределение методов
Дочерний класс может **переопределить** метод родителя — задать своё поведение под тем же именем. Если в `Student` определён свой `greet()`, Python будет вызывать его, а не родительский:
-```python-executable
+```python
class Person:
def __init__(self, name, age):
self.name = name
@@ -94,18 +105,22 @@ person = Person("Иван", 30)
student = Student("Анна", 20, "МГУ")
print(person.greet())
-# Вывод: Привет, меня зовут Иван.
+
print(student.greet())
-# Вывод: Привет, меня зовут Анна. Я учусь в МГУ.
+
```
Внутри переопределённого метода можно вызвать `super().greet()`, чтобы не повторять логику родителя, а только расширить её.
## Многоуровневые иерархии
-Наследоваться можно цепочкой: `Student` от `Person`, а `GraduateStudent` от `Student`. Получится цепочка из трёх классов, и потомок будет иметь доступ ко всему, что есть выше:
+Наследоваться можно цепочкой: `Student` от `Person`, а `GraduateStudent` от `Student`. Потомок получает доступ ко всему, что объявлено выше:
-```python-executable
+```python
class Person:
def __init__(self, name):
self.name = name
@@ -127,50 +142,57 @@ grad = GraduateStudent("Анна", "МГУ", "Петров И.И.")
# Метод из Person, атрибуты со всех уровней
print(grad.greet())
-# Вывод: Привет, я Анна.
+
print(grad.school, "/", grad.advisor)
-# Вывод: МГУ / Петров И.И.
+
```
Когда Python ищет метод или атрибут, он идёт по цепочке: сначала в текущем классе, потом в родителе, потом в родителе родителя — пока не найдёт.
-## Проверка типов: isinstance и issubclass
+## Проверка типа: isinstance
-`isinstance(obj, Class)` проверяет, является ли объект экземпляром класса (или любого его наследника). `issubclass(A, B)` проверяет, является ли класс `A` наследником `B`:
+`isinstance(obj, Class)` проверяет, является ли объект экземпляром класса или любого его наследника:
-```python-executable
+```python
class Person:
pass
class Student(Person):
pass
+person = Person()
student = Student()
-# Student - это разновидность Person
print(isinstance(student, Student))
-# Вывод: True
+
print(isinstance(student, Person))
-# Вывод: True
-
-# Отношения между классами
-print(issubclass(Student, Person))
-# Вывод: True
-print(issubclass(Person, Student))
-# Вывод: False
+
+print(isinstance(person, Student))
+
```
-Главное здесь: студент **является** человеком (`isinstance(student, Person)` это `True`), но человек не обязательно студент. Наследование задаёт отношение «is-a» в одну сторону.
+Главное здесь: студент **является** человеком, поэтому `isinstance(student, Person)` даёт `True`. Обратное неверно: человек — не обязательно студент.
-## А что насчёт множественного наследования?
+## Проверка понимания
-Python разрешает классу иметь несколько родителей: `class Duck(Flying, Swimming):`. Это работает, но порядок поиска методов в таких иерархиях быстро становится неочевидным (есть отдельный алгоритм — Method Resolution Order, MRO). На практике для большинства задач хватает одного родителя, и так код легче читать. С множественным наследованием можно разобраться позже, в продвинутом курсе.
+**Что произойдёт при вызове метода из дочернего класса, который не переопределяет этот метод родительского класса?**
-## Что дальше?
+1. **Правильный ответ:** Будет вызван метод родительского класса — Если метод не переопределён в дочернем классе, Python ищет его выше по цепочке родителей и выполняет найденную версию.
-В следующем уроке возьмём второй принцип ООП — инкапсуляцию: как прятать внутренности класса за интерфейсом и почему это делает код устойчивее к изменениям.
+2. Будет вызвано исключение AttributeError — Если метод существует у родителя, Python найдёт и выполнит его, даже если в дочернем классе метод явно не определён.
----
+3. Ничего не произойдёт, метод просто не будет выполнен — Python пройдёт по иерархии классов и, если найдёт метод у родителя, выполнит его.
-**Что произойдёт при вызове метода из дочернего класса, который не переопределяет этот метод родительского класса?**
+4. Метод вернёт None по умолчанию — Будет выполнена родительская версия метода, а не возвращено None.
+В следующем уроке возьмём второй принцип ООП — инкапсуляцию: как прятать внутренности класса, открывая наружу только нужное, и почему это делает код устойчивее к изменениям.
diff --git a/course/module-2/lambda-functions/article.md b/course/module-2/lambda-functions/article.md
index fd64308..5013a1b 100644
--- a/course/module-2/lambda-functions/article.md
+++ b/course/module-2/lambda-functions/article.md
@@ -1,210 +1,140 @@
+---
+meta:
+ title: "Лямбда-функции"
+ description: "Крошечные безымянные функции: синтаксис lambda и применение с sorted, map и filter."
+---
+
# Лямбда-функции в Python
-> Лямбда-функция — это маленькая анонимная функция, которая может принимать любое количество аргументов, но содержит только одно выражение.
+Список чисел `sorted()` отсортирует сама: с числами всё понятно — меньше, больше. А вот со списком студентов уже вопрос: что сравнивать — имена, оценки? Python этого не знает.
-В переводе с программистского на человеческий:
+Чтобы подсказать, `sorted()` принимает маленькую вспомогательную функцию: та берёт одного студента и возвращает значение, по которому сравнивать, — например оценку. Заводить ради одной такой строки полноценную функцию через `def`, придумывать ей имя и выносить отдельным блоком — громоздко.
-- **Анонимная** — у неё нет имени (в отличие от обычных функций, определяемых через `def`)
-- **Маленькая** — она ограничена одним выражением (одной строкой кода)
-- **Функция** — она принимает аргументы и возвращает результат
+Для таких случаев есть лямбда-функции: крошечная функция прямо там, где она нужна, без имени и без `def`.
-### Синтаксис лямбда-функции
+> Лямбда-функция — это безымянная функция из одного выражения: она принимает аргументы и возвращает результат этого выражения. Записывается по формуле:
```python
lambda аргументы: выражение
```
-Где:
-
-- `lambda` — это ключевое слово, которое сообщает Python, что мы создаём лямбда-функцию
-- `аргументы` — это входные параметры (могут быть от 0 до нескольких, разделенных запятыми)
-- `выражение` — это одно выражение, результат которого будет возвращен
+## Лямбда рядом с обычной функцией
-## Сравнение с обычными функциями
-
-Давайте сравним обычную функцию и лямбда-функцию, которые делают одно и то же:
+Сравним обычную функцию и лямбду, которые делают одно и то же:
```python
-# Обычная функция
def square(x):
return x * x
-# Эквивалентная лямбда-функция
square_lambda = lambda x: x * x
-# Используем обе функции
print(square(5))
+
print(square_lambda(5))
+
+```
-# Функция с несколькими аргументами
-def power(base, exponent):
- return base ** exponent
-
-# Эквивалентная лямбда-функция
-power_lambda = lambda base, exponent: base ** exponent
+Запись короче: имени нет, `return` не пишется — возвращается результат единственного выражения. Правда, сохранять лямбду в переменную, как здесь, почти не приходится: её сила — в передаче напрямую туда, где нужна маленькая функция. Начнём с задачи из вступления.
-print(power(2, 3))
-print(power_lambda(2, 3))
-```
+## sorted(): сортировка по своему правилу
-Как видите, лямбда-функции более компактны, но менее читаемы для сложных операций.
+У `sorted()` есть параметр `key` — функция, которая достаёт из элемента значение для сравнения. Лямбда пишется прямо в вызове:
-## Когда использовать лямбда-функции?
+```python
+students = [
+ {"name": "Alice", "grade": 85},
+ {"name": "Bob", "grade": 92},
+ {"name": "Charlie", "grade": 78},
+]
-Лямбда-функции лучше всего подходят для случаев, когда:
+sorted_by_grade = sorted(students, key=lambda student: student["grade"], reverse=True)
+for student in sorted_by_grade:
+ print(f"{student['name']}: {student['grade']}")
+
+```
-1. Функция простая (одно выражение)
-2. Функция используется только один раз (или несколько раз в одном месте)
-3. Функция передается как аргумент другой функции
+`key=lambda student: student["grade"]` — та самая вспомогательная функция из вступления: берёт студента, возвращает его оценку. А `reverse=True` разворачивает порядок от большей к меньшей. Правило сравнения может быть любым — например, отсортируем числа по абсолютному значению:
-Наиболее распространенные случаи использования — с функциями высшего порядка, такими как `map()`, `filter()`, `sorted()` и др.
+```python
+numbers = [5, -3, 2, -8, 1, 0, -2]
-## Лямбда с функциями высшего порядка
+print(sorted(numbers, key=lambda x: abs(x)))
+
+```
-### map() — применение функции к каждому элементу
+## map(): применить функцию к каждому элементу
-Функция `map()` применяет указанную функцию к каждому элементу итерируемого объекта:
+`map()` применяет функцию к каждому элементу списка, а `list(...)` собирает результаты в новый список:
```python
-# Удвоение всех чисел в списке
numbers = [1, 2, 3, 4, 5]
-# С обычной функцией
-def double(x):
- return x * 2
-
-doubled = list(map(double, numbers))
+doubled = list(map(lambda x: x * 2, numbers))
print(doubled)
+
-# С лямбда-функцией (гораздо компактнее!)
-doubled_lambda = list(map(lambda x: x * 2, numbers))
-print(doubled_lambda)
-
-# Преобразование температуры из Цельсия в Фаренгейт
-celsius = [0, 10, 20, 30, 40]
-fahrenheit = list(map(lambda c: (c * 9/5) + 32, celsius))
+celsius = [0, 10, 20, 30]
+fahrenheit = list(map(lambda c: c * 9 / 5 + 32, celsius))
print(fahrenheit)
+
```
-### filter() — отбор элементов по условию
+## filter(): отобрать элементы по условию
-Функция `filter()` создает итератор из элементов, для которых функция возвращает `True`:
+`filter()` оставляет только те элементы, для которых функция вернула `True`:
```python
-# Фильтрация четных чисел
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
-# С лямбда-функцией
-even_lambda = list(filter(lambda x: x % 2 == 0, numbers))
-print(even_lambda)
+even = list(filter(lambda x: x % 2 == 0, numbers))
+print(even)
+
-# Фильтрация слов длиннее 3 букв
-words = ["hi", "hello", "hey", "howdy", "hi there"]
+words = ["hi", "hello", "hey", "howdy"]
long_words = list(filter(lambda word: len(word) > 3, words))
print(long_words)
+
```
-### sorted() — сортировка с пользовательским ключом
+## Когда лямбда, а когда def
-Функция `sorted()` возвращает отсортированный список, а с помощью параметра `key` можно указать функцию для извлечения значения для сравнения:
+Лямбда уместна, когда функция простая — одно выражение — и нужна один раз, обычно как аргумент `sorted()`, `map()` или `filter()`.
-```python
-# Сортировка чисел по абсолютному значению
-numbers = [5, -3, 2, -8, 1, 0, -2]
+Ограничения вытекают из самой записи:
-sorted_numbers = sorted(numbers, key=lambda x: abs(x))
-print(sorted_numbers)
+- тело — ровно одно выражение: ни нескольких строк, ни присваиваний внутри;
+- имени нет, поэтому вызвать её из другого места не получится.
-# Сортировка словарей по значению определенного ключа
-students = [
- {"name": "Alice", "grade": 85},
- {"name": "Bob", "grade": 92},
- {"name": "Charlie", "grade": 78},
- {"name": "Diana", "grade": 95}
-]
+Если логика не помещается в одно выражение или нужна больше одного раза — пишите обычную функцию через `def`: у неё есть имя и место для нескольких строк.
-# Сортировка по оценке (по убыванию)
-sorted_by_grade = sorted(students, key=lambda student: student["grade"], reverse=True)
-for student in sorted_by_grade:
- print(f"{student['name']}: {student['grade']}")
-```
-
-### reduce() — последовательное применение функции
-
-Функция `reduce()` (из модуля `functools`) последовательно применяет функцию к элементам, накапливая результат:
-
-```python
-from functools import reduce
-
-# Сумма всех чисел в списке
-numbers = [1, 2, 3, 4, 5]
-
-total_lambda = reduce(lambda x, y: x + y, numbers)
-print(total_lambda)
-
-# Объединение строк
-words = ["Hello", "world", "of", "Python"]
-sentence = reduce(lambda x, y: x + " " + y, words)
-print(sentence)
-```
-
-## Передача лямбда-функций в другие функции
-
-Лямбда-функции часто используются как аргументы в других функциях:
-
-```python
-def apply_operation(x, y, operation):
- """Применяет операцию к двум числам и возвращает результат"""
- return operation(x, y)
-
-# Использование с разными лямбда-функциями
-print(apply_operation(5, 3, lambda x, y: x + y)) # Сложение
-print(apply_operation(5, 3, lambda x, y: x * y)) # Умножение
-
-# Форматирование данных
-def format_data(data, formatter):
- """Форматирует данные с помощью указанной функции"""
- return [formatter(item) for item in data]
-
-names = ["alice", "bob", "charlie"]
-print(format_data(names, lambda x: x.title())) # Начало с заглавной
-```
+## Проверка понимания
-## Ограничения лямбда-функций
+**Какое из следующих утверждений о лямбда-функциях в Python верно?**
-Лямбда-функции имеют несколько важных ограничений:
+1. Лямбда-функции могут содержать несколько выражений, разделённых точкой с запятой — Лямбда-функции могут содержать только одно выражение.
-1. **Только одно выражение** — нельзя использовать несколько строк кода
-2. **Отсутствие документации** — нельзя добавить docstring
-3. **Ограниченная читаемость** — для сложных операций лучше обычные функции
-4. **Нет оператора присваивания** — в лямбде нельзя использовать `=`
+2. Лямбда-функции всегда эффективнее, чем обычные функции — Эффективность лямбда-функций и обычных функций практически одинакова.
-```python
-# Пример сложной логики, где лучше не использовать лямбду
-complex_lambda = lambda x: (
- x ** 2 if x > 0
- else x + 1 if x < 0
- else 42
-)
-print(complex_lambda(5))
-
-# То же самое, но с обычной функцией - гораздо понятнее
-def process_number(x):
- """Обрабатывает число по правилам:
- - Положительное -> квадрат
- - Отрицательное -> x + 1
- - Ноль -> 42
- """
- if x > 0:
- return x ** 2
- elif x < 0:
- return x + 1
- else:
- return 42
-
-print(process_number(5))
-```
+3. **Правильный ответ:** Лямбда-функции безымянны и могут содержать только одно выражение — Лямбда-функции не имеют имени и ограничены одним выражением — поэтому и записываются прямо там, где нужны.
-## Проверка понимания
+4. В лямбда-функциях можно использовать оператор присваивания (=) — В лямбда-функциях нельзя использовать присваивание: тело лямбды — одно выражение, а не инструкции.
-**Какое из следующих утверждений о лямбда-функциях в Python верно?**
+В следующем уроке — обработка ошибок: что делать, когда программа падает, и как `try/except` превращает аварию в управляемую ситуацию.
diff --git a/course/module-2/libraries/article.md b/course/module-2/libraries/article.md
index 4bbb0e5..aba2330 100644
--- a/course/module-2/libraries/article.md
+++ b/course/module-2/libraries/article.md
@@ -1,72 +1,70 @@
+---
+meta:
+ title: "Использование библиотек в Python"
+ description: "Введение в библиотеки Python: что такое библиотеки, как их импортировать и использовать, стандартная библиотека и сторонние модули."
+---
+
# Использование библиотек в Python
-Библиотеки — это наборы готового кода, который можно использовать в своих программах. Они позволяют не "изобретать велосипед" и быстрее достигать результатов, используя решения, созданные другими разработчиками.
+Вам понадобилось значение π или квадратный корень из числа. Выводить формулу и проверять её самому — долго и легко ошибиться, а кто-то уже написал и отладил этот код за вас. Готовые наборы такого кода называются библиотеками: подключаете нужную одной строкой и пользуетесь.
## Что такое библиотеки?
> Библиотека (или модуль) в Python — это файл с кодом, содержащий функции, классы и переменные, которые вы можете использовать в своих программах.
-Библиотеки помогают:
-
-- Экономить время, избегая написания кода "с нуля"
-- Использовать проверенные, оптимизированные решения
-- Расширять возможности языка Python
-- Создавать более сложные программы с меньшими усилиями
-
## Импорт библиотек
Чтобы использовать библиотеку, нужно сначала импортировать её в свою программу. Python предлагает несколько способов импорта:
### Импорт всей библиотеки
+Подключаем модуль целиком. Всё его содержимое остаётся за именем модуля, и обращаемся мы к нему через точку:
+
```python
-# Импорт всей библиотеки math
import math
-# Использование функций через имя библиотеки
radius = 5
circle_area = math.pi * radius ** 2
print(f"Площадь круга радиусом {radius} равна {circle_area:.2f}")
+
```
### Импорт конкретных элементов
+Забираем из модуля только нужное. Эти имена попадают прямо в вашу программу, и писать `math.` перед ними больше не надо:
+
```python
-# Импорт только определенных функций из модуля
-from math import sqrt, pow
+from math import sqrt, floor
-# Используем импортированные функции напрямую
x = 16
result = sqrt(x)
print(f"Квадратный корень из {x} равен {result}")
-
-y = pow(2, 3)
-print(f"2 в степени 3 равно {y}")
+
+
+y = floor(3.7)
+print(f"Округление 3.7 вниз: {y}")
+
```
### Импорт с переименованием
+Тот же импорт целиком, но под коротким именем. Пригодится, когда полное имя длинное или уже занято вашей переменной:
+
```python
-# Импорт модуля с альтернативным именем (псевдонимом)
import math as m
angle = 45
-# Используем m вместо math
sin_value = m.sin(m.radians(angle))
print(f"Синус {angle} градусов равен {sin_value:.4f}")
-```
-
-### Импорт всех элементов
-
-```python
-# Импорт всех элементов из модуля
-# Примечание: обычно этот способ не рекомендуется
-from math import *
-
-radius = 3
-# Используем pi и cos напрямую, без указания math
-print(f"π примерно равно {pi:.5f}")
-print(f"Косинус 0 равен {cos(0)}")
+
```
## Виды библиотек в Python
@@ -82,19 +80,21 @@ print(f"Косинус 0 равен {cos(0)}")
### Примеры встроенных модулей
```python
-# Модуль random для генерации случайных чисел
import random
-# Генерация случайного числа от 1 до 10
random_number = random.randint(1, 10)
print(f"Случайное число: {random_number}")
+
-# Модуль datetime для работы с датами и временем
import datetime
-# Получение текущей даты и времени
current_date = datetime.datetime.now()
print(f"Текущая дата и время: {current_date}")
+
```
## Поиск функций в документации
@@ -103,28 +103,62 @@ print(f"Текущая дата и время: {current_date}")
### Использование функции help()
+`help()` печатает всё, что модуль рассказывает о себе: назначение, список функций, описание каждой. У `math` справка длинная, поэтому ниже показано только её начало, а многоточие в конце — знак, что она на этом не кончается.
+
```python
-# Получение информации о модуле
import math
-help(math) # Выведет только начало справки, чтобы не перегружать пример
+help(math)
+
```
### Использование функции dir()
```python
-# Получение списка всех доступных атрибутов и методов
import random
attributes = dir(random)
# Выведем только первые 10 элементов для краткости
print(attributes[:10])
+
```
## Проверка понимания
-Давайте проверим, насколько хорошо вы усвоили тему библиотек:
-
**Какие из следующих способов импорта библиотек являются правильными в Python?**
+1. **Правильный ответ:** import math — Это правильный способ импорта всей библиотеки. После этого функции используются через имя модуля: math.sqrt(16).
+
+2. **Правильный ответ:** from math import sqrt — Это правильный способ импорта конкретной функции из модуля. После этого функцию можно использовать напрямую: sqrt(16).
+
+3. import sqrt from math — Это неправильный синтаксис. Правильный способ: "from math import sqrt".
+
+4. import math.sqrt — Этот синтаксис не работает в Python. Нельзя импортировать конкретную функцию через точку.
-В следующих уроках мы рассмотрим конкретные библиотеки и их применение более подробно.
+В следующем уроке посмотрим [встроенные библиотеки](https://python-academy.org/ru/guide/built-in-libraries) — те, что доступны сразу после установки Python.
diff --git a/course/module-2/modules/article.md b/course/module-2/modules/article.md
index d8d85c7..3ee4086 100644
--- a/course/module-2/modules/article.md
+++ b/course/module-2/modules/article.md
@@ -1,287 +1,224 @@
+---
+meta:
+ title: "Модули в Python"
+ description: "Как создавать, импортировать и использовать собственные модули в Python: организация кода, пространство имен и лучшие практики."
+---
+
# Модули в Python
-Представьте, что вы пишете большую программу, которая постепенно превращается в сотни или даже тысячи строк кода. Держать всё в одном файле становится неудобно — код трудно поддерживать и находить нужные части. Здесь на помощь приходят модули! 📦
+Пока каждая ваша программа помещалась в один файл, и этого хватало. Но однажды вы напишете функцию, которая пригодится и в следующей задаче, а перенести её туда получится только копированием. Скопировали в три скрипта, нашли ошибку — правите в трёх местах.
-## Что такое модуль?
+Значит, нужен способ держать код в отдельном файле и подключать оттуда только нужное. Такой файл называется модулем.
-> Модуль в Python — это просто файл с расширением `.py`,
-> содержащий код Python (функции, классы, переменные),
-> который можно импортировать и использовать в других программах.
+## Вы уже пользуетесь модулями
-Модули помогают:
+Строка `import math` встречалась вам в главах про библиотеки. Посмотрим внимательнее, что происходит после неё.
-- Организовать связанный код в отдельные файлы
-- Повторно использовать код в разных программах
-- Избежать конфликтов имен
-- Сделать код более читаемым и поддерживаемым
+```python
+import math
+
+print(math.sqrt(144))
+
+print(type(math))
+
+```
-## Создание собственного модуля
+`import` не вставляет текст чужого файла в ваш. Он создаёт **объект-модуль** и кладёт его в переменную с именем `math`. Дальше `math.sqrt` — обычное обращение к атрибуту объекта, точно такое же, как `"строка".upper()`.
-Создать модуль в Python очень просто — достаточно написать код в файле с расширением `.py`:
+> Модуль — это файл с расширением `.py` (у нескольких встроенных модулей вроде `math` файла нет, они вшиты в интерпретатор). Всё, что в нём определено (функции, классы, переменные), становится атрибутами объекта-модуля после импорта.
-Создадим файл mymath.py:
+## Создаём свой модуль
-```python
-# mymath.py
+Теперь сделаем такой файл сами. Пусть у нас есть небольшой скрипт, который считает цены: прибавляет НДС и применяет скидки. Сам расчёт положим в `prices.py`, а в `main.py` оставим только вывод результатов:
-"""
-Модуль с математическими функциями.
-"""
+**prices.py**
-# Константы
-PI = 3.14159
+```python
+"""Расчёт цен: НДС и скидки."""
-# Функции
-def add(a, b):
- """Сложение двух чисел."""
- return a + b
+VAT = 0.2
-def subtract(a, b):
- """Вычитание одного числа из другого."""
- return a - b
-def multiply(a, b):
- """Умножение двух чисел."""
- return a * b
+def with_vat(price):
+ """Цена с НДС."""
+ return round(price * (1 + VAT), 2)
-def divide(a, b):
- """Деление одного числа на другое."""
- if b == 0:
- raise ValueError("Деление на ноль невозможно")
- return a / b
-```
-Вот и все! Теперь у нас есть модуль `mymath`, который содержит константу `PI` и четыре функции.
+def discount(price, percent):
+ """Цена после скидки в процентах."""
+ return round(price * (1 - percent / 100), 2)
-## Импорт модулей
-
-Чтобы использовать ваш модуль в других программах, нужно его импортировать. Python предлагает несколько способов импорта:
+```
-### Импорт всего модуля
+**main.py**
```python
-# Предположим, что файл mymath.py находится в том же каталоге
-import mymath
+import prices
-# Использование функций из модуля
-result = mymath.add(5, 3)
-print(f"5 + 3 = {result}")
+print(prices.with_vat(100)) # 120.0
+print(prices.discount(1000, 15)) # 850.0
+print(prices.VAT) # 0.2
-# Использование константы из модуля
-area = mymath.PI * 5**2
-print(f"Площадь круга радиусом 5: {area}")
```
-### Импорт конкретных элементов
+`main.py` пишет `import prices`, и Python находит `prices.py` в той же папке. Имя модуля берётся из имени файла: убираем `.py`, получаем `prices`. Этого достаточно, чтобы импорт заработал.
-```python
-# Импорт только определенных функций
-from mymath import add, multiply
+Раз имя модуля — это имя файла, называйте файлы в snake_case: `user_interface.py`, а не `UserInterface.py`. И не `random.py`: файл, названный как модуль стандартной библиотеки, перекроет настоящий модуль.
-# Теперь можно использовать функции напрямую, без префикса mymath
-print(f"5 + 3 = {add(5, 3)}")
+## Код модуля выполняется ровно один раз
-print(f"5 * 3 = {multiply(5, 3)}")
-
-# Но остальные функции не доступны
-# print(subtract(5, 3)) # Вызовет ошибку
-```
+При импорте Python выполняет файл сверху донизу. В `boot.py` ниже `print` стоит на верхнем уровне, не внутри функции, поэтому сработает при импорте. Импортируем этот модуль двумя строками подряд:
-### Импорт с переименованием
+**boot.py**
```python
-# Импорт модуля с псевдонимом
-import mymath as mm
+print("boot.py выполняется")
+
+READY = True
-print(f"5 - 3 = {mm.subtract(5, 3)}")
```
-### Импорт всех элементов
+**main.py**
```python
-# Импорт всех элементов из модуля
-# (обычно это не рекомендуется, так как может привести к конфликтам имен)
-from mymath import *
+import boot
+import boot
-print(f"PI = {PI}")
+print("а строк с import было две")
-print(f"5 / 2 = {divide(5, 2)}")
```
-## Структура модуля
+```text
+boot.py выполняется
+а строк с import было две
+```
-Хороший модуль должен иметь четкую структуру:
+Хотя строк с `import` было две, файл выполнился один раз: готовые модули Python запоминает и при повторном импорте отдаёт тот же объект. Отсюда практический вывод: на верхнем уровне модуля держат определения, а не действия — всё, что там написано, выполнится при первом же импорте, даже если ни одна функция оттуда не понадобится.
-1. **Документация** — в начале модуля (строки документации в тройных кавычках)
-2. **Импорты** — сначала стандартная библиотека, затем сторонние библиотеки, затем собственные модули
-3. **Константы** — глобальные константы
-4. **Классы** — определения классов
-5. **Функции** — определения функций
-6. **Код для выполнения** — код, который должен выполняться при запуске модуля как самостоятельной программы
+## from ... import: имя переезжает к вам
-## Пакеты
+Писать `prices.` перед каждым вызовом надоедает. Форма `from ... import` забирает из модуля конкретное имя и кладёт его прямо в вашу программу. В примере ниже обе формы импорта стоят рядом нарочно, чтобы в последней строке сравнить результат — в обычном коде так не пишут.
-Когда ваш проект становится большим, можно организовать связанные модули в пакеты.
+**prices.py**
-> Пакет в Python — это директория, содержащая модули и файл `__init__.py`, который указывает Python, что эта директория должна рассматриваться как пакет.
+```python
+"""Расчёт цен: НДС и скидки."""
-Пример структуры пакета:
+VAT = 0.2
-```text
-mypackage/
-│
-├── __init__.py
-├── module1.py
-├── module2.py
-└── subpackage/
- ├── __init__.py
- └── module3.py
-```
-### Создание пакета
+def with_vat(price):
+ """Цена с НДС."""
+ return round(price * (1 + VAT), 2)
-1. Создайте директорию с именем пакета
-2. Добавьте файл `__init__.py` (может быть пустым)
-3. Добавьте ваши модули (`.py` файлы)
-Файл `__init__.py` выполняется при импорте пакета. Он может быть пустым или содержать код инициализации:
+def discount(price, percent):
+ """Цена после скидки в процентах."""
+ return round(price * (1 - percent / 100), 2)
-```python
-# mypackage/__init__.py
+```
-"""
-Мой первый пакет.
-"""
+**main.py**
-# Код инициализации
-print("Пакет mypackage загружен")
+```python
+import prices
+from prices import with_vat
-# Переменные, доступные при импорте пакета
-__version__ = '0.1'
+print(with_vat(100)) # 120.0
+print(with_vat is prices.with_vat) # True
-# Можно автоматически импортировать модули при импорте пакета
-from . import module1
-from . import module2
```
-### Импорт из пакета
+Последняя строка — самое важное здесь. Никакой копии функции не появилось: `with_vat` и `prices.with_vat` — два имени одного объекта. `from ... import` не «другой способ импорта», а дополнительное действие: сначала модуль импортируется целиком (со всем своим кодом), потом одно из его имён копируется в ваше пространство имён.
-```python
-# Предположим, что у нас есть пакет mypackage как описано выше
+Итого у вас три формы, и выбирают между ними так:
-# Импорт модуля из пакета
-import mypackage.module1
+- **Функций из модуля нужно много, или важно видеть, откуда взялось имя** — оставьте префикс:
-# Импорт определенных элементов из модуля в пакете
-from mypackage.module2 import some_function
+ ```python
+ import prices
-# Импорт из подпакета
-import mypackage.subpackage.module3
+ print(prices.with_vat(100))
+ ```
-# Относительные импорты (внутри модулей пакета)
-# from . import module2 # импорт из того же каталога
-# from .. import module1 # импорт из родительского каталога
-```
+- **Нужны две-три функции и вызываются они часто** — заберите их по именам, код станет короче:
-## Специальные переменные модуля
+ ```python
+ from prices import with_vat, discount
-В Python модули имеют несколько специальных переменных:
+ print(with_vat(100))
+ ```
-### name
+- **Имя модуля длинное или уже занято вашей переменной** — подключите его под коротким именем:
-Переменная `__name__` позволяет определить, запущен ли модуль как программа или импортирован:
+ ```python
+ import prices as p
-```python
-# mymodule.py
+ print(p.with_vat(100))
+ ```
-def some_function():
- return "Функция выполнена"
+Последняя форма — та самая причина, по которой пишут `import pandas as pd`.
-# Этот код будет выполняться только если модуль запущен напрямую
-if __name__ == "__main__":
- print("Модуль запущен как программа")
- print(some_function())
-else:
- print("Модуль импортирован")
-```
+## Как отличить запуск от импорта
-Если запустить этот модуль напрямую:
+Как мы видели, при импорте выполняется весь код модуля. Значит, код внутри `prices.py` отработает в обоих случаях:
```bash
-$ python mymodule.py
-Модуль запущен как программа
-Функция выполнена
+python prices.py # запускаем сам модуль
+python main.py # запускаем программу, которая его импортирует
```
-Если импортировать этот модуль:
-
-```python
-import mymodule # Выведет: "Модуль импортирован"
-```
+Но иногда код нужен только при прямом запуске файла. Например, проверка «а верно ли считают функции»: автору она удобна, а тому, кто импортирует модуль, только мешает.
-### all
-
-Переменная `__all__` определяет, какие имена будут импортированы при использовании `from module import *`:
+Отличить одно от другого позволяет переменная `__name__`, она есть у каждого модуля. При импорте в ней лежит имя модуля, здесь это `"prices"`. А у файла, который запустили напрямую, там всегда `"__main__"`. Поэтому такое условие истинно только при прямом запуске:
```python
-# mymodule.py
-
-__all__ = ['public_function', 'PUBLIC_CONSTANT']
-
-PUBLIC_CONSTANT = 42
-_PRIVATE_CONSTANT = 43 # Начинается с _, считается приватной
-
-def public_function():
- return "Это публичная функция"
-
-def _private_function():
- return "Это приватная функция"
+if __name__ == "__main__":
+ print("проверка:", with_vat(100))
```
-Теперь:
-
-```python
-from mymodule import *
-# Доступны только: public_function и PUBLIC_CONSTANT
-# _PRIVATE_CONSTANT и _private_function не импортируются
-```
+В `prices.py` ниже эта проверка стоит в самом конце, а `main.py` на соседней вкладке просто импортирует модуль:
-## Лучшие практики при работе с модулями
+**prices.py**
-1. **Один модуль — одна ответственность**: Каждый модуль должен отвечать за одну конкретную функциональность.
+```python
+"""Расчёт цен: НДС и скидки."""
-2. **Понятные имена модулей**: Используйте описательные, но краткие имена (например, `data_processing.py`, `user_interface.py`).
+VAT = 0.2
-3. **Документация**: Каждый модуль должен начинаться с документации, описывающей его назначение.
-4. **Импорты в начале файла**: Все импорты должны быть в начале модуля.
+def with_vat(price):
+ """Цена с НДС."""
+ return round(price * (1 + VAT), 2)
-5. **Явные импорты**: Предпочитайте `from module import specific_thing` вместо `from module import *`.
-6. **Приватные имена**: Начинайте имена "приватных" функций и переменных с подчеркивания (`_private_function`).
+def discount(price, percent):
+ """Цена после скидки в процентах."""
+ return round(price * (1 - percent / 100), 2)
-7. **Тестирование модуля**: Используйте блок `if __name__ == "__main__":` для тестирования модуля.
-## Проверка понимания
+if __name__ == "__main__":
+ print("проверка:", with_vat(100))
-Давайте проверим, насколько хорошо вы усвоили тему модулей:
+```
-**Что будет выведено при выполнении следующего кода?**
+**main.py**
```python
-# file: test_module.py
-def hello():
- return "Привет, мир!"
+import prices
-if __name__ == "__main__":
- print("Модуль запущен напрямую")
-else:
- print("Модуль импортирован")
+print("итог:", prices.with_vat(100))
-# file: main.py
-import test_module
-print(test_module.hello())
```
+```text
+итог: 120.0
+```
+
+Запустили `main.py` — в выводе только его собственная строка: `__name__` внутри `prices.py` равен `"prices"`, условие ложно, проверка промолчала. А при прямом запуске модуля напечаталось бы `проверка: 120.0`.
-Помните, модули и пакеты помогают применять принцип "Не повторяйся" (DRY - Don't Repeat Yourself) и делают ваши программы более профессиональными. Организуйте свой код мудро, и вам будет намного легче развивать его в будущем! 💪
+Так один файл совмещает две роли: набор функций для чужого кода и самостоятельная программа. В этот блок кладут всё, что не должно случаться при импорте: запуск программы, разбор аргументов командной строки, проверки вроде нашей.
diff --git a/course/module-2/oop/article.md b/course/module-2/oop/article.md
index 9d20feb..cd621e8 100644
--- a/course/module-2/oop/article.md
+++ b/course/module-2/oop/article.md
@@ -1,132 +1,83 @@
-# ООП Python
+---
+meta:
+ title: "ООП в Python"
+ description: "Введение в объектно-ориентированное программирование: что такое класс и объект и какие четыре принципа ООП разберут следующие главы."
+---
-Пришло время познакомиться с одной из самых важных парадигм программирования — объектно-ориентированным программированием (ООП).
-Это фундаментальный подход, который поможет вам создавать более организованный, многоразовый и понятный код. 🧩
+# ООП в Python
-## Что такое ООП?
+Программы часто моделируют вещи из реального мира: банковский счёт, пользователя, машину. У каждой такой вещи есть **данные** (баланс, имя, скорость) и **действия** (внести деньги, представиться, ускориться).
-> Объектно-ориентированное программирование — это подход к разработке программ, при котором программа строится как набор объектов, каждый из которых является экземпляром определенного класса, а классы образуют иерархию наследования.
+> Объектно-ориентированное программирование (ООП) — способ описывать данные и действия вместе. Шаблон такой связки называется **классом**, а конкретная вещь, созданная по шаблону, — **объектом**.
-Если объяснять простыми словами: ООП позволяет нам моделировать реальный мир в коде, представляя сущности как "объекты" с характеристиками (данными) и поведением (функциями).
+## Первый пример
-Представьте, что вы описываете автомобиль. У него есть:
+Опишем класс `Car` и создадим по нему две машины. В синтаксис пока не вглядывайтесь: и `class`, и `__init__`, и `self` по шагам разберёт следующая глава. Сейчас важна форма: данные и действия описаны в одном месте.
-- Характеристики: цвет, марка, год выпуска, текущая скорость
-- Поведение: завести двигатель, разогнаться, затормозить, сигналить
-
-В ООП характеристики становятся атрибутами объекта, а поведение — методами.
-
-## Основные принципы ООП
-
-Существует четыре основных принципа ООП:
-
-1. **Инкапсуляция** — объединение данных и методов, которые работают с этими данными, в единый объект и скрытие деталей реализации
-2. **Наследование** — создание новых классов на основе существующих с сохранением их свойств и методов
-3. **Полиморфизм** — возможность использовать объекты с одинаковыми интерфейсами без информации о типе и внутренней структуре объекта
-4. **Абстракция** — выделение важных характеристик объекта и игнорирование несущественных деталей
-
-Мы будем изучать каждый из этих принципов в отдельных статьях, а сейчас давайте посмотрим, как ООП выглядит в Python.
-
-## Пример ООП в Python
-
-Вот простой пример класса `Car` с атрибутами и методами:
-
-```python-executable
+```python
class Car:
def __init__(self, make, model):
- self.make = make # марка
- self.model = model # модель
- self.is_running = False # двигатель заведен или нет
+ self.make = make
+ self.model = model
+ self.is_running = False
def start_engine(self):
- """Запуск двигателя"""
- if not self.is_running:
- self.is_running = True
- return f"{self.make} {self.model}: Двигатель запущен"
- return f"{self.make} {self.model}: Двигатель уже был запущен"
+ self.is_running = True
+ return f"{self.make} {self.model}: двигатель запущен"
def stop_engine(self):
- """Остановка двигателя"""
- if self.is_running:
- self.is_running = False
- return f"{self.make} {self.model}: Двигатель остановлен"
- return f"{self.make} {self.model}: Двигатель уже был остановлен"
+ self.is_running = False
+ return f"{self.make} {self.model}: двигатель остановлен"
-# Создаем экземпляр класса Car
my_car = Car("Toyota", "Corolla")
-
-# Используем методы объекта
-print(my_car.start_engine())
-print(my_car.stop_engine())
-
-# Можем создать еще один экземпляр класса Car
another_car = Car("Honda", "Civic")
+
print(another_car.start_engine())
+
+print(my_car.is_running)
+
+print(another_car.is_running)
+
```
-В этом примере:
+Двигатель запустили только у второй машины, и `is_running` поменялся только у неё. В этом суть: из одного класса создаётся сколько угодно объектов, и у каждого свои данные.
-- `Car` — это класс, который описывает, что такое автомобиль
-- `my_car` и `another_car` — экземпляры (объекты) класса
-- `make`, `model`, `is_running` — атрибуты
-- `start_engine()`, `stop_engine()` — методы
+## Класс и объект
-## Преимущества ООП
+`класс Person` → `объект person1`, `объект person2`.
-ООП предлагает множество преимуществ:
+Термины, которыми это описывают:
-1. **Модульность** — код разделен на логические блоки (классы), которые легче понимать и поддерживать
-2. **Повторное использование** — классы можно использовать многократно в разных частях программы или даже в разных проектах
-3. **Масштабируемость** — легче добавлять новые функции без нарушения существующей функциональности
-4. **Управление сложностью** — сложные системы легче моделировать и понимать
+- `Car` — **класс**: шаблон, по которому создаются объекты
+- `my_car`, `another_car` — **объекты** (экземпляры) этого класса
+- `make`, `model`, `is_running` — **атрибуты**: данные объекта
+- `start_engine()`, `stop_engine()` — **методы**: действия объекта
-## Практический пример: банковская система
+## Четыре принципа ООП
-Давайте создадим простую модель банковского счета:
+ООП традиционно строится на четырёх концепциях. Каждой дальше посвящён отдельный урок (абстракцию разберём внутри урока про полиморфизм), так что пока хватит одной фразы о каждой:
-```python
-class BankAccount:
- def __init__(self, owner, balance=0):
- self.owner = owner
- self.balance = balance
-
- def deposit(self, amount):
- if amount > 0:
- self.balance += amount
- return f"Внесено {amount} руб. Новый баланс: {self.balance} руб."
- return "Сумма депозита должна быть положительной"
-
- def withdraw(self, amount):
- if amount > 0:
- if amount <= self.balance:
- self.balance -= amount
- return f"Снято {amount} руб. Новый баланс: {self.balance} руб."
- return "Недостаточно средств"
- return "Сумма снятия должна быть положительной"
-
- def get_balance(self):
- return f"Баланс счета {self.owner}: {self.balance} руб."
-
-# Создаем банковский счет
-account = BankAccount("Иван Петров", 1000)
-
-# Используем методы счета
-print(account.get_balance())
-print(account.deposit(500))
-print(account.withdraw(200))
-print(account.withdraw(2000)) # Попытка снять больше, чем есть на счете
-```
-
-Этот пример показывает, как с помощью ООП можно смоделировать реальный объект (банковский счет) с его данными (владелец, баланс) и операциями (внесение, снятие, проверка баланса).
+1. **Инкапсуляция** — упаковка данных и методов, которые с этими данными работают, в один объект. Внешний код не лезет в детали реализации, а работает через интерфейс класса.
+2. **Наследование** — создание нового класса на основе существующего с переиспользованием его атрибутов и методов.
+3. **Полиморфизм** — возможность работать с объектами разных классов через единый интерфейс (одинаково вызвать `.area()` у круга, квадрата и треугольника).
+4. **Абстракция** — выделение существенных характеристик объекта, скрытие неважных деталей.
## Проверка понимания
-**Какие основные принципы лежат в основе ООП?**
+**Чем класс отличается от объекта?**
+
+1. **Правильный ответ:** Класс — это шаблон, а объект — созданная по нему конкретная вещь — Из одного класса можно создать сколько угодно объектов, и у каждого будут свои значения атрибутов — как у my_car и another_car свои марки.
+2. Класс и объект — синонимы, разницы нет — Разница есть: класс Car в примере один, а машин по нему создано две, и данные у них разные.
-## Заключение
+3. Объект — это шаблон, а класс создаётся по объекту — Наоборот: шаблон — это класс, а объекты создаются по нему вызовом Car(...).
-Вы познакомились с основами ООП в Python.
-Теперь вы знаете о четырех ключевых принципах: инкапсуляции, наследовании, полиморфизме и абстракции.
+4. Класс хранит данные, а объект — только методы — Данные (атрибуты) хранит каждый объект по отдельности, а класс описывает, какие атрибуты и методы будут у любого объекта.
-В следующих статьях мы рассмотрим каждую из этих концепций подробнее.
+Следующая глава — «Классы»: разберём весь синтаксис, который здесь мелькнул, шаг за шагом.
diff --git a/course/module-2/polymorphism/article.md b/course/module-2/polymorphism/article.md
index 6e57c07..08c3ce4 100644
--- a/course/module-2/polymorphism/article.md
+++ b/course/module-2/polymorphism/article.md
@@ -1,3 +1,9 @@
+---
+meta:
+ title: "Полиморфизм"
+ description: "Один вызов — разное поведение: полиморфизм через наследование, абстрактные классы и утиная типизация."
+---
+
# Полиморфизм
Допустим, у нас есть собака, кошка и утка, и нужно у каждой вызвать `speak()`. Без полиморфизма получится примерно так:
@@ -27,7 +33,7 @@ for animal in animals:
Это самый частый случай: общий родительский класс задаёт «контракт» (какие методы должны быть у потомков), а каждый потомок реализует их по-своему.
-```python-executable
+```python
class Animal:
def __init__(self, name):
self.name = name
@@ -50,21 +56,24 @@ class Duck(Animal):
animals = [Dog("Бобик"), Cat("Мурка"), Duck("Кряк")]
for animal in animals:
print(animal.speak())
-# Вывод:
-# Бобик говорит: Гав!
-# Мурка говорит: Мяу!
-# Кряк говорит: Кря!
+
```
Внутри цикла мы не задаём вопрос «а кто ты такой?» — каждое животное само знает, как ему говорить. Если добавится `class Cow(Animal):` со своим `speak()`, цикл не изменится ни на строчку.
+
+
## Абстрактные классы
-Часто хочется быть уверенным, что **все потомки точно реализовали нужный метод**. Например, базовый `Shape` должен заставлять каждый конкретный класс (Rectangle, Circle, …) реализовать `area()`. Если кто-то забудет, лучше пусть это обнаружится сразу, а не во время работы программы.
+Часто хочется быть уверенным, что **все потомки точно реализовали нужный метод**. Например, базовый `Shape` должен заставлять каждый конкретный класс (Rectangle, Circle, …) реализовать `area()`. Если кто-то забудет, лучше пусть ошибка всплывёт сразу — при попытке создать объект, а не позже, когда программа дойдёт до вызова этого метода.
-Для этого в Python есть **абстрактные базовые классы** из модуля `abc`:
+Для этого в Python есть **абстрактные базовые классы** из модуля `abc`. Обязательные методы помечает знакомый по прошлому уроку приём — декоратор `@abstractmethod`:
-```python-executable
+```python
from abc import ABC, abstractmethod
import math
@@ -79,7 +88,7 @@ class Shape(ABC):
def describe(self):
# Использует area() и perimeter(), не зная, как они реализованы
- return f"Площадь: {self.area():.2f}, периметр: {self.perimeter():.2f}"
+ return f"Площадь: {round(self.area(), 2)}, периметр: {round(self.perimeter(), 2)}"
class Rectangle(Shape):
def __init__(self, width, height):
@@ -103,25 +112,28 @@ class Circle(Shape):
return 2 * math.pi * self.radius
shapes = [Rectangle(5, 3), Circle(4)]
-for i, shape in enumerate(shapes, 1):
- print(f"Фигура {i}: {shape.describe()}")
-# Вывод:
-# Фигура 1: Площадь: 15.00, периметр: 16.00
-# Фигура 2: Площадь: 50.27, периметр: 25.13
+for shape in shapes:
+ print(shape.describe())
+
```
Что нам дал `ABC`:
-- Класс `Shape` нельзя инстанцировать (`Shape()` упадёт). Это и логично: «фигура» сама по себе бессмысленна, нужна конкретная.
-- Если бы `Circle` забыл определить `perimeter()`, Python отказался бы создавать объект `Circle()` с ошибкой ещё на этапе создания, а не позже при вызове несуществующего метода.
+- Создать объект `Shape()` не получится — попытка упадёт с ошибкой. Это и логично: «фигура» сама по себе бессмысленна, нужна конкретная.
+- Если бы `Circle` забыл определить `perimeter()`, Python отказался бы создавать объект `Circle()` с ошибкой ещё на этапе создания, а не позже при вызове несуществующего метода.
При этом метод `describe()` определён прямо в `Shape` и работает для любого потомка — он опирается на `area()` и `perimeter()`, не зная их реализации. Это и есть полиморфизм через абстрактный базовый класс.
+Заодно это четвёртый принцип ООП — **абстракция**: код работает с «фигурой вообще», не зная деталей конкретной. Внешнему коду достаточно знать, _что_ умеет фигура, а не _как_ она это делает.
+
## Утиная типизация
Python идёт ещё дальше: для полиморфизма **не обязательно** общее наследование. Если объект ведёт себя как нужно (имеет правильные методы), он сойдёт. Эту идею формулируют так: «если оно ходит как утка и крякает как утка, то это утка».
-```python-executable
+```python
class Duck:
def swim(self):
return "Утка плывёт."
@@ -141,24 +153,33 @@ def describe(entity):
print(entity.sound())
describe(Duck())
-# Вывод:
-# Утка плывёт.
-# Кря-кря!
+
describe(Person())
-# Вывод:
-# Человек плывёт.
-# Привет!
+
```
Функция `describe` не проверяет, утка перед ней или человек. Ей важно только одно: чтобы у объекта были методы `swim()` и `sound()`. Никакого общего родителя у `Duck` и `Person` нет, но полиморфизм работает.
-Это очень питоновский подход: вместо «опиши тип» — «опиши поведение». На практике он часто экономит лишние слои абстракций.
+Это очень питоновский подход: вместо «опиши тип» — «опиши поведение». На практике он часто позволяет обойтись без лишних классов-родителей.
-## Что дальше?
+## Проверка понимания
-Полиморфизм закрывает четыре принципа ООП. Главный практический эффект: код, который вызывает методы через общий интерфейс, не нужно переписывать, когда появляются новые классы. Расширяемость встроена в саму архитектуру.
+**Что из перечисленного ниже является примером полиморфизма в Python?**
----
+1. **Правильный ответ:** Использование одной и той же функции для обработки объектов разных типов — Это основная идея полиморфизма: единый интерфейс для объектов разных типов.
-**Что из перечисленного ниже является примером полиморфизма в Python?**
+2. Скрытие внутреннего атрибута за подчёркиванием — Это пример инкапсуляции, а не полиморфизма.
+
+3. Создание класса на основе другого класса — Это описывает наследование, а не полиморфизм (хотя наследование часто используется для реализации полиморфизма).
+
+4. Объединение данных и методов в единый объект — Это описывает общий принцип ООП и больше относится к инкапсуляции, чем к полиморфизму.
+
+Полиморфизм закрывает четыре принципа ООП. Главный практический эффект: код, который вызывает методы через общий интерфейс, не нужно переписывать, когда появляются новые классы. Расширяемость встроена в саму архитектуру.
+В следующем уроке отвлечёмся от классов и разберём лямбда-функции — способ записать крошечную функцию в одну строку.
diff --git a/course/module-2/structured-data/article.md b/course/module-2/structured-data/article.md
index 5a1355d..e177837 100644
--- a/course/module-2/structured-data/article.md
+++ b/course/module-2/structured-data/article.md
@@ -1,38 +1,48 @@
-# Форматы JSON и CSV в Python: чтение, запись и анализ данных
+---
+meta:
+ title: "Форматы JSON и CSV в Python: чтение и запись данных"
+ description: "Руководство по работе со структурированными данными в Python: модули json и csv — чтение и запись данных."
+---
-Мы уже рассмотрели основы работы с файлами и текстовыми файлами в Python.
-Теперь мы переходим к структурированным данным — форматам, которые используются для хранения и обмена данными в структурированном виде. 🧩
+# Форматы JSON и CSV в Python: чтение и запись данных
-Мы сосредоточимся на двух наиболее популярных форматах:
+В прошлой главе мы разобрали файл настроек вручную, резая строки по знаку `=`. Для данных посложнее так не делают: есть готовые форматы, которые Python читает и пишет готовыми модулями.
-- **JSON** (JavaScript Object Notation) — формат для обмена данными, широко используемый в веб-приложениях и API
-- **CSV** (Comma-Separated Values) — простой формат для представления табличных данных
+Разберём два самых частых:
-Эти форматы очень распространены и используются во всех областях программирования — от веб-разработки до анализа данных.
+- **JSON** (JavaScript Object Notation) — формат обмена данными в вебе и API
+- **CSV** (Comma-Separated Values) — табличный формат; открывается в Excel и Google Таблицах
## JSON: JavaScript Object Notation
-> JSON (JavaScript Object Notation) — это текстовый формат обмена данными, похожий на словари и списки в Python. Он легко читается как человеком, так и машиной.
+### Что JSON умеет хранить
-### Структура данных JSON
+JSON придумывали как формат обмена между разными языками, поэтому набор типов в нём намеренно скромный, только то, что есть везде:
-JSON поддерживает следующие типы данных:
+| JSON | Python |
+| ---------- | ----------- |
+| объект | dict |
+| строка | str |
+| число | int / float |
+| логическое | bool |
+| null | None |
+| массив | list |
-- Объекты (словари): `{"name": "Alice", "age": 30}`
-- Массивы (списки): `[1, 2, 3, 4]`
-- Строки: `"Hello, world!"`
-- Числа: `42`, `3.14`
-- Логические значения: `true`, `false`
-- `null` (в Python соответствует `None`)
+Соответствие почти один в один: только у числа в Python два варианта — целое становится `int`, дробное — `float`. На этом и держится удобство работы с JSON из Python.
-### Модуль json в Python
+### Модуль json
-Python предоставляет встроенный модуль `json` для работы с этим форматом:
+В стандартной библиотеке для формата есть модуль `json`, и вся работа с ним сводится к двум действиям: превратить объект Python в JSON и разобрать JSON обратно в объект.
+
+У модуля четыре функции. `dump` и `load` работают с файлом: первая записывает объект Python, вторая читает его обратно. `dumps` и `loads` делают то же самое со строкой: буква `s` на конце как раз от слова **string**.
+
+`строка JSON` ⇄ `json.loads` / `json.dumps` ⇄ `объект Python` ⇄ `json.load` / `json.dump` ⇄ `файл JSON`
+
+Начнём со строк: так виден сам формат. У `dumps` есть два аргумента, которые почти всегда стоит указывать. `ensure_ascii=False` оставляет кириллицу кириллицей. Без него «Анна» уедет в строку как `\u0410\u043d\u043d\u0430`: формально верно, а читать невозможно. А `indent=2` расставляет переносы и отступы, чтобы результат можно было разглядывать глазами.
```python
import json
-# Простой словарь Python
person = {
"name": "Анна",
"age": 28,
@@ -40,242 +50,178 @@ person = {
"languages": ["Python", "JavaScript"]
}
-# Преобразование словаря Python в строку JSON
json_string = json.dumps(person, ensure_ascii=False, indent=2)
-print("JSON строка:")
print(json_string)
+
-# Преобразование строки JSON обратно в объект Python
parsed_data = json.loads(json_string)
-print("\nПреобразовано обратно в Python:")
-print(f"Имя: {parsed_data['name']}")
-print(f"Возраст: {parsed_data['age']}")
-print(f"Языки программирования: {', '.join(parsed_data['languages'])}")
+print(f"Имя: {parsed_data['name']}, языки: {parsed_data['languages']}")
+
```
### Запись JSON в файл и чтение из файла
-Вот как можно сохранить данные в JSON-файл и затем прочитать их:
+Те же данные обычно нужно не просто превратить в строку, а положить на диск. Тут в дело идёт пара без `s`: `dump` пишет объект прямо в открытый файл, `load` читает его оттуда. Промежуточная строка не нужна.
```python
import json
-# Данные о студентах
students = [
{"id": 1, "name": "Иван", "scores": [85, 90, 78]},
{"id": 2, "name": "Мария", "scores": [92, 88, 95]}
]
-# Запись в файл
with open('students.json', 'w', encoding='utf-8') as file:
json.dump(students, file, ensure_ascii=False, indent=2)
- print("Данные записаны в файл students.json")
-# Чтение из файла
with open('students.json', 'r', encoding='utf-8') as file:
loaded_students = json.load(file)
- print(f"\nЗагружено {len(loaded_students)} студентов:")
- for student in loaded_students:
- avg_score = sum(student['scores']) / len(student['scores'])
- print(f" {student['name']}: средний балл {avg_score:.1f}")
+for student in loaded_students:
+ print(f" {student['name']}: оценки {student['scores']}")
+
```
-### Основные методы модуля json
-
-| Метод | Описание |
-| ---------------------- | --------------------------------------- |
-| `json.dumps(obj)` | Преобразует объект Python в строку JSON |
-| `json.loads(str)` | Преобразует строку JSON в объект Python |
-| `json.dump(obj, file)` | Записывает объект Python в JSON-файл |
-| `json.load(file)` | Читает JSON из файла в объект Python |
+## CSV: Comma-Separated Values
-Параметр `ensure_ascii=False` позволяет корректно сохранять русские буквы и другие символы Unicode, а `indent` делает вывод более читаемым.
+### Модуль csv
-## CSV: Comma-Separated Values
+Ниже два файла: `people.csv` с маленькой таблицей и `main.py`, который её читает. Начнём с вкладки `people.csv`. Это основа формата: первая строка — заголовки, каждая следующая — строка таблицы, столбцы разделены запятыми.
-> CSV (Comma-Separated Values) — это простой текстовый формат для представления табличных данных, где строки таблицы — это строки файла, а столбцы разделены запятыми (или другими разделителями).
+Глядя на такой файл, легко решить, что никакой модуль не нужен: режем строку через `split(",")`, и готово. Так и работает, но только до первой запятой внутри самого значения: она рано или поздно встретится в адресе или описании товара. Модуль `csv` знает правила формата и берёт эти хлопоты на себя. В `main.py` файл читает `csv.reader`:
-CSV выглядит примерно так:
+**people.csv**
-```python
+```csv
Имя,Возраст,Город
Анна,28,Москва
Иван,35,Санкт-Петербург
-```
+Мария,22,Казань
-### Модуль csv в Python
+```
-Python предоставляет встроенный модуль `csv` для работы с этим форматом:
+**main.py**
```python
import csv
-# Данные для записи
-data = [
- ['Имя', 'Возраст', 'Город'], # Заголовки
- ['Анна', '28', 'Москва'],
- ['Иван', '35', 'Санкт-Петербург'],
- ['Мария', '22', 'Казань']
-]
-
-# Запись в CSV файл
-with open('people.csv', 'w', newline='', encoding='utf-8') as file:
- writer = csv.writer(file)
- writer.writerows(data)
- print("Данные записаны в файл people.csv")
-
-# Чтение из CSV файла
with open('people.csv', 'r', encoding='utf-8') as file:
reader = csv.reader(file)
+ next(reader) # пропускаем строку заголовков
- # Чтение заголовков (первая строка)
- headers = next(reader)
- print(f"\nЗаголовки: {headers}")
-
- # Чтение данных
- print("\nДанные:")
for row in reader:
print(f" {row[0]}, {row[1]} лет, город {row[2]}")
+
+```
+
+Запустим `main.py` — в выводе:
+
+```text
+ Анна, 28 лет, город Москва
+ Иван, 35 лет, город Санкт-Петербург
+ Мария, 22 лет, город Казань
```
-### Использование DictReader и DictWriter
+### Запятая внутри значения
-Для более удобной работы с CSV можно использовать `DictReader` и `DictWriter`,
-которые позволяют работать с данными как со словарями:
+Вернёмся к обещанной проблеме. Что произойдёт, если запятая окажется в самом значении, например в описании «лёгкий, тонкий»? Наивный `split(",")` разрезал бы такое значение надвое: один столбец превратился бы в два, и вся строка поехала бы вправо.
+
+Формат решает это кавычками: значение с запятой берётся в кавычки, и разделителем считается только запятая снаружи них. Расставлять их руками не нужно: запись таблицы в файл — работа `csv.writer`, и кавычки он добавит сам, а `csv.reader` при чтении снимет.
+
+Файл для записи открывают с параметром `newline=''`: так переводами строк управляет сам модуль `csv`, иначе на Windows между строками таблицы появились бы пустые.
+
+Посмотрим, как это выглядит в файле:
```python
import csv
-# Запись в CSV с использованием DictWriter
-data = [
- {'Имя': 'Алексей', 'Профессия': 'Инженер', 'Зарплата': 85000},
- {'Имя': 'Екатерина', 'Профессия': 'Дизайнер', 'Зарплата': 75000},
- {'Имя': 'Сергей', 'Профессия': 'Программист', 'Зарплата': 110000}
+rows = [
+ ['Товар', 'Описание', 'Цена'],
+ ['Ноутбук', 'лёгкий, тонкий', '45000'],
]
-with open('employees.csv', 'w', newline='', encoding='utf-8') as file:
- fieldnames = ['Имя', 'Профессия', 'Зарплата']
- writer = csv.DictWriter(file, fieldnames=fieldnames)
+with open('quoted.csv', 'w', newline='', encoding='utf-8') as file:
+ csv.writer(file).writerows(rows)
- writer.writeheader() # Запись заголовков
- writer.writerows(data) # Запись данных
- print("Данные сотрудников записаны в файл")
+with open('quoted.csv', 'r', encoding='utf-8') as file:
+ print(file.read(), end="")
+
+```
-# Чтение из CSV с использованием DictReader
-with open('employees.csv', 'r', encoding='utf-8') as file:
- reader = csv.DictReader(file)
+Кавычки вокруг «лёгкий, тонкий» появились сами, и столбцов в строке осталось три.
- print("\nСотрудники:")
- for row in reader:
- print(f" {row['Имя']} - {row['Профессия']}, зарплата: {row['Зарплата']} руб.")
+Кавычки — не единственная тонкость формата: несмотря на название, разделителем не всегда служит запятая. В выгрузках из русской версии Excel это чаще всего `;`, потому что запятая там занята под десятичный разделитель. Разделитель задают параметром `delimiter`, причём одинаково при записи и при чтении:
+
+```python
+writer = csv.writer(file, delimiter=';')
+reader = csv.reader(file, delimiter=';')
```
-### Основные особенности работы с CSV
+Иначе вся строка вернётся одним куском вместо отдельных столбцов.
-1. **Разделители**: Хотя CSV расшифровывается как "значения, разделённые запятыми", на практике могут использоваться и другие разделители (точка с запятой, табуляция)
-2. **Кавычки**: Если значение содержит разделитель или кавычки, оно заключается в кавычки
-3. **Экранирование**: Если внутри значения есть кавычки, они экранируются
+### Обращение к столбцам по имени
-```python
-import csv
+До сих пор строки приходили к нам списками, и до нужного значения мы добирались по номеру: `row[0]`, `row[1]`, `row[2]`. Пока столбцов три и файл ваш собственный, это терпимо. Но стоит кому-то добавить в выгрузку новый столбец в середину — и все номера съедут. Программа при этом не упадёт: она молча начнёт печатать город там, где ожидался возраст. Неправильный результат без ошибки найти труднее, чем падение.
-# Пример с другим разделителем
-data = [
- ['Товар', 'Цена', 'Наличие'],
- ['Ноутбук', '45000', 'Да'],
- ['Смартфон', '25000', 'Нет']
-]
+От номеров избавляет `DictReader`. Он смотрит на строку заголовков и отдаёт каждую строку словарём, где ключ — имя столбца. Прочитаем им тот же `people.csv`:
-# Запись с использованием точки с запятой
-with open('products.csv', 'w', newline='', encoding='utf-8') as file:
- writer = csv.writer(file, delimiter=';')
- writer.writerows(data)
- print("Данные записаны с разделителем ';'")
+**people.csv**
-# Чтение с указанием правильного разделителя
-with open('products.csv', 'r', encoding='utf-8') as file:
- reader = csv.reader(file, delimiter=';')
- for row in reader:
- print(' '.join(row))
-```
+```csv
+Имя,Возраст,Город
+Анна,28,Москва
+Иван,35,Санкт-Петербург
+Мария,22,Казань
-## Практический пример: Анализ данных о продажах
+```
-Рассмотрим пример, где мы сначала сохраняем данные о продажах в CSV, а затем анализируем их и сохраняем результаты в JSON:
+**main.py**
```python
import csv
-import json
-# Создаем данные о продажах
-sales = [
- ['Дата', 'Товар', 'Категория', 'Цена', 'Количество'],
- ['2023-01-05', 'Ноутбук HP', 'Электроника', '45000', '2'],
- ['2023-01-10', 'Смартфон Apple', 'Электроника', '85000', '3'],
- ['2023-01-15', 'Книга "Python"', 'Книги', '1200', '5'],
- ['2023-02-10', 'Микроволновка', 'Бытовая техника', '7000', '1']
-]
+with open('people.csv', 'r', encoding='utf-8') as file:
+ for row in csv.DictReader(file):
+ print(f" {row['Имя']}, {row['Возраст']} лет, город {row['Город']}")
-# Шаг 1: Сохраняем данные в CSV
-with open('sales.csv', 'w', newline='', encoding='utf-8') as file:
- writer = csv.writer(file)
- writer.writerows(sales)
- print("Данные о продажах сохранены в CSV")
+```
-# Шаг 2: Читаем и анализируем данные
-with open('sales.csv', 'r', encoding='utf-8') as file:
- reader = csv.reader(file)
- headers = next(reader) # Пропускаем заголовки
+Запустим `main.py` — в выводе:
- # Подготовка переменных для анализа
- total_revenue = 0
- sales_by_category = {}
+```text
+ Анна, 28 лет, город Москва
+ Иван, 35 лет, город Санкт-Петербург
+ Мария, 22 лет, город Казань
+```
- # Анализ данных
- for row in reader:
- date, product, category, price, quantity = row
- revenue = float(price) * int(quantity)
-
- # Общая выручка
- total_revenue += revenue
-
- # Выручка по категориям
- if category in sales_by_category:
- sales_by_category[category] += revenue
- else:
- sales_by_category[category] = revenue
-
- # Вывод результатов анализа
- print(f"\nОбщая выручка: {total_revenue} руб.")
- print("\nВыручка по категориям:")
- for category, rev in sales_by_category.items():
- print(f" {category}: {rev} руб.")
-
-# Шаг 3: Сохраняем результаты анализа в JSON
-results = {
- "total_revenue": total_revenue,
- "sales_by_category": sales_by_category
-}
+Результат тот же, но в коде вместо номеров стоят имена столбцов, и новый столбец в файле ничего не сдвинет. Для записи словарей есть парный `DictWriter`: он устроен так же, только заранее просит список имён столбцов.
-with open('sales_analysis.json', 'w', encoding='utf-8') as file:
- json.dump(results, file, ensure_ascii=False, indent=2)
- print("\nРезультаты анализа сохранены в JSON")
+## Проверка понимания
-# Шаг 4: Проверяем сохраненный JSON
-with open('sales_analysis.json', 'r', encoding='utf-8') as file:
- saved_results = json.load(file)
- print("\nСодержимое JSON файла с результатами:")
- print(json.dumps(saved_results, ensure_ascii=False, indent=2))
-```
+**Чем `json.dumps` отличается от `json.dump`?**
-В этом примере мы:
+1. dumps работает со словарями, а dump со списками — Тип данных тут ни при чём: обе функции одинаково работают и со словарями, и со списками.
-1. Создали CSV-файл с данными о продажах
-2. Прочитали данные и рассчитали выручку по категориям
-3. Сохранили результаты анализа в JSON-файл
-4. Прочитали сохраненный JSON, чтобы убедиться в корректности
+2. dumps сохраняет кириллицу, а dump заменяет её кодами — За кириллицу отвечает отдельный аргумент \`ensure_ascii=False\`, и он есть у обеих функций.
-## Проверка понимания
+3. **Правильный ответ:** dumps возвращает строку, а dump пишет прямо в открытый файл — Буква \`s\` на конце значит string: такая функция отдаёт строку в память. Без \`s\` функция принимает открытый файл и пишет данные туда.
-**Какой код правильно считывает данные из JSON файла в Python?**
+4. Разницы нет, это два имени одной функции — Функции разные: \`dumps\` возвращает строку, \`dump\` пишет в открытый файл.
diff --git a/course/module-2/text-files/article.md b/course/module-2/text-files/article.md
index bebebfe..0edaee0 100644
--- a/course/module-2/text-files/article.md
+++ b/course/module-2/text-files/article.md
@@ -1,236 +1,103 @@
-# Текстовые файлы: кодировки, эффективная обработка и анализ
+---
+meta:
+ title: "Текстовые файлы в Python: кодировки и разбор текста"
+ description: "Кодировка UTF-8 и что бывает, когда кодировки не совпали. Разбор конфигурационного файла построчно."
+---
-В предыдущей статье мы рассмотрели основы работы с файлами в Python.
-Сейчас мы углубимся в работу с текстовыми файлами, которые являются одним из самых распространенных типов файлов в программировании. 📝
+# Текстовые файлы: кодировки и разбор текста
-Текстовые файлы используются повсеместно: для хранения конфигураций, логов, данных, исходного кода программ и многого другого.
-Умение эффективно работать с ними — важный навык для любого программиста.
+В предыдущей статье мы разобрали основы работы с файлами. Здесь — то, что касается именно текста: кодировки и разбор содержимого построчно.
-## Особенности работы с текстовыми файлами
+## Кодировки
-Текстовые файлы хранят последовательности символов, организованные в строки. При работе с ними в Python нужно учитывать несколько важных моментов:
+На диске файл — это просто последовательность байтов. Чтобы из них получились буквы, нужно знать, по какой схеме переводить байты обратно в символы. Эта схема и называется кодировкой, а придумано их много: разные языки исторически получили разные.
-### Кодировки символов
-
-> Кодировка — это способ представления символов в виде байтов. Разные кодировки используют разные схемы для отображения символов.
-
-В современном мире существует множество различных языков и алфавитов.
-Чтобы компьютер мог работать с текстом на разных языках, были разработаны различные системы кодирования символов:
-
-- **ASCII** — самая простая кодировка, содержит только латинские буквы, цифры и базовые символы (всего 128 символов)
-- **UTF-8** — современный стандарт, поддерживающий все языки мира (включая эмодзи 😊)
-- **Windows-1251** (cp1251) — кодировка для кириллицы, популярная в Windows
-
-В Python рекомендуется всегда использовать UTF-8, особенно если ваш текст содержит не только английские буквы:
+Современный стандарт — **UTF-8**: он покрывает все алфавиты мира разом, включая эмодзи 😊. Правило для новичка простое: писать и читать в UTF-8, указывая кодировку явно параметром `encoding`.
```python
-# Запись текста в разных кодировках
-text = "Привет, мир! Hello, world! 你好,世界!"
+text = "Привет, мир! Hello, world!"
-# Запись в UTF-8 (стандарт для международных текстов)
with open('text_utf8.txt', 'w', encoding='utf-8') as file:
file.write(text)
-# Запись в ASCII (только английские буквы)
-try:
- with open('text_ascii.txt', 'w', encoding='ascii') as file:
- file.write(text)
-except UnicodeEncodeError as e:
- print(f"Ошибка кодирования ASCII: {e}")
-
-# Запись в cp1251 (кириллица для Windows)
-with open('text_cp1251.txt', 'w', encoding='cp1251') as file:
- # Китайские символы будут заменены на '?'
- file.write(text)
+with open('text_utf8.txt', 'r', encoding='utf-8') as file:
+ print(file.read())
+
```
-Как видите, попытка записать русский или китайский текст в ASCII кодировке вызывает ошибку, поскольку ASCII поддерживает только английские символы.
-
-### Чтение с правильной кодировкой
-
-При чтении файла важно указать ту же кодировку, с которой он был создан:
+Проблемы начинаются, когда кодировки не совпали: файл записан по одной схеме, а читают его по другой. Попробуем прочитать наш файл как `ascii` — старую кодировку, в которой есть только латиница, цифры и знаки препинания:
```python
-# Чтение файла с указанием кодировки
-with open('text_utf8.txt', 'r', encoding='utf-8') as file:
- content = file.read()
- print(f"Содержимое в UTF-8: {content}")
+with open('text_utf8.txt', 'w', encoding='utf-8') as file:
+ file.write("Привет, мир! Hello, world!")
-# Чтение с неправильной кодировкой может привести к ошибкам
try:
with open('text_utf8.txt', 'r', encoding='ascii') as file:
- content = file.read()
- print(f"Содержимое с неправильной кодировкой: {content}")
+ print(file.read())
except UnicodeDecodeError as e:
print(f"Ошибка декодирования: {e}")
+
```
-### Определение кодировки файла
+Первый же байт кириллической буквы не укладывается в `ascii`, и Python останавливается с `UnicodeDecodeError`. Обратная ситуация выглядит так же: если записывать кириллицу в кодировку, которая её не знает, будет ошибка `UnicodeEncodeError`.
-Иногда вы получаете текстовый файл и не знаете, в какой кодировке он сохранен.
-Python не может автоматически определить кодировку, но есть библиотеки, которые
-помогут в этом:
+Конструкция `try/except` здесь нужна лишь для того, чтобы программа не оборвалась на ошибке. Ей посвящён отдельный урок дальше в курсе, так что сейчас вникать в неё не обязательно.
-```python
-# Библиотека chardet для определения кодировки
-import chardet
-
-# Создадим файл в cp1251
-with open('text_cp1251.txt', 'w', encoding='cp1251') as file:
- file.write("Привет, мир!")
-
-# Прочитаем файл как байты и определим кодировку
-with open('text_cp1251.txt', 'rb') as file:
- raw_data = file.read()
- result = chardet.detect(raw_data)
- print(f"Определенная кодировка: {result}")
-
- # Теперь можем открыть файл с правильной кодировкой
- encoding = result['encoding']
- with open('text_cp1251.txt', 'r', encoding=encoding) as text_file:
- content = text_file.read()
- print(f"Правильно прочитанное содержимое: {content}")
-```
-
-## Эффективное чтение больших текстовых файлов
+## Разбираем конфигурационный файл
-При работе с большими текстовыми файлами важно использовать методы, которые не загружают весь файл в память.
-Это особенно критично, когда вы работаете с файлами размером в сотни мегабайт или гигабайты.
+Настройки программ часто хранят в текстовом файле: каждая строка — пара «ключ = значение». Ниже два файла рядом: `config.ini` с настройками и `main.py`, который собирает из них словарь.
-### Почему не стоит читать весь файл целиком?
+**config.ini**
-Когда вы используете метод `read()` без аргументов, Python загружает весь файл в память:
+```text
+theme = dark
+language = ru
+autosave = True
-```python
-with open('big_file.txt', 'r') as file:
- content = file.read() # Весь файл загружается в память!
```
-Это может вызвать проблемы:
-
-1. **Потребление памяти** — если файл очень большой (например, гигабайты), он может занять всю доступную оперативную память, что приведет к замедлению или даже сбою программы
-2. **Задержка** — чтение всего файла сразу занимает время, и ваша программа будет "зависать" до завершения чтения
-3. **Неэффективность** — часто для обработки нужны не все данные сразу, а последовательный доступ к ним
-
-### Построчное чтение
-
-Более эффективный подход — читать файл построчно с помощью цикла.
-Python будет загружать в память только одну строку за раз:
+**main.py**
```python
-# Создадим тестовый файл с большим количеством строк
-with open('big_file.txt', 'w') as file:
- for i in range(1000):
- file.write(f"Строка номер {i+1}\n")
-
-# Эффективное чтение по строкам
-with open('big_file.txt', 'r') as file:
- line_count = 0
- for line in file: # Итерируем по строкам, не загружая весь файл в память
- line_count += 1
- if line_count <= 5: # Показываем только первые 5 строк
- print(line.strip())
- print(f"Всего строк: {line_count}")
-```
-
-Преимущества такого подхода:
-
-- В памяти находится только одна строка за раз
-- Обработка начинается немедленно, не нужно ждать загрузки всего файла
-- Можно прервать чтение в любой момент, если вы нашли нужные данные
-
-### Чтение блоками
-
-Если вам нужен еще больший контроль над процессом чтения, вы можете читать файл
-фиксированными блоками:
-
-```python
-# Чтение файла блоками
-with open('big_file.txt', 'r') as file:
- block_size = 100 # Размер блока в байтах
- blocks_read = 0
-
- while True:
- block = file.read(block_size)
- if not block: # Если блок пустой, значит достигнут конец файла
- break
-
- blocks_read += 1
- if blocks_read <= 2: # Показываем только первые 2 блока
- print(f"Блок {blocks_read}: {block[:50]}...") # Выводим начало блока
-
- print(f"Всего прочитано блоков: {blocks_read}")
-```
-
-Этот метод позволяет контролировать количество памяти, используемой для чтения файла.
-Размер блока можно настроить в зависимости от ваших потребностей и доступной памяти.
-
-## Практический пример: Чтение и обработка конфигурационного файла
-
-Давайте рассмотрим простой практический пример: чтение конфигурационного файла и использование его параметров в программе:
-
-```python
-# Создадим пример конфигурационного файла
-config_text = """
-# Параметры базы данных
-database_host = localhost
-database_port = 5432
-database_name = myapp
-database_user = admin
-database_password = secret123
-
-# Параметры веб-сервера
-server_port = 8080
-debug_mode = True
-log_level = INFO
-"""
-
-with open('config.ini', 'w') as config_file:
- config_file.write(config_text)
-
-# Чтение и обработка конфигурации
def read_config(filename):
config = {}
- with open(filename, 'r') as file:
+ with open(filename, 'r', encoding='utf-8') as file:
for line in file:
- # Пропускаем пустые строки и комментарии
- line = line.strip()
- if not line or line.startswith('#'):
- continue
-
- # Разделяем ключ и значение
- if '=' in line:
- key, value = line.split('=', 1)
- config[key.strip()] = value.strip()
+ key, value = line.split('=', 1) # режем только по первому '='
+ config[key.strip()] = value.strip()
return config
-# Считываем конфигурацию
-app_config = read_config('config.ini')
-# Используем параметры
-print("Конфигурация приложения:")
-print(f"База данных: {app_config['database_name']} на {app_config['database_host']}:{app_config['database_port']}")
-print(f"Пользователь БД: {app_config['database_user']}")
-print(f"Порт веб-сервера: {app_config['server_port']}")
-print(f"Режим отладки: {app_config['debug_mode']}")
-```
+settings = read_config('config.ini')
-В этом примере мы:
+print(settings)
+print(f"Тема оформления: {settings['theme']}, язык: {settings['language']}")
-1. Создали файл конфигурации с параметрами
-2. Написали функцию для чтения и обработки файла построчно
-3. Извлекли нужные параметры и использовали их в программе
+```
+
+Запустим `main.py` — в выводе:
-Такой подход часто используется в реальных приложениях для хранения настроек в удобочитаемом формате.
+```text
+{'theme': 'dark', 'language': 'ru', 'autosave': 'True'}
+Тема оформления: dark, язык: ru
+```
## Проверка понимания
-Давайте проверим, насколько хорошо вы усвоили тему чтения и записи текстовых файлов:
+**Файл записан в UTF-8, а открыли его с `encoding='ascii'`. Что произойдёт?**
+
+1. **Правильный ответ:** Python остановит программу ошибкой UnicodeDecodeError — Байты кириллицы не укладываются в ascii, и угадывать Python не станет: чтение прервётся на первом же неподходящем байте.
+
+2. Файл прочитается, но кириллица превратится в кракозябры — Кракозябры получаются, когда байты формально подходят под другую кодировку. В ascii байтов кириллицы нет вовсе, поэтому Python сообщает об ошибке, а не выдаёт мусор.
-**Как правильно открыть файл для чтения с указанием кодировки UTF-8?**
+3. Python сам определит настоящую кодировку и прочитает верно — Определять кодировку по содержимому Python не умеет. Он использует ту, которую указали, а если не указать — кодировку по умолчанию, что на чужом файле часто и приводит к ошибке.
+4. Прочитается только начало файла, до первой кириллической буквы — Частичного результата не будет: ошибка возникает при попытке декодировать неподходящий байт, и \`read()\` не вернёт ничего.
-Теперь вы знаете основные принципы работы с текстовыми файлами в Python.
-В следующей статье мы рассмотрим работу со структурированными форматами данных, такими как JSON и CSV. До встречи! 👋
+В следующей статье посмотрим структурированные форматы данных — JSON и CSV.
diff --git a/course/module-2/third-party-libraries/article.md b/course/module-2/third-party-libraries/article.md
index 1c26e8b..d8e8696 100644
--- a/course/module-2/third-party-libraries/article.md
+++ b/course/module-2/third-party-libraries/article.md
@@ -1,193 +1,196 @@
+---
+meta:
+ title: "Сторонние библиотеки и pip"
+ description: "Зачем нужны сторонние библиотеки Python, как их ставить через pip, зачем каждому проекту своё виртуальное окружение и как зафиксировать зависимости в requirements.txt."
+---
+
# Сторонние библиотеки и pip
-Встроенные библиотеки Python — это только базовый набор инструментов, как отвертка и молоток. Но для создания действительно крутых проектов вам нужны специализированные инструменты. И здесь на помощь приходят сторонние библиотеки! 🛠️
+Допустим, у нас на руках выгрузка продаж: список чеков, в каждом товар, количество и цена. Нужна выручка по каждому товару.
-## Что такое сторонние библиотеки?
+Всё необходимое мы уже проходили — словарь и цикл:
-> Сторонние библиотеки — это модули Python, которые не входят в стандартную библиотеку и разрабатываются независимыми разработчиками или организациями.
+```python
+sales = [
+ {"товар": "Кофе", "количество": 3, "цена": 150},
+ {"товар": "Хлеб", "количество": 2, "цена": 60},
+ {"товар": "Кофе", "количество": 1, "цена": 150},
+ {"товар": "Молоко", "количество": 4, "цена": 45},
+ {"товар": "Хлеб", "количество": 2, "цена": 60},
+]
-Сторонние библиотеки помогают:
+revenue = {}
+for row in sales:
+ revenue[row["товар"]] = revenue.get(row["товар"], 0) + row["количество"] * row["цена"]
-- Решать специфические задачи в разных областях
-- Экономить время за счёт использования готовых решений
-- Упрощать сложные операции, требующие глубоких знаний
-- Создавать более мощные и функциональные приложения
+print(revenue)
+
+```
-## Что такое pip?
+Работает. Трудности начнутся со следующей просьбы: отсортируй по убыванию, посчитай средний чек, выброси позиции дешевле сотни, сгруппируй ещё и по дням. Каждый пункт — ещё один цикл, и через час перед вами триста строк, которые страшно трогать.
-> pip (Package Installer for Python) — это система управления пакетами, которая используется для установки и управления программными пакетами, написанными на Python.
+Задача при этом типовая, её решают тысячи людей ежедневно. Поэтому под неё давно написан инструмент: библиотека `pandas`.
-pip позволяет:
+## То же самое на pandas
-- Устанавливать пакеты из Python Package Index (PyPI) и других источников
-- Обновлять и удалять пакеты
-- Управлять зависимостями (другими пакетами, необходимыми для работы)
-- Создавать список зависимостей проекта для их последующей установки
+```python
+import pandas as pd
-## Установка и использование pip
+sales = [
+ {"товар": "Кофе", "количество": 3, "цена": 150},
+ {"товар": "Хлеб", "количество": 2, "цена": 60},
+ {"товар": "Кофе", "количество": 1, "цена": 150},
+ {"товар": "Молоко", "количество": 4, "цена": 45},
+ {"товар": "Хлеб", "количество": 2, "цена": 60},
+]
-Обычно pip уже установлен вместе с Python. Проверить наличие и версию pip можно так:
+table = pd.DataFrame(sales)
+table["сумма"] = table["количество"] * table["цена"]
-```bash
-pip --version
+print(table.groupby("товар")["сумма"].sum().to_string())
+
```
-Примерный результат:
-
-```bash
-pip 23.1.2 from /usr/local/lib/python3.11/site-packages/pip (python 3.11)
+Цикл свернулся в одну строку, которая читается почти по-русски: сгруппируй по товару, возьми колонку «сумма», сложи. А сортировка, ради которой пришлось бы дописывать код, — это ещё один вызов в той же цепочке:
+
+```python
+import pandas as pd
+
+sales = [
+ {"товар": "Кофе", "количество": 3, "цена": 150},
+ {"товар": "Хлеб", "количество": 2, "цена": 60},
+ {"товар": "Кофе", "количество": 1, "цена": 150},
+ {"товар": "Молоко", "количество": 4, "цена": 45},
+ {"товар": "Хлеб", "количество": 2, "цена": 60},
+]
+
+table = pd.DataFrame(sales)
+table["сумма"] = table["количество"] * table["цена"]
+
+print(table.groupby("товар")["сумма"].sum().sort_values(ascending=False).to_string())
+
```
-### Основные команды pip
+Вот ради чего нужны сторонние библиотеки. Кто-то уже прошёл путь от «сложить числа по группам» до «отфильтровать, пересчитать и построить график», отладил это на тысячах чужих проектов и отдал вам готовым.
-#### Установка пакета
+## Откуда они берутся
-```bash
-pip install requests
-```
+Вместе с Python приезжает только стандартная библиотека, а `pandas` в неё не входит. На чистой машине строка `import pandas` закончится ошибкой `ModuleNotFoundError`, пока пакет не установлен.
+
+> Сторонние библиотеки пишут независимые разработчики и компании, а выкладывают в общий каталог [PyPI](https://pypi.org/) (Python Package Index). Оттуда их и забирают к себе в проект.
-Примерный результат:
+Скачивает и ставит их `pip` — менеджер пакетов, который приезжает вместе с Python. Проверить, что он на месте:
```bash
-Collecting requests
- Downloading requests-2.31.0-py3-none-any.whl (62 kB)
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 62.6/62.6 kB 1.2 MB/s eta 0:00:00
-Installing collected packages: requests
-Successfully installed requests-2.31.0
+pip --version
```
-#### Установка определенной версии
+## Куда ставить: сначала окружение
-```bash
-pip install requests==2.25.1
-```
+По умолчанию `pip` кладёт пакет туда, где стоит сам Python, и с этого момента пакет виден всем вашим программам сразу. Пока программа одна, это удобно. Когда их становится две, выясняется, что старой нужен `django 3.0`, новой `django 4.2`, а лежать в одном месте две версии не могут. Обновили ради новой — сломали старую.
-Примерный результат:
+Поэтому под каждый проект заводят отдельное виртуальное окружение: свой набор пакетов, который ничего не знает о соседях.
-```bash
-Collecting requests==2.25.1
- Downloading requests-2.25.1-py2.py3-none-any.whl (61 kB)
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 61.2/61.2 kB 1.8 MB/s eta 0:00:00
-Installing collected packages: requests
-Successfully installed requests-2.25.1
-```
+> Виртуальное окружение (venv) — изолированная копия Python со своей папкой пакетов. Проекты перестают делить зависимости и не могут сломать друг друга.
-#### Список установленных пакетов
+
-```bash
-pip list
-```
+Создаём окружение и входим в него:
-Примерный результат:
+**macOS / Linux**
-```bash
-Package Version
----------- ---------
-certifi 2023.5.7
-charset-normalizer 3.1.0
-idna 3.4
-pip 23.1.2
-requests 2.31.0
-setuptools 67.8.0
-urllib3 2.0.3
+```sh
+python -m venv myenv
+source myenv/bin/activate
```
-#### Удаление пакета
+**Windows**
-```bash
-pip uninstall requests -y
+```powershell
+python -m venv myenv
+myenv\Scripts\activate
```
-Примерный результат:
+Что вы внутри, видно по началу строки, где появляется имя окружения:
```bash
-Found existing installation: requests 2.31.0
-Uninstalling requests-2.31.0:
- Successfully uninstalled requests-2.31.0
+(myenv) $
```
-## Популярные сторонние библиотеки
+## Ставим пакет
-Python имеет огромное количество сторонних библиотек для самых разных задач. Вот некоторые из наиболее популярных и полезных:
-
-| Библиотека | Описание | Основное применение | Ссылка |
-| ------------------ | --------------------------------------------------------- | ------------------------------------------------ | ----------------------------------------------------------------- |
-| **Requests** | Удобная работа с HTTP-запросами | Взаимодействие с веб-ресурсами, API | [Документация](https://requests.readthedocs.io/) |
-| **Pandas** | Мощный инструмент для анализа данных | Обработка и анализ табличных данных | [Документация](https://pandas.pydata.org/) |
-| **NumPy** | Работа с массивами и математическими вычислениями | Научные вычисления, работа с массивами | [Документация](https://numpy.org/) |
-| **Matplotlib** | Визуализация данных | Построение графиков и диаграмм | [Документация](https://matplotlib.org/) |
-| **Seaborn** | Статистическая визуализация данных | Красивые статистические графики | [Документация](https://seaborn.pydata.org/) |
-| **scikit-learn** | Машинное обучение | Построение и обучение моделей машинного обучения | [Документация](https://scikit-learn.org/) |
-| **TensorFlow** | Глубокое обучение и нейронные сети | Сложные модели глубокого обучения | [Документация](https://www.tensorflow.org/) |
-| **PyTorch** | Глубокое обучение с динамическими вычислительными графами | Исследования в области глубокого обучения | [Документация](https://pytorch.org/) |
-| **Flask** | Микрофреймворк для веб-разработки | Создание веб-приложений и API | [Документация](https://flask.palletsprojects.com/) |
-| **Django** | Полнофункциональный веб-фреймворк | Крупные веб-проекты | [Документация](https://www.djangoproject.com/) |
-| **Beautiful Soup** | Парсинг HTML и XML | Извлечение данных с веб-страниц | [Документация](https://www.crummy.com/software/BeautifulSoup/) |
-| **Pillow** | Обработка изображений | Редактирование и анализ изображений | [Документация](https://pillow.readthedocs.io/) |
-| **SQLAlchemy** | ORM для работы с базами данных | Взаимодействие с SQL базами данных | [Документация](https://www.sqlalchemy.org/) |
-| **Pygame** | Создание игр и мультимедийных приложений | Разработка 2D игр | [Документация](https://www.pygame.org/) |
-| **PyQt** | Создание настольных приложений | Графические интерфейсы (GUI) | [Документация](https://www.riverbankcomputing.com/software/pyqt/) |
-
-Выбор библиотеки зависит от конкретной задачи, которую вы хотите решить. Python-сообщество очень активно, и для большинства практических задач уже существуют готовые решения, которые можно установить через pip.
-
-Пример установки популярной библиотеки:
+Пока окружение активно, `pip` кладёт пакеты в него, а не в общую папку:
```bash
-# Установка pandas для анализа данных
pip install pandas
-
-# Установка Flask для веб-разработки
-pip install flask
```
-## Виртуальные окружения
+Если нужна конкретная версия, её указывают через двойное равно:
+
+```bash
+pip install pandas==2.0.3
+```
-При работе с разными проектами часто возникает необходимость использовать разные версии библиотек. Для этого в Python есть виртуальные окружения.
+Посмотреть, что уже установлено, — `pip list`. Убрать лишнее — `pip uninstall pandas`.
-> Виртуальное окружение — это изолированная среда Python, в которой можно устанавливать пакеты, не влияя на другие проекты или системный Python.
+## Чтобы то же самое собралось у коллеги
-### Создание виртуального окружения
+Окружение остаётся на вашем компьютере, а в репозиторий уезжает только код. Чтобы у коллеги и на сервере встали ровно те же версии, список зависимостей сохраняют в файл:
```bash
-# Создание виртуального окружения
-python -m venv myenv
+pip freeze > requirements.txt
+```
-# Активация виртуального окружения
-# В Windows:
-myenv\Scripts\activate
+Внутри — построчно пакет и его точная версия:
-# В macOS/Linux:
-source myenv/bin/activate
-
-# После активации в начале командной строки появится имя окружения
-(myenv) $
+```text
+numpy==1.25.2
+pandas==2.0.3
+python-dateutil==2.8.2
```
-### Установка пакетов в виртуальное окружение
+Файл кладут в репозиторий рядом с кодом, и дальше любой, кто склонирует проект, повторяет окружение одной командой:
```bash
-# Устанавливаем пакеты в активированное виртуальное окружение
-pip install pandas matplotlib
+pip install -r requirements.txt
```
-### Сохранение и установка зависимостей
+Версии зафиксированы, поэтому у него соберётся ровно то же, что у вас, а не «у меня-то работает».
-```bash
-# Сохранение списка установленных пакетов
-pip freeze > requirements.txt
+## Куда идти дальше
-# Содержимое файла requirements.txt будет выглядеть примерно так:
-# matplotlib==3.7.2
-# numpy==1.25.2
-# pandas==2.0.3
-# ...
+Пакетов на PyPI сотни тысяч, но на старте хватит нескольких:
-# Установка пакетов из файла requirements.txt
-pip install -r requirements.txt
-```
+| Когда понадобится | Библиотека |
+| ------------------------------- | ------------------ |
+| Таблицы, отчёты, аналитика | `pandas` |
+| Запросы к чужому API по HTTP | `requests` |
+| Достать данные из HTML-страницы | `beautifulsoup4` |
+| Графики и диаграммы | `matplotlib` |
+| Свой веб-сервис или API | `flask`, `fastapi` |
+
+Остальное ищется на PyPI тогда, когда упрётесь в задачу. Учить список наизусть заранее не нужно 🎓.
## Проверка понимания
-Давайте проверим, насколько хорошо вы усвоили тему сторонних библиотек и pip:
+**Зачем для каждого проекта заводят отдельное виртуальное окружение?**
+
+1. **Правильный ответ:** Чтобы проекты не делили версии пакетов и не ломали друг друга — Верно. Разным проектам нужны разные версии одной библиотеки, а в одной общей папке может лежать только одна. Отдельное окружение снимает конфликт.
+
+2. Чтобы код в проекте выполнялся быстрее — На скорость выполнения окружение не влияет: это про то, где лежат пакеты, а не про то, как работает код.
+
+3. Чтобы не устанавливать pip отдельно для каждого проекта — pip приезжает вместе с Python, отдельно его ставить не нужно. Окружение решает другую задачу — конфликт версий.
-**Какая команда правильно установит пакет pandas версии 1.5.0?**
+4. Чтобы Python не пришлось устанавливать в систему — Установленный на компьютер Python всё равно нужен: именно он и создаёт виртуальное окружение командой python -m venv.
diff --git a/course/module-2/type-annotations/article.md b/course/module-2/type-annotations/article.md
index 97c8ad0..5b8037c 100644
--- a/course/module-2/type-annotations/article.md
+++ b/course/module-2/type-annotations/article.md
@@ -1,48 +1,59 @@
-# Аннотации типов в Python
+---
+meta:
+ title: "Аннотации типов в Python"
+ description: "Как подсказать типы аргументов и результата: синтаксис аннотаций, коллекции, запись X | None и псевдонимы типов."
+---
-В больших проектах бывает трудно с первого взгляда понять, какие именно данные принимает функция или что хранится в переменной. Чтобы сделать код более предсказуемым и безопасным, в Python добавили **аннотации типов**.
+# Аннотации типов в Python
-Сравните два варианта одной и той же функции:
+В больших проектах бывает трудно с первого взгляда понять, какие данные принимает функция. Сравните два варианта одной и той же заготовки:
```python
-# Было: что такое user? Объект? Данные из базы? Что вернет функция?
+# Что такое user? Объект? Строка с именем? Что вернёт функция?
def get_discount(user):
pass
-# Стало: сразу понятно, что передаем ID (число), а получаем скидку (число)
+# Сразу понятно: передаём ID (число), получаем скидку (число с точкой)
def get_discount(user_id: int) -> float:
pass
```
-Аннотации помогают разработчикам и IDE (среде разработки) точно понимать, какие типы данных ожидаются в разных частях программы, что значительно ускоряет написание и отладку кода.
+Приписки `: int` и `-> float` — это **аннотации типов**: подсказки о том, значения каких типов функция принимает и возвращает.
-## Что такое аннотации типов?
+> **Аннотации типов** — специальный синтаксис, которым явно указывают ожидаемые типы данных для переменных, аргументов функций и возвращаемых значений.
-> **Аннотации типов** — это специальный синтаксис, который позволяет явно указывать ожидаемые типы данных для переменных, аргументов функций и возвращаемых значений.
+## Подсказки, а не запреты
-Важно понимать: **Python остаётся языком с динамической типизацией**. Аннотации типов — это лишь _подсказки_. Сам Python не остановит выполнение программы, если вы передадите строку вместо числа. Однако они незаменимы для:
+Python остаётся языком с динамической типизацией: во время работы программы аннотация ничего не проверяет. Убедимся:
-- Улучшения автодополнения кода в IDE (PyCharm, VS Code)
-- Статического анализа кода (например, линтером `mypy`)
-- Самодокументируемости кода (код проще читать и понимать)
+```python
+def double(x: int) -> int:
+ return x * 2
-## Базовый синтаксис
+print(double(5))
+
+print(double("ха")) # аннотация не остановила строку
+
+```
-### Аннотация переменных
+Программа не упала: для Python аннотация — просто пометка. Пользу из неё извлекают инструменты вокруг кода — редактор и IDE. Чем именно она полезна, разберём в конце урока.
-Для аннотации переменных ставится двоеточие `:` после имени переменной, а затем указывается тип:
+## Базовый синтаксис
-```python
-# Аннотация базовых типов
+Для переменной тип пишут через двоеточие после имени:
+
+```python-interactive
name: str = "Алексей"
age: int = 28
height: float = 1.82
is_developer: bool = True
```
-### Аннотация функций
-
-В функциях мы можем указать типы для передаваемых аргументов и тип возвращаемого значения (используя стрелку `->`):
+В функциях аннотируют аргументы и — через стрелку `->` — возвращаемое значение:
```python
def greet(name: str, age: int) -> str:
@@ -50,90 +61,55 @@ def greet(name: str, age: int) -> str:
message = greet("Иван", 25)
print(message)
+
```
-В этом примере:
-
-- `name: str` — ожидается строка
-- `age: int` — ожидается целое число
-- `-> str` — функция обязуется вернуть строку
+Если функция ничего не возвращает, пишут `-> None`.
-_(Примечание: если функция ничего не возвращает, мы используем тип возвращаемого значения `None`, например `-> None`)._
+## Типизация коллекций
-## Типизация коллекций (списки, словари)
+Для коллекций указывают и тип содержимого:
-Вы можете использовать стандартные коллекции для аннотации их содержимого:
-
-```python
-# Список целых чисел
+```python-interactive
numbers: list[int] = [1, 2, 3, 4, 5]
-# Словарь, где ключи - строки, а значения - числа
+# Ключи — строки, значения — числа
user_ages: dict[str, int] = {
"Иван": 25,
"Анна": 22
}
-# Кортеж с четкой структурой: (строка, целое число, число с точкой)
+# Кортеж с чёткой структурой: (строка, целое, число с точкой)
user_info: tuple[str, int, float] = ("Алексей", 30, 75.5)
-
-# Множество строк
-unique_names: set[str] = {"Иван", "Анна", "Петр"}
```
-## Модуль typing
-
-Для более сложных сценариев используется встроенный модуль `typing`:
-
-### Optional (Опциональное значение)
-
-Если переменная может содержать значение определенного типа **или** `None`, используйте `Optional`:
+## Значение или None
-```python
-from typing import Optional
+Частый случай: функция возвращает значение — или `None`, если ничего не нашлось. Это записывают через вертикальную черту:
-def get_user_email(user_id: int) -> Optional[str]:
+```python-interactive
+def get_user_email(user_id: int) -> str | None:
if user_id == 1:
return "admin@example.com"
- return None # Возвращаем None, если пользователь не найден
+ return None
```
-### Union (Объединение типов)
-
-Когда переменная может принимать один из нескольких возможных типов:
+Та же черта объединяет любые типы: `int | float` — «целое или с точкой»:
-```python
-from typing import Union
-
-# Функция может принимать как целое число, так и число с точкой
-def process_price(price: Union[int, float]) -> float:
- return float(price) * 1.2 # Добавляем 20% НДС
+```python-interactive
+def process_price(price: int | float) -> float:
+ return float(price) * 1.2
```
-### Callable (Функции как аргументы)
-
-Если вы передаете функцию как аргумент в другую функцию, её тоже можно типизировать.
-
-`Callable` принимает два аргумента: список типов входных параметров и тип возвращаемого значения:
-
-```python
-from typing import Callable
-
-def apply_twice(value: int, func: Callable[[int], int]) -> int:
- return func(func(value))
+> В старом коде вместо `str | None` вы встретите `Optional[str]`, а вместо `int | float` — `Union[int, float]` из модуля `typing`. Это одно и то же: запись через `|` появилась в Python 3.10 и сейчас предпочтительна.
-def double(x: int) -> int:
- return x * 2
-
-result = apply_twice(3, double) # double(double(3)) = 12
-```
-
-## Создание собственных типов
+## Псевдонимы типов
-Чтобы не писать длинные сложные типы по много раз, вы можете создавать **псевдонимы типов**:
+Чтобы не повторять длинные типы по много раз, им дают имена:
-```python
-# Создаем псевдоним
+```python-interactive
Coordinates = tuple[float, float]
UserDict = dict[str, str | int]
@@ -144,14 +120,20 @@ def process_user(user: UserDict) -> None:
pass
```
-## Зачем всё это нужно?
+## Зачем всё это нужно
-1. **Меньше багов**: Ваш редактор кода подчеркнет ошибку красным _еще до запуска программы_, если вы попытаетесь передать строку `""` туда, где ожидается `int`.
-2. **Идеальное автодополнение**: IDE будет точно знать, какие методы есть у объекта, потому что вы указали его тип.
-3. **Легче читать код**: Читая заголовок `def get_user(user_id: int) -> dict[str, str]:`, вы мгновенно понимаете, что функция принимает числовой ID и возвращает словарь. Не нужно вчитываться в тело функции.
+1. **Меньше ошибок**: редактор подчеркнёт несоответствие ещё до запуска, если передать строку туда, где ожидается `int`.
+2. **Точное автодополнение**: IDE знает тип значения — значит, знает и его методы.
+3. **Код проще читать**: по заголовку `def get_user(user_id: int) -> dict[str, str]:` всё ясно без чтения тела функции.
## Проверка понимания
-Давайте проверим ваши знания об аннотациях типов:
+**Как описать функцию, которая принимает целое число и строку-или-None, а возвращает список чисел?**
+
+1. def process(id: int, name: str) -> list\[int]: — Здесь name — обязательная строка, а по условию она может быть и None.
+
+2. def process(id: int, name: str | None) -> list: — У возвращаемого списка не указано содержимое — нужен list\[int].
+
+3. **Правильный ответ:** def process(id: int, name: str | None) -> list\[int]: — Запись str | None допускает строку или None, а list\[int] уточняет содержимое списка.
-**Как правильно описать функцию, которая принимает целое число и опциональную строку (может быть None), а возвращает список чисел?**
+4. def process(id: Integer, name: str | None) -> list\[int]: — В Python встроенный тип называется int, а не Integer.
diff --git a/course/module-3/asyncio-advanced/article.md b/course/module-3/asyncio-advanced/article.md
index 4f40f65..71e4368 100644
--- a/course/module-3/asyncio-advanced/article.md
+++ b/course/module-3/asyncio-advanced/article.md
@@ -1,6 +1,14 @@
+---
+meta:
+ title: "Продвинутый asyncio: очереди, синхронизация, executor"
+ description: "asyncio.Queue для обмена между корутинами, asyncio.Lock для синхронизации, и главное это run_in_executor для блокирующего кода без остановки event loop."
+---
+
# Продвинутый asyncio в Python
-В прошлой статье разобрали базовый asyncio: `async def`, `await`, `gather`, Tasks. Здесь — три инструмента, которые приходится использовать в реальных приложениях: очереди между корутинами, синхронизация и (главное) запуск блокирующего кода без остановки event loop.
+Асинхронный сервис держал тысячи соединений и вдруг замер весь, для всех разом. Причина в одной строке: кто-то вызвал синхронную `requests.get()`, и она на секунду заблокировала единственный поток event loop, а с ним и все остальные корутины.
+
+Базовых средств asyncio из прошлой статьи (`async def`, `await`, `gather`, Tasks) здесь недостаточно. Для реальных приложений нужны ещё три инструмента: очереди между корутинами, синхронизация и, главное, запуск блокирующего кода без остановки event loop.
## asyncio.Queue: обмен данными между корутинами
@@ -63,10 +71,12 @@ asyncio.run(main())
## Блокирующий код в asyncio: run_in_executor
-Главное правило asyncio: **никогда не вызывайте блокирующие функции напрямую**. `time.sleep(2)`, `requests.get()`, тяжёлые вычисления — всё это остановит event loop, и все остальные корутины замрут.
+Вернёмся к аварии из начала главы. Правило, которое там нарушили: **в event loop нельзя вызывать блокирующие функции напрямую**. `time.sleep(2)`, `requests.get()`, тяжёлый расчёт останавливают его целиком.
Но иногда деваться некуда: нужна старая синхронная библиотека или CPU-bound расчёт. На этот случай есть `loop.run_in_executor()`: запустить блокирующую функцию в **отдельном потоке** (или процессе), пока event loop спокойно продолжает работу.
+
+
```python
import asyncio
import time
@@ -118,40 +128,39 @@ async with aiohttp.ClientSession() as session:
## Сравнение трёх подходов
-| | threading | multiprocessing | asyncio |
-| --- | --- | --- | --- |
-| Параллелизм CPU | нет (GIL) | да | нет (1 поток) |
-| I/O-bound | хорошо | хорошо, но дорого | отлично |
-| Накладные расходы | низкие | высокие | минимальные |
-| Память | общая | изолированная | общая (1 поток) |
-| Обмен данными | переменные + Lock / Queue | Queue, Pipe, Manager | переменные / asyncio.Queue |
-| Тысячи задач | плохо | очень плохо | прекрасно |
+| | threading | multiprocessing | asyncio |
+| ----------------- | ------------------------- | -------------------- | -------------------------- |
+| Параллелизм CPU | нет (GIL) | да | нет (1 поток) |
+| I/O-bound | хорошо | хорошо, но дорого | отлично |
+| Накладные расходы | низкие | высокие | минимальные |
+| Память | общая | изолированная | общая (1 поток) |
+| Обмен данными | переменные + Lock / Queue | Queue, Pipe, Manager | переменные / asyncio.Queue |
+| Тысячи задач | плохо | очень плохо | прекрасно |
**Правило выбора:**
-- Тысячи сетевых соединений, новые проекты → **asyncio**
-- I/O в существующем синхронном коде без async-библиотек → **threading** или `ThreadPoolExecutor`
-- Тяжёлые вычисления → **multiprocessing** или `ProcessPoolExecutor`
-- В одном приложении часто всё это сочетается: asyncio как основной слой + `run_in_executor` с пулом потоков/процессов для блокирующих кусков.
+- Тысячи сетевых соединений, новые проекты → **asyncio**
+- I/O в существующем синхронном коде без async-библиотек → **threading** или `ThreadPoolExecutor`
+- Тяжёлые вычисления → **multiprocessing** или `ProcessPoolExecutor`
+- В одном приложении часто всё это сочетается: asyncio как основной слой + `run_in_executor` с пулом потоков/процессов для блокирующих кусков.
## Несколько подводных камней
-- **CPU-bound в asyncio** убивает event loop. Используйте `run_in_executor` с `ProcessPoolExecutor` для тяжёлых вычислений в async-коде.
-- **Забытая `await`**: `asyncio.sleep(1)` без `await` ничего не делает (создаёт корутину и выбрасывает её). В современных IDE это подсвечивается.
-- **Mix sync/async**: вызов `requests.get()` (синхронный) в asyncio блокирует всё. Используйте `aiohttp` / `httpx` для async-HTTP.
-- **`if __name__ == "__main__":`** на Windows и macOS обязательна для `multiprocessing`, иначе процессы будут рекурсивно создавать сами себя.
+- **CPU-bound в asyncio** убивает event loop. Используйте `run_in_executor` с `ProcessPoolExecutor` для тяжёлых вычислений в async-коде.
+- **Забытая `await`**: `asyncio.sleep(1)` без `await` ничего не делает (создаёт корутину и выбрасывает её). В современных IDE это подсвечивается.
+- **Mix sync/async**: вызов `requests.get()` (синхронный) в asyncio блокирует всё. Используйте `aiohttp` / `httpx` для async-HTTP.
+- **`if __name__ == "__main__":`** на Windows и macOS обязательна для `multiprocessing`, иначе процессы будут рекурсивно создавать сами себя.
-## Что дальше?
+## Проверка понимания
-На этом модуль конкурентности завершён. Главное правило: подберите инструмент под тип задачи.
+**Какой инструмент asyncio используется для безопасного запуска блокирующего кода, не останавливая event loop?**
-- Тысячи сетевых соединений → **asyncio**
-- I/O в legacy-коде → **threading**
-- Математика и обработка данных → **multiprocessing**
+1. asyncio.sleep() — asyncio.sleep() — неблокирующая пауза, она сама уже асинхронная.
-В современных приложениях чаще всего основная программа на asyncio, а CPU-тяжёлые куски выносятся в process pool через `run_in_executor`.
+2. asyncio.gather() — gather() запускает несколько корутин конкурентно. Для блокирующих синхронных функций он не подходит — они всё равно остановят event loop.
----
+3. **Правильный ответ:** loop.run_in_executor() — Верно. run_in_executor отдаёт блокирующую функцию в отдельный поток (или процесс), и event loop продолжает работать с другими корутинами параллельно.
-**Какой инструмент asyncio используется для безопасного запуска блокирующего кода, не останавливая event loop?**
+4. asyncio.create_task() — create_task() планирует корутину к выполнению в event loop. Но он не помогает с СИНХРОННЫМИ блокирующими функциями — те всё равно зависнут event loop.
+На этом модуль конкурентности завершён. Карта из вводной статьи и матрица выше уже отвечают на вопрос «что брать», а на практике основная программа чаще всего живёт на asyncio, отдавая CPU-тяжёлые куски в process pool через `run_in_executor`.
diff --git a/course/module-3/asyncio-basics/article.md b/course/module-3/asyncio-basics/article.md
index f82b111..f3f0b3e 100644
--- a/course/module-3/asyncio-basics/article.md
+++ b/course/module-3/asyncio-basics/article.md
@@ -1,6 +1,12 @@
+---
+meta:
+ title: "Основы asyncio: async/await, корутины, event loop"
+ description: "Базовое асинхронное программирование в Python: ключевые слова async и await, корутины, цикл событий, задачи и asyncio.gather."
+---
+
# Основы asyncio в Python
-В предыдущей статье разбирали потоки и процессы. У них общая модель: ОС переключает между «исполнителями». **asyncio** идёт другим путём: один поток, кооперативная многозадачность. Программа сама помечает места, где можно «отложить» задачу. Эти места обозначаются ключевым словом `await`.
+Три «скачивания» из вводной статьи заняли полторы секунды — и почти всё это время процессор просто ждал ответа сервера. Потоки и процессы решали это, поручая переключение операционной системе. **asyncio** идёт иначе: один поток и кооперативная многозадачность. Пока один запрос ждёт сервер, тот же поток сам берётся за следующий, а места, где задачу можно «отложить», программа помечает ключевым словом `await`.
Для I/O-bound нагрузок asyncio даёт лучшее соотношение производительности и ресурсов: тысячи одновременных соединений на одном потоке без накладных расходов на потоки ОС.
@@ -8,14 +14,16 @@
Сердце asyncio — **event loop (цикл событий)**. Он держит список задач, выполняет одну из них, и когда задача доходит до `await something_slow()`, задача «уступает управление», event loop переключается на следующую готовую задачу. Когда `something_slow()` завершается, исходная задача снова становится готовой.
+
+
Важно: переключение происходит **только на `await`**. Никаких прерываний посередине вычисления. Это «кооперативная» многозадачность: задачи договариваются, когда уступать. У такого подхода есть последствие: если задача не делает `await` (например, считает что-то долго на CPU), весь event loop стоит.
## async и await
В Python 3.5 появились ключевые слова для асинхронности:
-- `async def` — определяет **корутину** (асинхронную функцию)
-- `await` — внутри корутины: «дождись завершения этой операции, на время ожидания отпусти управление»
+- `async def` — определяет **корутину** (асинхронную функцию)
+- `await` — внутри корутины: «дождись завершения этой операции, на время ожидания отпусти управление»
```python
import asyncio
@@ -120,11 +128,16 @@ asyncio.run(main())
2. **Хочется конкурентно — `asyncio.gather()` или `asyncio.create_task()`**. Просто `await` подряд = последовательно.
3. **CPU-bound в asyncio останавливает всё**. Считаете долго? Выносите в `run_in_executor` (следующая статья) или в `multiprocessing`.
-## Что дальше?
+## Проверка понимания
-В следующей статье — продвинутые техники asyncio: очереди, синхронизация между корутинами и (главное) как запускать блокирующий код, не убивая event loop.
+**Что такое основная особенность асинхронного программирования в asyncio?**
----
+1. Истинный параллелизм выполнения кода на нескольких ядрах CPU — Это про multiprocessing. asyncio работает на одном потоке и для CPU-bound задач не подходит.
-**Что такое основная особенность асинхронного программирования в asyncio?**
+2. **Правильный ответ:** Кооперативная многозадачность: задачи явно передают управление event loop через await — Верно. Корутины сами уступают контроль на await, позволяя другим задачам выполняться во время ожидания I/O. Это базовый принцип asyncio.
+
+3. Автоматическое распараллеливание любых функций без async/await — Не автоматическое. Функция становится корутиной только через async def, и переключение происходит только на await.
+4. Создание системных потоков для каждой задачи — Наоборот, asyncio работает в одном потоке. Это и есть его главное преимущество для I/O, нет накладных расходов на создание потоков.
+
+В следующей статье — продвинутые техники asyncio: очереди, синхронизация между корутинами и (главное) как запускать блокирующий код, не убивая event loop.
diff --git a/course/module-3/coverage-and-ci/article.md b/course/module-3/coverage-and-ci/article.md
index bf84b90..e3bbcd5 100644
--- a/course/module-3/coverage-and-ci/article.md
+++ b/course/module-3/coverage-and-ci/article.md
@@ -1,6 +1,12 @@
+---
+meta:
+ title: "Покрытие кода и CI: автоматизация тестов"
+ description: "Измерение покрытия кода тестами через pytest-cov и автоматический запуск тестов через GitHub Actions. Базовый production-конфиг."
+---
+
# Покрытие кода и CI
-Тесты, которые лежат в репозитории и не запускаются, ничего не гарантируют. Чтобы тесты реально работали, нужны две вещи:
+В репозитории сорок тестов, все зелёные. Вот только последний раз их кто-то запускал одиннадцать дней назад, а в `main` с тех пор влили десяток правок. «Зелёные» они лишь на бумаге: проходят ли они сейчас, не знает никто. Чтобы тесты реально что-то гарантировали, нужны две вещи:
1. **Покрытие**: знать, какие части кода тесты действительно прогоняют, а какие нет.
2. **CI** (Continuous Integration): чтобы тесты автоматически запускались на каждый push, а не «когда-нибудь» вручную.
@@ -33,9 +39,9 @@ app/orders.py 18 0 100%
TOTAL 43 5 88%
```
-- `Stmts`: сколько строк кода в файле.
-- `Miss`: сколько не выполнилось во время тестов.
-- `Cover`: процент покрытия.
+- `Stmts`: сколько строк кода в файле.
+- `Miss`: сколько не выполнилось во время тестов.
+- `Cover`: процент покрытия.
Для детального отчёта (с подсветкой непокрытых строк в браузере) используют HTML:
@@ -53,6 +59,8 @@ pytest --cov=app --cov-report=html
**CI** это автоматический запуск тестов (и любых других проверок) при изменениях в репозитории. На GitHub это делает **GitHub Actions**, в GitLab встроенный GitLab CI, есть ещё CircleCI, Jenkins и другие. Принцип у всех один: на каждый `git push` запускается заданный pipeline.
+
+
## Минимальный CI на GitHub Actions
Положите файл `.github/workflows/tests.yml` в корне репозитория:
@@ -73,7 +81,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
- python-version: '3.12'
+ python-version: "3.12"
- run: pip install -r requirements.txt
- run: pytest --cov=app
```
@@ -89,16 +97,21 @@ jobs:
## Что это даёт
-- **Ошибка ловится сразу**, а не через неделю в production.
-- **Тесты прогоняются всегда**, не нужно полагаться на «я запущу локально перед мержем».
-- **Pull request показывает**, прошли тесты или нет, ревьюер видит сразу.
-- **Покрытие отслеживается**: если в PR добавлен код без тестов, видно в отчёте.
+- **Ошибка ловится сразу**, а не через неделю в production.
+- **Тесты прогоняются всегда**, не нужно полагаться на «я запущу локально перед мержем».
+- **Pull request показывает**, прошли тесты или нет, ревьюер видит сразу.
+- **Покрытие отслеживается**: если в PR добавлен код без тестов, видно в отчёте.
-## Что дальше?
+## Проверка понимания
-На этом модуль по тестированию закончен. Вы умеете писать тесты на `pytest`, использовать фикстуры и параметризацию, подменять зависимости моками, читать `unittest`-код в legacy-проектах, мерить покрытие и настраивать CI. Этого достаточно, чтобы вкатиться в любой production-проект — остальное добирается по месту.
+**Что верно про покрытие кода и CI?**
----
+1. **Правильный ответ:** Покрытие кода показывает, какой процент исходного кода выполняется во время тестов — Верно. Это сигнал о пробелах в тестировании, но не гарантия корректности самого кода.
-**Что верно про покрытие кода и CI?**
+2. Для интеграции с pytest используется coverage.py напрямую — coverage.py это базовая библиотека, но для интеграции с pytest обычно ставят плагин-обёртку pytest-cov.
+
+3. CI нужен только чтобы запускать тесты по расписанию — CI автоматически запускает тесты при каждом push/PR, а не по расписанию. Расписание это уже отдельный сценарий.
+4. 100% покрытие гарантирует отсутствие багов — Нет. Можно покрыть строку, но проверить её плохо или не учесть граничный случай. Высокое покрытие полезный сигнал, но не гарантия корректности.
+
+На этом модуль по тестированию закончен. Вы умеете писать тесты на `pytest`, использовать фикстуры и параметризацию, подменять зависимости моками, читать `unittest`-код в legacy-проектах, мерить покрытие и настраивать CI. Этого достаточно, чтобы вкатиться в любой production-проект — остальное добирается по месту.
diff --git a/course/module-3/intro-concurrency-async/article.md b/course/module-3/intro-concurrency-async/article.md
index fd52605..ca3e6ad 100644
--- a/course/module-3/intro-concurrency-async/article.md
+++ b/course/module-3/intro-concurrency-async/article.md
@@ -1,28 +1,61 @@
+---
+meta:
+ title: "Конкурентность, параллелизм, асинхронность в Python"
+ description: "Три способа выполнять задачи как бы одновременно: потоки, процессы, asyncio. Что такое GIL и какой инструмент когда использовать."
+---
+
# Конкурентность, параллелизм, асинхронность в Python
Допустим, наша программа должна скачать 100 страниц с разных сайтов. Если делать по очереди — каждый запрос ждёт ответа сервера 1-2 секунды, и общее время это сумма всех ожиданий. Большую часть этого времени процессор простаивает. Решение очевидно: пока один запрос ждёт ответа, можно отправлять другие. Это и есть **конкурентное выполнение**.
В Python три инструмента для этого: `threading`, `multiprocessing`, `asyncio`. Эта статья — карта: какой и когда брать.
+Сначала посмотрим на проблему вживую. Три «скачивания» по очереди, каждое ждёт полсекунды:
+
+```python
+import time
+
+def download(url):
+ time.sleep(0.5) # имитация ожидания ответа сервера
+ return f"данные с {url}"
+
+start = time.time()
+for url in ["site-1", "site-2", "site-3"]:
+ print(download(url))
+
+print(f"Ушло {time.time() - start:.1f} с")
+
+```
+
+Три запроса — полторы секунды, и процессор всё это время не делал ничего, просто ждал ответа. Дальше три разных способа уложить те же запросы в полсекунды.
+
## Три понятия
В разговорах о конкурентности постоянно мешают три похожих слова. Различие важное:
-- **Конкурентность (concurrency)**: задачи могут переключаться между собой, создавая иллюзию одновременной работы. Один бариста за стойкой принимает заказ, ставит молоко греться, переходит к следующему клиенту, возвращается к молоку. Один исполнитель, несколько задач «в воздухе» одновременно.
+- **Конкурентность (concurrency)**: задачи могут переключаться между собой, создавая иллюзию одновременной работы. Один бариста за стойкой принимает заказ, ставит молоко греться, переходит к следующему клиенту, возвращается к молоку. Один исполнитель, несколько задач «в воздухе» одновременно.
-- **Параллелизм (parallelism)**: задачи выполняются **физически одновременно** на разных ядрах процессора. Несколько бариста, каждый делает свой кофе. Требует многоядерности.
+- **Параллелизм (parallelism)**: задачи выполняются **физически одновременно** на разных ядрах процессора. Несколько бариста, каждый делает свой кофе. Требует многоядерности.
-- **Асинхронность (asynchronicity)**: способ организации кода, при котором задача может «отложиться» в ожидании (например, ответа сервера), не блокируя весь поток. Это **способ достижения конкурентности** на одном потоке, без переключения ОС.
+- **Асинхронность (asynchronicity)**: способ организации кода, при котором задача может «отложиться» в ожидании (например, ответа сервера), не блокируя весь поток. Это **способ достижения конкурентности** на одном потоке, без переключения ОС.
Конкурентность это цель, параллелизм и асинхронность — два способа её достичь.
+
+
## I/O-bound vs CPU-bound: ключевая дихотомия
Выбор инструмента зависит **только** от того, чего ждёт ваша задача:
-- **I/O-bound**: процессор простаивает в ожидании внешнего ресурса. Сетевой запрос, чтение с диска, ответ БД. Здесь побеждает асинхронность — пока один запрос ждёт, отправляем следующие.
+- **I/O-bound**: процессор простаивает в ожидании внешнего ресурса. Сетевой запрос, чтение с диска, ответ БД. Здесь побеждает асинхронность — пока один запрос ждёт, отправляем следующие.
-- **CPU-bound**: процессор честно работает над вычислениями. Сжатие изображения, шифрование, научные вычисления. Здесь нужен реальный параллелизм на нескольких ядрах.
+- **CPU-bound**: процессор честно работает над вычислениями. Сжатие изображения, шифрование, научные вычисления. Здесь нужен реальный параллелизм на нескольких ядрах.
Самая частая ошибка новичка: брать `multiprocessing` для скачивания страниц или `asyncio` для перемножения матриц. Это даст замедление, не ускорение.
@@ -32,25 +65,30 @@
Что из этого следует:
-- Для **CPU-bound** задач `threading` бесполезен — потоки делят одно ядро через GIL. Нужны процессы (`multiprocessing`), у каждого свой GIL и своё ядро.
-- Для **I/O-bound** задач GIL **отпускается** при ожидании сети/диска. Поэтому `threading` отлично подходит для I/O, как и `asyncio` (но без накладных расходов на потоки).
+- Для **CPU-bound** задач `threading` бесполезен — потоки делят одно ядро через GIL. Нужны процессы (`multiprocessing`), у каждого свой GIL и своё ядро.
+- Для **I/O-bound** задач GIL **отпускается** при ожидании сети/диска. Поэтому `threading` отлично подходит для I/O, как и `asyncio` (но без накладных расходов на потоки).
## Какой инструмент когда
-| Задача | Инструмент |
-| --- | --- |
-| Множество сетевых запросов, тысячи соединений | **asyncio** |
-| I/O в legacy-коде без async-библиотек | **threading** |
-| Тяжёлые вычисления на нескольких ядрах | **multiprocessing** |
+| Задача | Инструмент |
+| ------------------------------------------------- | --------------------------------------------------------------------- |
+| Множество сетевых запросов, тысячи соединений | **asyncio** |
+| I/O в legacy-коде без async-библиотек | **threading** |
+| Тяжёлые вычисления на нескольких ядрах | **multiprocessing** |
| Простое распараллеливание без погружения в детали | **concurrent.futures** (`ThreadPoolExecutor` / `ProcessPoolExecutor`) |
В дальнейших статьях разберём каждый по очереди. Сначала **потоки и процессы**, потом **asyncio** (две статьи: основы и продвинутые техники).
-## Что дальше?
+## Проверка понимания
-В следующей статье разберём **потоки и процессы** — два классических подхода. Поток это легковесный исполнитель внутри одного процесса (для I/O), процесс это отдельная программа с собственной памятью (для CPU-bound).
+**Что из перечисленного верно про конкурентность в Python?**
----
+1. Параллелизм и асинхронность это синонимы — Это разные понятия. Параллелизм это про физически одновременное выполнение (нужны несколько ядер). Асинхронность это про переключение задач на одном потоке во время ожидания I/O.
-**Какое утверждение наиболее точно описывает разницу между параллелизмом и асинхронностью?**
+2. **Правильный ответ:** Параллелизм это физически одновременное выполнение на нескольких ядрах, а асинхронность это переключение между задачами на одном потоке во время ожидания — Параллелизм требует многоядерности и работает для CPU-bound задач. Асинхронность эффективна на одном потоке для I/O-bound задач.
+3. Асинхронность всегда требует многоядерного процессора — Наоборот: асинхронность отлично работает на одном ядре, переключаясь между задачами во время ожидания. Многоядерность нужна именно для параллелизма.
+
+4. GIL в Python делает многопоточность бесполезной для всех задач — GIL ограничивает только CPU-bound задачи. Для I/O-bound потоки эффективны, потому что GIL отпускается во время ожидания сети/диска.
+
+В следующей статье разберём **потоки и процессы** — два классических подхода. Поток это легковесный исполнитель внутри одного процесса (для I/O), процесс это отдельная программа с собственной памятью (для CPU-bound).
diff --git a/course/module-3/intro-to-databases/article.md b/course/module-3/intro-to-databases/article.md
index 5f66965..f9a2d02 100644
--- a/course/module-3/intro-to-databases/article.md
+++ b/course/module-3/intro-to-databases/article.md
@@ -1,110 +1,70 @@
-# Основы работы с базами данных в Python: что такое БД и зачем они нужны
+---
+meta:
+ title: "Что такое база данных и зачем она нужна"
+ description: "Почему список Python не годится для хранения данных приложения, что такое БД и СУБД и какие бывают типы баз данных."
+---
-Каждый день мы пользуемся десятками приложений: проверяем социальные сети,
-делаем покупки онлайн, слушаем музыку, бронируем билеты.
-За каждым из них стоит **база данных** — система, которая мгновенно находит нужную информацию среди миллионов записей.
+# Что такое база данных и зачем она нужна
-**Базы данных** — это фундаментальная технология современных приложений.
-Для Python-разработчика понимание принципов работы с базами данных открывает двери к созданию масштабируемых и надежных решений.
+Почти любое приложение хранит что-то надолго: пользователей, заказы, сообщения. Первая мысль — держать их в знакомом Python-списке. Посмотрим, где эта идея ломается.
-## Что такое база данных?
-
-> **База данных (БД)** — это организованная коллекция данных, которая хранится и управляется специальной системой (СУБД).
-
-Простая аналогия: база данных — это как умный склад. Каждая «полка» имеет свой адрес, а «складской робот» (СУБД) может быстро найти и доставить любую нужную информацию.
-
-## Почему не хранить данные в Python-списках?
-
-Рассмотрим простой пример — хранение пользователей:
+## Почему не хранить данные в Python-списке
```python
-# Хранение в Python-списке
users = [
{"id": 1, "name": "Анна", "email": "anna@example.com"},
- {"id": 2, "name": "Петр", "email": "petr@example.com"},
+ {"id": 2, "name": "Пётр", "email": "petr@example.com"},
# ... представьте здесь миллион пользователей
]
-# Поиск пользователя по email
def find_user_by_email(email):
- for user in users: # Просматриваем ВЕСЬ список!
+ for user in users: # перебираем весь список
if user["email"] == email:
return user
return None
```
-**Проблемы такого подхода:**
-
-- **Медленный поиск** — нужно проверить каждого пользователя
-- **Ограничения памяти** — все данные должны помещаться в RAM
-- **Потеря данных** — если программа упадет, все пропадет
-- **Конкуренция** — что если несколько пользователей одновременно изменяют данные?
+Пока пользователей тысяча, всё работает. Дальше начинаются проблемы:
-**Базы данных решают все эти проблемы из коробки** — именно поэтому они стали стандартом для серьезных приложений.
+- **Медленный поиск** — запрос «найди по email» перебирает записи одну за другой, и на миллионе это уже ощутимая задержка.
+- **Всё в оперативной памяти** — данные должны целиком в неё помещаться.
+- **Данные живут до перезапуска** — программа завершилась, и список исчез.
+- **Одновременный доступ** — двое меняют данные в один момент, и список к этому не готов.
-## Основные типы баз данных
+## Что такое база данных
-Существует много видов баз данных, но выделим три наиболее важных:
+Эти проблемы и решает **база данных**: быстрый поиск по нужному полю, хранение на диске между запусками и одновременный доступ без путаницы.
-### 1. Реляционные базы данных
+> **База данных (БД)** — организованная коллекция данных, которой управляет специальная программа — СУБД (система управления базами данных).
-Организуют данные в **таблицы** с четкой структурой. Самый популярный тип.
+Простая аналогия: база данных — умный склад. У каждой «полки» свой адрес, а «складской робот» (СУБД) быстро находит и выдаёт нужное.
-```sql
--- Пример таблицы пользователей
-CREATE TABLE users (
- id INTEGER PRIMARY KEY,
- name VARCHAR(100),
- email VARCHAR(150)
-);
-```
+## Основные типы баз данных
-### 2. Key-Value базы данных
+Видов баз данных много, но на практике постоянно встречаются три:
-Хранят простые пары «ключ-значение». Очень быстрые для простых операций.
+| Тип базы данных | Описание |
+| ------------------------ | ---------------------------------------------- |
+| Реляционные | Данные в связанных таблицах — строки и столбцы |
+| Key-value | Быстрый доступ к значению по ключу |
+| Документоориентированные | Гибкие документы в формате JSON |
-```python
-# Пример в Redis — популярной key-value БД
-user_session = {
- "session:user123": "logged_in",
- "cart:user123": "[1,5,9]", # ID товаров в корзине
- "last_seen:user123": "2024-01-15 10:30"
-}
-```
+## Самые распространённые СУБД
-### 3. Документоориентированные базы данных
+Реляционные БД — то, с чем вы столкнётесь чаще всего: **PostgreSQL** и **MySQL** на серверах, **SQLite** — внутри телефонов, браузеров и самого Python. Из остальных регулярно попадаются **Redis** (ключ-значение: кэш и сессии) и **MongoDB** (документы).
-Хранят данные как документы (JSON-формат). Гибкая структура.
+Следующие три главы — про реляционные БД, поэтому дальше говорим только о них.
-```python
-# Пример документа пользователя
-{
- "name": "Анна",
- "email": "anna@example.com",
- "preferences": {
- "theme": "dark",
- "language": "ru"
- }
-}
-```
+## Проверка понимания
-## Популярные системы управления БД
+**Почему для хранения пользователей приложения выбирают базу данных, а не обычный список в памяти?**
-Вот топ-8 наиболее используемых СУБД в 2024 году:
+1. **Правильный ответ:** База данных быстро ищет по нужному полю, хранит данные между запусками и допускает одновременный доступ — Быстрый поиск по нужному полю вместо перебора всего списка, данные переживают перезапуск, и несколько клиентов пишут одновременно без путаницы — это и есть задачи, ради которых заводят БД.
-1. **PostgreSQL** — мощная **реляционная** БД
-2. **MySQL** — популярная **реляционная** БД для веба
-3. **SQLite** 📱 — легковесная **реляционная** БД
-4. **MongoDB** — лидер **документоориентированных** БД
-5. **Redis** ⚡ — быстрая **key-value** БД
-6. **Oracle Database** — корпоративная **реляционная** БД
-7. **Microsoft SQL Server** — **реляционная** БД от Microsoft
-8. **Elasticsearch** — **документоориентированная** поисковая БД
+2. База данных всегда работает быстрее любого Python-кода — Скорость зависит от задачи: на обращении к одному элементу по индексу список в памяти даже быстрее. Выигрыш БД — в поиске по большим объёмам, надёжности и одновременном доступе.
-**Вывод:** реляционные базы данных (5 из 8 позиций) являются стандартом индустрии
-благодаря своей надежности и универсальности.
+3. Список в Python не может хранить больше 1000 элементов — Размер списка ограничен только оперативной памятью, а не числом 1000. Проблема не в лимите на элементы, а в скорости поиска и в потере данных при перезапуске.
-## Что дальше?
+4. С базой данных не нужно писать код — Запросы к БД всё равно приходится писать — на SQL или через библиотеку. БД избавляет не от кода, а от ручного поиска, хранения в файлах и борьбы за одновременный доступ.
-В следующей статье мы начнем практику с **SQLite** — идеальной базой данных для изучения, которая уже встроена в Python.
-Вы создадите свою первую БД и научитесь работать с реальными данными.
+В следующей статье начнём практику с **SQLite** — идеальной для обучения базой данных, которая уже встроена в Python: создадим первую БД и поработаем с настоящими данными.
diff --git a/course/module-3/intro-to-testing/article.md b/course/module-3/intro-to-testing/article.md
index b7b15c6..4321e2b 100644
--- a/course/module-3/intro-to-testing/article.md
+++ b/course/module-3/intro-to-testing/article.md
@@ -1,26 +1,20 @@
-# Основы тестирования в Python: что, зачем и типы тестов
+---
+meta:
+ title: "Зачем нужны тесты и какими они бывают"
+ description: "Почему ошибки дешевле ловить рано, типы тестов (unit, интеграционные, E2E), пирамида тестирования и принципы FIRST."
+---
-Представьте, что вы строите мост — разве вы позволите людям пройти по нему, не проверив сначала, насколько он безопасен?
+# Зачем нужны тесты и какими они бывают
-Тот же принцип применим и к вашему коду! Тестирование помогает убедиться, что ваши программы работают как ожидается, даже когда вы вносите изменения или добавляете новые функции.
+Вы поправили одну функцию, выкатили обновление — а через день пользователь пишет, что сломалось что-то совсем другое, работавшее вчера. Правка в одном месте незаметно задела другое, и узнали вы об этом последним.
-## Что такое тестирование программного обеспечения?
+Тестирование нужно, чтобы узнавать о таких поломках сразу, а не от пользователей. Это набор проверок, которые подтверждают, что код делает то, что от него ждут, — и продолжает делать после каждого изменения.
-> **Тестирование программного обеспечения** — это процесс исследования и оценки программного продукта с целью проверки его соответствия заявленным требованиям и выявления потенциальных дефектов (ошибок или багов) до того, как с ними столкнутся конечные пользователи.
+## Зачем тестировать
-Проще говоря, это способ убедиться, что ваш код делает то, что должен, и не делает того, чего не должен.
+В 1999 году NASA потеряло марсианский аппарат стоимостью $125 миллионов из-за простой ошибки: в расчётах смешались разные системы единиц. Один тест мог выявить это до запуска.
-## Зачем нужно тестирование? Ключевые преимущества
-
-Вы когда-нибудь отправляли сообщение с неловкой опечаткой? 😳 Представьте, если бы ваш код имел ту же проблему, но вместо небольшого смущения это привело бы к серьезным последствиям!
-
-### 1. Раннее обнаружение ошибок и снижение стоимости их исправления
-
-В 1999 году NASA потеряло марсианский орбитальный аппарат стоимостью \$125 миллионов из-за простой ошибки в расчетах, связанной с использованием разных систем единиц. Простой тест мог бы выявить это несоответствие до запуска 🚀.
-
-Программные ошибки могут варьироваться от небольших неудобств до катастрофических сбоев с огромными финансовыми потерями, проблемами безопасности или ущербом для репутации.
-
-**Чем позже найдена ошибка, тем дороже её исправить:**
+Это крайний случай общего правила — чем позже найдена ошибка, тем дороже её исправить:
| Когда обнаружена ошибка | Относительная стоимость исправления |
| ---------------------------- | ----------------------------------- |
@@ -29,91 +23,92 @@
| Во время тестирования QA | 5x-10x |
| После выпуска (в продакшене) | 30x-100x+ |
-Тестирование помогает выявить проблемы на ранних стадиях, когда их исправление требует меньше времени и ресурсов.
-
-### 2. Уверенность при внесении изменений и рефакторинге
-
-Вы когда-нибудь боялись менять существующий код, опасаясь что-то сломать? Наличие хорошего набора тестов дает вам "сеть безопасности". Вы можете смело проводить рефакторинг, добавлять новые функции или исправлять ошибки, зная, что тесты предупредят вас, если что-то пойдет не так.
-
-### 3. Тесты как живая документация
-
-Хорошо написанные тесты служат формой живой, исполняемой документации. Они наглядно демонстрируют, как должен использоваться ваш код и какое поведение от него ожидается. В отличие от статической документации, тесты не устаревают, так как они должны проходить для каждой версии кода.
-
-### 4. Улучшение дизайна кода
-
-Процесс написания тестов часто заставляет задуматься о структуре и дизайне кода. Если функцию или модуль сложно тестировать, это может быть признаком того, что он слишком сложен, имеет много зависимостей или нарушает принцип единственной ответственности. Тестируемость — важный аспект хорошего дизайна.
+Кроме ранней ловли ошибок тесты дают ещё несколько вещей:
-### 5. Предотвращение регрессий
-
-Регрессии — это ошибки, которые появляются в уже работавшем коде после внесения изменений. Автоматические тесты эффективно отлавливают такие проблемы, гарантируя, что старая функциональность не была случайно нарушена.
+- **уверенность при изменениях** — с набором тестов можно смело рефакторить и добавлять новое: если что-то сломается, тесты скажут об этом первыми;
+- **живую документацию** — тесты показывают, как код должен использоваться, и, в отличие от обычной документации, не устаревают;
+- **лучший дизайн** — если функцию трудно протестировать, обычно она просто слишком сложно устроена;
+- **защиту от регрессий** — так называют поломки уже работавшего кода после новых изменений; ровно их ловит пример ниже.
## Типы тестов
-Существует несколько уровней и типов тестов, каждый из которых служит своей цели.
+Тесты различаются масштабом проверяемого — от одной функции до всей системы.
-### 1. Модульные тесты (Unit Tests)
+### Модульные тесты (unit)
-Модульные тесты проверяют самые маленькие, изолированные части вашей программы — отдельные функции, методы или классы. Цель — убедиться, что каждый "кирпичик" вашего кода работает правильно сам по себе.
+Проверяют самые маленькие, изолированные части программы — отдельные функции, методы или классы. Цель — убедиться, что каждый «кирпичик» кода работает правильно сам по себе.
```python
-# Пример: функция для сложения
def add(a, b):
return a + b
-# Простой ручной тест (концепция)
-# result = add(2, 3)
-# expected = 5
-# print(f"Тест пройден: {result == expected}")
-# Далее мы увидим, как автоматизировать это с помощью фреймворков
+print(add(2, 3) == 5)
+
```
-### 2. Интеграционные тесты (Integration Tests)
+Проверка вернула `True` — функция работает как ожидалось. Теперь представим, что кто-то поправил `add` и случайно задел логику:
-Интеграционные тесты проверяют взаимодействие между несколькими модулями или компонентами системы. Например, как ваш модуль обработки заказов взаимодействует с модулем уведомлений или базой данных. Они помогают убедиться, что "кирпичики" правильно складываются вместе.
+```python
+def add(a, b):
+ return a + b + 1 # случайно закравшаяся ошибка
-- **Пример**: Проверка того, что после успешного создания пользователя в базе данных (один модуль), система аутентификации (другой модуль) может его распознать.
+print(add(2, 3) == 5)
+
+```
-### 3. Функциональные / Сквозные тесты (Functional / End-to-End, E2E Tests)
+Та же проверка мгновенно вернула `False`. Это и есть модульный тест: маленькая проверка, которая сама говорит, цела ли функция, — так и ловят регрессии. В следующей статье увидим, как оформлять такие проверки через `assert` и запускать их пачками.
-Эти тесты проверяют всю систему или значительную ее часть с точки зрения пользователя. Они имитируют реальные пользовательские сценарии, проходя через все слои приложения — от пользовательского интерфейса (если он есть) до базы данных.
+### Интеграционные тесты
-- **Пример**: Полный сценарий регистрации нового пользователя на сайте: заполнение формы, отправка данных, получение письма с подтверждением, первый вход в систему.
+Проверяют взаимодействие между несколькими модулями: например, что после создания пользователя в базе данных его распознаёт система входа. Модули могут быть исправны поодиночке и всё равно не сойтись друг с другом — это ловится именно здесь.
-## Пирамида тестирования
+### Сквозные тесты (E2E)
-Пирамида тестирования — это модель, которая помогает визуализировать рекомендуемое соотношение различных типов тестов в проекте.
+Проверяют систему целиком с точки зрения пользователя, проходя через все слои приложения. Пример — полный сценарий регистрации на сайте: заполнить форму, отправить данные, получить письмо, войти в систему.
-
+## Пирамида тестирования
-**Идея пирамиды:**
+Сколько каких тестов писать? Ответ обычно рисуют пирамидой:
-- **Модульные тесты** составляют основу. Их должно быть больше всего, так как они быстрые, надежные и точно указывают на место ошибки.
-- **Интеграционные тесты** находятся на следующем уровне. Их меньше, чем модульных, они проверяют взаимодействие компонентов.
-- **Функциональные (E2E) тесты** находятся на вершине. Их должно быть наименьшее количество, так как они медленные, хрупкие (часто ломаются из-за изменений в UI) и дорогие в поддержке.
+| Уровень | Характеристика |
+| ---------------- | ---------------- |
+| Модульные (unit) | быстрые · много |
+| Интеграционные | среднее |
+| Сквозные (E2E) | медленные · мало |
-Соблюдение этой структуры помогает создать эффективную и надежную стратегию тестирования.
+Чем выше уровень, тем тесты медленнее и дороже в поддержке — поэтому их меньше. Основной объём составляют быстрые модульные тесты: они точно указывают на место поломки.
## Принципы хорошего теста (FIRST)
-Чтобы тесты были действительно полезными, они должны соответствовать некоторым принципам. Акроним FIRST помогает их запомнить:
+Пять признаков полезного теста складываются в акроним FIRST:
-- **F**ast (Быстрые): Тесты должны выполняться быстро. Медленные тесты замедляют разработку и их реже запускают.
-- **I**ndependent/Isolated (Независимые/Изолированные): Тесты не должны зависеть друг от друга. Порядок их выполнения не должен влиять на результат. Каждый тест должен настраивать свое окружение и очищать его при необходимости.
-- **R**epeatable (Повторяемые): Тесты должны давать одинаковый результат при каждом запуске в одной и той же среде. Никаких случайных сбоев или зависимости от внешних изменяемых факторов.
-- **S**elf-Validating (Самопроверяемые): Тест должен сам определять, прошел он или нет, без необходимости ручной проверки результатов. Обычно это достигается с помощью `assert` утверждений.
-- **T**imely/Thorough (Своевременные/Тщательные): Тесты нужно писать своевременно (в идеале, до или вместе с кодом, который они тестируют – TDD). Они должны быть достаточно тщательными, чтобы покрывать важные аспекты тестируемого кода, включая граничные случаи.
+- **F**ast — быстрый: медленные тесты запускают редко;
+- **I**ndependent — независимый: не полагается на другие тесты и порядок запуска;
+- **R**epeatable — повторяемый: даёт один и тот же результат при каждом запуске;
+- **S**elf-validating — самопроверяемый: сам говорит, прошёл или нет, без ручного сравнения результатов;
+- **T**imely — своевременный: написан вместе с кодом, а не «когда-нибудь потом».
-## Краткое введение в фреймворки для тестирования
+## Фреймворки для тестирования
-Хотя можно писать проверки вручную, как в примере с функцией `add`, это быстро становится неэффективным. Фреймворки для тестирования предоставляют инструменты и структуру для написания, организации и запуска тестов, а также для получения отчетов о результатах.
+Писать проверки вручную, как в примере с `add`, быстро надоедает: нужен запуск пачкой, отчёты, удобные сравнения. Это работа фреймворков, и в Python их два основных:
-В Python популярными фреймворками являются:
+- **pytest** — сторонний, с лаконичным синтаксисом; с него и начнём;
+- **unittest** — встроенный в стандартную библиотеку.
-- **`pytest`**: Мощный и гибкий сторонний фреймворк, известный своим лаконичным синтаксисом. Мы начнем изучение именно с него.
-- **`unittest`**: Встроенный в Python модуль, следующий классическому xUnit стилю.
+## Проверка понимания
-## Что дальше?
+**Какое утверждение о тестировании программного обеспечения наиболее точно?**
-В следующей статье мы погрузимся в практическое написание тестов с использованием фреймворка `pytest`, который поможет нам легко и эффективно автоматизировать проверку нашего кода.
+1. **Правильный ответ:** Тестирование помогает обнаруживать ошибки на ранних стадиях разработки, снижая стоимость их исправления. — Чем раньше поймана ошибка, тем дешевле починка — вплоть до стократной разницы между разработкой и продакшеном.
-**Какое утверждение о тестировании программного обеспечения наиболее точно?**
+2. Основная цель тестирования — достижение 100% покрытия кода тестами. — Хотя покрытие — важная метрика (о ней мы поговорим позже), само по себе 100% покрытие не гарантирует отсутствие ошибок. Качество тестов важнее.
+
+3. Функциональные (E2E) тесты являются самыми быстрыми и должны составлять основу пирамиды тестирования. — Согласно пирамиде тестирования, основу составляют модульные (unit) тесты: они самые быстрые и многочисленные. Сквозные тесты находятся на вершине пирамиды.
+
+4. Принцип FIRST означает, что тесты должны быть прежде всего Тщательными (Thorough), даже если они медленные. — FIRST как раз подчёркивает важность быстроты (Fast): медленные тесты запускают редко, и пользы от них меньше.
+
+В следующей статье — практика с `pytest`: первые настоящие тесты, оператор `assert` и запуск пачкой.
diff --git a/course/module-3/iterations-and-loops/article.md b/course/module-3/iterations-and-loops/article.md
deleted file mode 100644
index 1548ebd..0000000
--- a/course/module-3/iterations-and-loops/article.md
+++ /dev/null
@@ -1,331 +0,0 @@
-# Итерации и циклы
-
-> Итерация в Python — это процесс последовательного перебора элементов в коллекции (например, списке, кортеже, строке).
-> Циклы — это конструкции, которые позволяют автоматизировать процесс итерации и выполнять блок кода несколько раз.
-
-## Цикл for
-
-Цикл `for` в Python используется для итерации по последовательностям (спискам, кортежам, строкам и другим итерируемым объектам):
-
-```python
-# Итерация по списку
-fruits = ["яблоко", "банан", "апельсин"]
-for fruit in fruits:
- print(fruit)
-# Вывод:
-# яблоко
-# банан
-# апельсин
-
-# Итерация по строке
-for char in "Python":
- print(char)
-# Вывод:
-# P
-# y
-# t
-# h
-# o
-# n
-```
-
-### Функция range()
-
-Функция `range()` генерирует последовательность чисел и часто используется с циклом `for` для выполнения кода определенное количество раз:
-
-```python
-# range(stop) - генерирует числа от 0 до stop-1
-for i in range(5):
- print(i)
-# Вывод:
-# 0
-# 1
-# 2
-# 3
-# 4
-
-# range(start, stop) - генерирует числа от start до stop-1
-for i in range(2, 6):
- print(i)
-# Вывод:
-# 2
-# 3
-# 4
-# 5
-
-# range(start, stop, step) - генерирует числа от start до stop-1 с шагом step
-for i in range(1, 10, 2):
- print(i)
-# Вывод:
-# 1
-# 3
-# 5
-# 7
-# 9
-```
-
-### Функция enumerate()
-
-Функция `enumerate()` позволяет получить индекс элемента вместе с самим элементом:
-
-```python
-fruits = ["яблоко", "банан", "апельсин"]
-for index, fruit in enumerate(fruits):
- print(f"Индекс: {index}, Значение: {fruit}")
-# Вывод:
-# Индекс: 0, Значение: яблоко
-# Индекс: 1, Значение: банан
-# Индекс: 2, Значение: апельсин
-
-# Можно задать начальный индекс
-for index, fruit in enumerate(fruits, 1):
- print(f"Номер {index}: {fruit}")
-# Вывод:
-# Номер 1: яблоко
-# Номер 2: банан
-# Номер 3: апельсин
-```
-
-### Итерация по нескольким последовательностям с zip()
-
-Функция `zip()` позволяет одновременно итерироваться по нескольким последовательностям:
-
-```python
-names = ["Иван", "Мария", "Алексей"]
-ages = [25, 30, 22]
-
-for name, age in zip(names, ages):
- print(f"{name}: {age} лет")
-# Вывод:
-# Иван: 25 лет
-# Мария: 30 лет
-# Алексей: 22 лет
-```
-
-## Цикл while
-
-Цикл `while` выполняет блок кода, пока заданное условие остается истинным:
-
-```python
-# Базовый цикл while
-count = 0
-while count < 5:
- print(count)
- count += 1
-# Вывод:
-# 0
-# 1
-# 2
-# 3
-# 4
-
-# Использование условия на пользовательском вводе
-password = ""
-while password != "secret":
- password = input("Введите пароль: ")
-print("Правильный пароль!")
-```
-
-### Бесконечные циклы
-
-Если условие в цикле `while` всегда истинно, цикл будет выполняться бесконечно. Чтобы выйти из такого цикла, используются операторы `break` или исключения:
-
-```python
-# Бесконечный цикл с выходом по условию
-while True:
- user_input = input("Введите 'выход' для завершения: ")
- if user_input.lower() == "выход":
- break
- print(f"Вы ввели: {user_input}")
-```
-
-## Операторы break, continue и else в циклах
-
-### Оператор break
-
-Оператор `break` позволяет досрочно выйти из цикла:
-
-```python
-# Поиск в списке с выходом при нахождении
-fruits = ["яблоко", "банан", "апельсин", "груша"]
-for fruit in fruits:
- if fruit == "апельсин":
- print("Нашли апельсин!")
- break
- print(f"Проверяем: {fruit}")
-# Вывод:
-# Проверяем: яблоко
-# Проверяем: банан
-# Нашли апельсин!
-```
-
-### Оператор continue
-
-Оператор `continue` пропускает оставшийся код в текущей итерации и переходит к следующей:
-
-```python
-# Пропуск нечетных чисел
-for i in range(1, 6):
- if i % 2 != 0: # Если число нечетное
- continue
- print(i)
-# Вывод:
-# 2
-# 4
-```
-
-### Блок else в циклах
-
-И `for`, и `while` могут иметь необязательный блок `else`, который выполняется, если цикл завершился нормально (без `break`):
-
-```python
-# Проверка, является ли число простым
-num = 13
-for i in range(2, num):
- if num % i == 0:
- print(f"{num} не является простым числом")
- break
-else:
- print(f"{num} является простым числом")
-# Вывод:
-# 13 является простым числом
-
-# Пример с циклом while
-count = 0
-while count < 3:
- print(count)
- count += 1
-else:
- print("Цикл while завершен нормально")
-# Вывод:
-# 0
-# 1
-# 2
-# Цикл while завершен нормально
-```
-
-## Генераторы списков (List Comprehensions)
-
-Генераторы списков (также называемые "понимание списков") — это компактный способ создания списков из других последовательностей:
-
-```python
-# Обычный способ создания списка
-squares = []
-for i in range(1, 6):
- squares.append(i ** 2)
-print(squares) # [1, 4, 9, 16, 25]
-
-# То же самое с помощью генератора списков
-squares = [i ** 2 for i in range(1, 6)]
-print(squares) # [1, 4, 9, 16, 25]
-
-# Генератор списков с условием
-even_squares = [i ** 2 for i in range(1, 11) if i % 2 == 0]
-print(even_squares) # [4, 16, 36, 64, 100]
-
-# Генератор списков с несколькими условиями (ветвление)
-numbers = [i if i % 2 == 0 else -i for i in range(1, 6)]
-print(numbers) # [-1, 2, -3, 4, -5]
-```
-
-## Генераторы словарей и множеств
-
-Аналогично генераторам списков можно создавать генераторы словарей и множеств:
-
-```python
-# Генератор словарей
-word = "hello"
-char_positions = {char: index for index, char in enumerate(word)}
-print(char_positions) # {'h': 0, 'e': 1, 'l': 3, 'o': 4}
-
-# Генератор множеств
-vowels = "aeiou"
-consonants = {char for char in "python" if char not in vowels}
-print(consonants) # {'p', 'y', 't', 'h', 'n'}
-```
-
-## Выражения-генераторы (Generator Expressions)
-
-Выражения-генераторы похожи на генераторы списков, но используют круглые скобки вместо квадратных и генерируют значения "на лету", что позволяет экономить память:
-
-```python
-# Генератор списков (создает весь список в памяти)
-list_comp = [i ** 2 for i in range(1000000)]
-# Занимает много памяти
-
-# Выражение-генератор (генерирует значения по запросу)
-gen_exp = (i ** 2 for i in range(1000000))
-# Занимает минимум памяти
-
-# Использование выражения-генератора с sum()
-total = sum(i ** 2 for i in range(1, 6))
-print(total) # 55 (1 + 4 + 9 + 16 + 25)
-```
-
-## Итерация по словарям
-
-Существует несколько способов итерации по словарям:
-
-```python
-person = {
- "name": "Иван",
- "age": 30,
- "city": "Москва"
-}
-
-# Итерация по ключам (по умолчанию)
-for key in person:
- print(key, person[key])
-
-# Явная итерация по ключам
-for key in person.keys():
- print(key)
-
-# Итерация по значениям
-for value in person.values():
- print(value)
-
-# Итерация по парам ключ-значение
-for key, value in person.items():
- print(f"{key}: {value}")
-```
-
-## Вложенные циклы
-
-Циклы могут быть вложены друг в друга:
-
-```python
-# Вложенные циклы для создания таблицы умножения
-for i in range(1, 4):
- for j in range(1, 4):
- print(f"{i} * {j} = {i * j}")
- print("-----")
-# Вывод:
-# 1 * 1 = 1
-# 1 * 2 = 2
-# 1 * 3 = 3
-# -----
-# 2 * 1 = 2
-# 2 * 2 = 4
-# 2 * 3 = 6
-# -----
-# 3 * 1 = 3
-# 3 * 2 = 6
-# 3 * 3 = 9
-# -----
-```
-
-## Самостоятельные упражнения
-
-1. Напишите программу, которая запрашивает у пользователя числа до тех пор, пока он не введет 0, а затем выводит сумму всех введенных чисел.
-2. Создайте генератор списков, который генерирует список квадратов четных чисел от 1 до 20.
-3. Напишите программу, которая выводит все числа от 1 до 100, но вместо чисел, кратных 3, выводит "Fizz", вместо чисел, кратных 5, выводит "Buzz", а вместо чисел, кратных и 3, и 5, выводит "FizzBuzz".
-4. Используя вложенные циклы, создайте программу, которая печатает следующий шаблон:
- ```
- *
- **
- ***
- ****
- *****
- ```
-5. Создайте словарь, где ключами будут числа от 1 до 10, а значениями — их кубы, используя генератор словарей.
diff --git a/course/module-3/mocks-and-stubs/article.md b/course/module-3/mocks-and-stubs/article.md
index 6514b79..45c7d44 100644
--- a/course/module-3/mocks-and-stubs/article.md
+++ b/course/module-3/mocks-and-stubs/article.md
@@ -1,13 +1,23 @@
+---
+meta:
+ title: "Моки и заглушки: изоляция тестов от внешних зависимостей"
+ description: "Зачем нужны моки и заглушки в Python, как использовать unittest.mock.patch, и базовая работа с unittest.mock.Mock."
+---
+
# Моки и заглушки: изоляция тестов
-Реальный код часто общается с внешним миром: HTTP-запросы, БД, файлы, время. В тестах это плохо: сеть может упасть, БД может быть медленной, время неуправляемо. Тест должен проверять **вашу логику**, а не работоспособность чужих сервисов.
+Один и тот же тест платежей то проходит мгновенно, то висит секунд десять, а иногда падает по таймауту — хотя код никто не трогал. Всё дело в том, что он каждый раз ходит в настоящую песочницу платёжного провайдера, а она то быстрая, то нет. По сути тест проверяет чужой сервер, а не **вашу логику**.
+
+Так и происходит, когда код в тестах общается с внешним миром: HTTP-запросы, БД, файлы, время. Сеть падает, БД тормозит, время неуправляемо — значит, зависимость на время теста нужно чем-то заменить.
Решение — **тестовый двойник**: подсовываем коду фейковый объект, который ведёт себя как нужно. У этого двойника есть две роли:
-- **Stub** (заглушка) — возвращает заранее заданные ответы, проверять ничего не нужно.
-- **Mock** — то же самое плюс умеет фиксировать вызовы (с какими аргументами и сколько раз), чтобы тест мог это проверить.
+- **Stub** (заглушка) — возвращает заранее заданные ответы, проверять ничего не нужно.
+- **Mock** — то же самое плюс умеет фиксировать вызовы (с какими аргументами и сколько раз), чтобы тест мог это проверить.
+
+Оба делаются одним инструментом — `unittest.mock` из стандартной библиотеки. Название историческое: модуль приехал вместе со встроенным фреймворком `unittest` (доберёмся до него через статью), но к pytest он привязан ничуть не меньше — ставить ничего не нужно, работает как есть.
-На практике в Python оба делаются одним инструментом: `unittest.mock`.
+
## Mock-объект
@@ -35,9 +45,31 @@ def test_payment_success():
Функция `process_payment` принимает `service` аргументом. В production-коде туда передают реальный платёжный сервис; в тесте мы подсовываем Mock через тот же аргумент. В этом и смысл «зависимость через параметр». Что происходит дальше:
-- `Mock()` создаёт объект, у которого **любой** атрибут или метод существует автоматически.
-- `service.charge.return_value = True` говорит «когда тест вызовет `service.charge(...)`, верни `True`».
-- `assert_called_once_with(100)` проверяет: «метод `charge` был вызван ровно один раз с аргументом `100`». Это и есть отличие Mock от Stub: тест не просто получает данные, он проверяет **как** код взаимодействовал с зависимостью.
+- `Mock()` создаёт объект, у которого **любой** атрибут или метод существует автоматически.
+- `service.charge.return_value = True` говорит «когда тест вызовет `service.charge(...)`, верни `True`».
+- `assert_called_once_with(100)` проверяет: «метод `charge` был вызван ровно один раз с аргументом `100`». Это и есть отличие Mock от Stub: тест не просто получает данные, он проверяет **как** код взаимодействовал с зависимостью.
+
+Мока проще один раз потрогать, чем описать. Запустите — и обратите внимание на последнюю строку: у `Mock` существует даже метод, которого мы никогда не задавали:
+
+```python
+from unittest.mock import Mock
+
+m = Mock()
+m.charge.return_value = True
+
+print(m.charge(100))
+
+print(m.charge.call_args)
+
+print(type(m.metod_kotorogo_net).__name__)
+
+```
## patch: подменить уже существующий объект
@@ -72,9 +104,9 @@ def test_get_user(mock_get):
Что важно:
-- `@patch("requests.get")` подменяет `requests.get` на мок только на время теста, после теста всё возвращается обратно.
-- Аргумент `mock_get` (имя любое) — это автоматически созданный мок, который заменил оригинал. Через него настраиваем поведение и проверяем вызовы.
-- `response.json()` это **метод** (с круглыми скобками), поэтому настраиваем `mock_response.json.return_value` — у вложенного мока `json` свой собственный `return_value`. Любой атрибут или метод мока тоже мок, по цепочке.
+- `@patch("requests.get")` подменяет `requests.get` на мок только на время теста, после теста всё возвращается обратно.
+- Аргумент `mock_get` (имя любое) — это автоматически созданный мок, который заменил оригинал. Через него настраиваем поведение и проверяем вызовы.
+- `response.json()` это **метод** (с круглыми скобками), поэтому настраиваем `mock_response.json.return_value` — у вложенного мока `json` свой собственный `return_value`. Любой атрибут или метод мока тоже мок, по цепочке.
`patch` можно использовать и как **контекстный менеджер** (`with patch(...) as mock_get:`) — пригодится, когда подмена нужна только для части теста.
@@ -141,11 +173,16 @@ mock_api.connect.side_effect = ConnectionError("network down")
Правило: **мокайте границы системы** — внешние API, БД, файловую систему, время. Свой код тестируйте напрямую, без моков.
-## Что дальше?
+## Проверка понимания
-В следующей статье — встроенный модуль **`unittest`**: классический xUnit-стиль с классами `TestCase` и методами `setUp`/`tearDown`. Это альтернатива pytest, которую вы встретите в legacy-коде.
+**Что верно про моки и заглушки в Python?**
----
+1. Заглушка (Stub) проверяет, что методы были вызваны с правильными аргументами — Заглушки просто возвращают заранее заданные данные. Проверка вызовов это задача моков (Mocks).
-**Что верно про моки и заглушки в Python?**
+2. **Правильный ответ:** patch из unittest.mock временно заменяет объект на мок во время теста — После завершения теста patch возвращает оригинальный объект на место.
+
+3. side_effect нужен только для возврата последовательных значений — side_effect также позволяет выбрасывать исключения (например, ConnectionError) или вызывать произвольную функцию.
+4. В patch путь указывают туда, где объект определён, а не туда, где он используется — Наоборот: патчить нужно там, где объект ищется во время вызова — по пути в модуле, который его использует, а не там, где он изначально определён. Это самая частая ошибка с patch.
+
+В следующей статье — встроенный модуль **`unittest`**: классический xUnit-стиль с классами `TestCase` и методами `setUp`/`tearDown`. Это альтернатива pytest, которую вы встретите в legacy-коде.
diff --git a/course/module-3/pytest-basics/article.md b/course/module-3/pytest-basics/article.md
index cb8c21d..6dfe4b3 100644
--- a/course/module-3/pytest-basics/article.md
+++ b/course/module-3/pytest-basics/article.md
@@ -1,6 +1,12 @@
+---
+meta:
+ title: "Основы pytest: первые тесты в Python"
+ description: "Базовая работа с pytest: установка, написание тестов через assert, запуск и чтение вывода. Минимум на старте."
+---
+
# Основы pytest: первые тесты в Python
-`pytest` это самый распространённый фреймворк для тестирования в Python. У него простой стартовый интерфейс: вы пишете обычные Python-функции с `assert`, запускаете `pytest` в терминале — он сам находит тесты и выводит, что прошло и что упало.
+После каждой правки хочется убедиться, что ничего не сломалось: запускаете пример, смотрите на вывод, проверяете ещё один. На пятый раз это надоедает, и рутину забирает `pytest`, самый распространённый фреймворк для тестирования в Python. Тест у него — обычная функция с `assert`: запускаете `pytest` в терминале, а он сам находит тесты и показывает, что прошло, а что упало.
## Установка
@@ -10,20 +16,22 @@ pip install pytest
## Первый тест
+`assert` — это оператор самого Python, а не часть pytest. Запись `assert выражение` ничего не делает, если выражение истинно, и выбрасывает `AssertionError`, если оно ложно. На этом pytest и стоит: тест — обычная функция, которая либо молча отработала до конца, либо упала на первом же несошедшемся `assert`.
+
Производственный код кладём в `example.py`, тесты в файл рядом с префиксом `test_`. По этому префиксу `pytest` и находит тесты автоматически:
-
+
+```
Тест это обычная функция, имя начинается с `test_`, внутри используется стандартный Python-овский `assert`. Никаких специальных классов, наследований, регистрации.
+
+
## Запуск
В терминале, в директории с тестами:
@@ -91,12 +99,27 @@ test_example.py:9: AssertionError
`pytest` показывает:
-- **где** упало (`test_example.py:9: AssertionError`)
-- **какое выражение** не сработало (`assert add(5, -2) == 10`)
-- **что получилось вместо ожидаемого** (`3 == 10`, и что `3 = add(5, -2)`)
+- **где** упало (`test_example.py:9: AssertionError`)
+- **какое выражение** не сработало (`assert add(5, -2) == 10`)
+- **что получилось вместо ожидаемого** (`3 == 10`, и что `3 = add(5, -2)`)
Такой развёрнутый вывод обычно сразу подсказывает причину. Это и есть главный аргумент в пользу обычного `assert` вместо специальных методов: pytest сам разбирает выражение и показывает интересные части.
+## Проверка, что код падает
+
+Тест фиксирует не только правильный ответ, но и правильную ошибку. Вызов `add("a", 1)` должен падать с `TypeError` — это тоже поведение, и его стоит закрепить. Обычным `assert` такое не проверить: исключение прервёт тест раньше, чем дело дойдёт до проверки. Для этого есть `pytest.raises`:
+
+```python
+import pytest
+from example import add
+
+def test_add_string_and_number_raises():
+ with pytest.raises(TypeError):
+ add("a", 1)
+```
+
+Тест пройдёт, если внутри блока `with` вылетит `TypeError`, и упадёт, если исключения не будет.
+
## Структура теста: Arrange / Act / Assert
По мере роста тестов появляется паттерн, который сильно упрощает их чтение: **AAA (Arrange / Act / Assert)**.
@@ -123,17 +146,22 @@ def test_user_can_change_email():
В реальных проектах имя теста читается как короткое утверждение о коде. Удобный шаблон: **`test_<что>_<при каких условиях>_<ожидаемое поведение>`**:
-- `test_add_returns_sum_for_positive_numbers`, понятнее, чем `test_add_1`
-- `test_withdraw_fails_when_balance_is_zero`, сразу видно, что и почему
-- `test_user_email_is_lowercased_after_save`, конкретное поведение
+- `test_add_returns_sum_for_positive_numbers`, понятнее, чем `test_add_1`
+- `test_withdraw_fails_when_balance_is_zero`, сразу видно, что и почему
+- `test_user_email_is_lowercased_after_save`, конкретное поведение
Когда такой тест падает в CI, по имени **сразу** понятно, что именно сломалось, без чтения тела. Это экономит часы дебага в больших проектах.
-## Что дальше?
+## Проверка понимания
-В следующей статье возьмём две главные «суперсилы» pytest: **фикстуры** (для общей настройки тестов) и **параметризацию** (для запуска одного теста с разными данными).
+**Какое утверждение об основах работы с `pytest` верно?**
----
+1. Для запуска pytest всегда нужно явно перечислить тестовые файлы — pytest сам находит файлы с префиксом test\_ и функции с префиксом test\_. Передавать имена вручную не нужно.
-**Какое утверждение об основах работы с `pytest` верно?**
+2. При падении теста pytest просто пишет FAILED без подробностей — Наоборот: pytest показывает выражение, фактические значения и место сбоя, чтобы быстро понять причину.
+3. **Правильный ответ:** pytest использует стандартный Python-овский assert для проверок — Никаких специальных методов вроде self.assertEqual() — обычный assert, и pytest сам разбирает его для красивого вывода.
+
+4. Все тесты в pytest должны быть методами классов — Не обязательно. Тесты в pytest — это обычные функции верхнего уровня. Классы можно использовать для группировки, но это опционально.
+
+В следующей статье возьмём две главные «суперсилы» pytest: **фикстуры** (для общей настройки тестов) и **параметризацию** (для запуска одного теста с разными данными).
diff --git a/course/module-3/pytest-fixtures-parametrization/article.md b/course/module-3/pytest-fixtures-parametrization/article.md
index bedcbc0..96627d1 100644
--- a/course/module-3/pytest-fixtures-parametrization/article.md
+++ b/course/module-3/pytest-fixtures-parametrization/article.md
@@ -1,6 +1,12 @@
+---
+meta:
+ title: "pytest: фикстуры и параметризация"
+ description: "Две главные возможности pytest: фикстуры для общей настройки тестов через @pytest.fixture с yield, и параметризация одного теста через @pytest.mark.parametrize."
+---
+
# pytest: фикстуры и параметризация
-В прошлой статье мы писали простые тесты-функции. Сейчас разберём две вещи, без которых тестирование быстро становится копипастой: **фикстуры** (общая подготовка для нескольких тестов) и **параметризация** (один тест на много входов).
+Дюжина тестов, и каждый начинается с одних и тех же трёх строк: собрать пользователя, заполнить поля, открыть соединение. В проект добавили ещё одно поле — и вы правите эти строки в каждом тесте, один, конечно, пропустив. Это та самая копипаста, от которой набор тестов начинает гнить. Убирают её две вещи из этой статьи: **фикстуры** (общая подготовка для нескольких тестов) и **параметризация** (один тест на много входов).
## Фикстуры: общая подготовка
@@ -47,6 +53,8 @@ def test_read(temp_file):
Код **до** `yield` выполняется перед тестом (setup), код **после** — после теста (teardown). Работает даже если тест упал.
+
+
## Scope: как часто пересоздавать
По умолчанию фикстура вызывается **для каждого теста**. Если подготовка дорогая (открыть подключение к БД, загрузить большой файл), можно сказать pytest «делай один раз на всю сессию» через `scope="session"`:
@@ -66,26 +74,24 @@ def db_connection():
Если фикстура нужна в нескольких файлах, положите её в `conftest.py` рядом с тестами. Импортировать не нужно — pytest найдёт автоматически:
-
+
+```
Это стандартный способ делиться фикстурами между тестами одного проекта.
@@ -131,11 +137,16 @@ test_discount.py::test_get_discount[30-True-0.1] PASSED
Если один из случаев упал, в имени теста видны его параметры — сразу понятно, какая комбинация сломалась.
-## Что дальше?
+## Проверка понимания
-В следующей статье — **моки и заглушки**: как изолировать тесты от внешних зависимостей (база, API, время), чтобы они оставались быстрыми и предсказуемыми.
+**Что верно о фикстурах и параметризации в `pytest`?**
----
+1. Фикстура с scope="function" выполняется один раз за всю сессию — Наоборот: scope="function" это поведение по умолчанию, фикстура вызывается для каждого теста. Один раз за сессию это scope="session".
-**Что верно о фикстурах и параметризации в `pytest`?**
+2. **Правильный ответ:** Файл conftest.py позволяет шарить фикстуры между файлами без импорта — pytest сам находит фикстуры из conftest.py для тестов в этой директории и поддиректориях.
+
+3. @pytest.mark.parametrize задаёт scope фикстуры — Нет, parametrize запускает один тест с разными входными данными. Scope фикстуры задаётся в @pytest.fixture(scope=...).
+4. yield в фикстуре только возвращает значение, ничего больше — Возвращает значение тесту, но код ПОСЛЕ yield выполняется как teardown — после того, как тест отработал.
+
+В следующей статье — **моки и заглушки**: как изолировать тесты от внешних зависимостей (база, API, время), чтобы они оставались быстрыми и предсказуемыми.
diff --git a/course/module-3/sqlalchemy-core-basics/article.md b/course/module-3/sqlalchemy-core-basics/article.md
index 42a1699..a917714 100644
--- a/course/module-3/sqlalchemy-core-basics/article.md
+++ b/course/module-3/sqlalchemy-core-basics/article.md
@@ -1,12 +1,35 @@
+---
+meta:
+ title: "SQLAlchemy Core: SQL из Python-выражений"
+ description: "Как строить SQL из Python-выражений: Engine, Table, помощники insert/select/update/delete и один код для разных СУБД."
+---
+
# SQLAlchemy Core: SQL из Python-выражений
-В прошлой статье мы выполняли SQL-запросы через `sqlite3`. Это работает, но есть две проблемы.
+В прошлой главе SQL-запросы жили в строках, и пока запрос постоянный, со строкой нет проблем. Но чаще запрос зависит от данных: скажем, найти задачу по названию, которое ввёл пользователь. Первым в голову приходит f-строка:
+
+```python
+query = f"SELECT * FROM tasks WHERE title = '{name}'"
+```
+
+Работает ровно до того дня, когда в `name` прилетает апостроф или чужой кусок SQL. За удобством прячутся две проблемы сырых строк.
-**Первая: SQL живёт в строке**, и любая опечатка или неаккуратная вставка пользовательских данных это потенциальная SQL-инъекция. Параметры `?` спасают, но про них нужно помнить каждый раз.
+**Первая: SQL живёт в строке**, и любая неаккуратная вставка пользовательских данных — потенциальная SQL-инъекция. Параметры `?` спасают, но про них нужно помнить каждый раз.
**Вторая: каждая СУБД имеет свой диалект SQL.** Если приложение пишется под SQLite, а потом переезжает на PostgreSQL — почти наверняка часть запросов придётся переписывать.
-**SQLAlchemy Core** решает обе проблемы: SQL строится из Python-выражений, безопасность встроена по умолчанию, и один и тот же код работает с PostgreSQL, MySQL, SQLite. Простые `SELECT/WHERE` действительно похожи во всех СУБД, но как только заходим в специфические функции (даты, строки, агрегаты) или схему, синтаксис расходится, и Core переводит ваш Python в правильный диалект:
+**SQLAlchemy Core** решает обе проблемы: SQL строится из Python-выражений, безопасность встроена по умолчанию, и один и тот же код работает с PostgreSQL, MySQL, SQLite. Простые `SELECT/WHERE` действительно похожи во всех СУБД, но как только заходим в специфические функции (даты, строки, агрегаты), синтаксис расходится, и Core переводит ваш Python в правильный диалект:
+
+Одно выражение — три диалекта: Postgres и MySQL совпали, SQLite получил свой вариант, и плейсхолдер у каждого свой
+
+Новых слов в Core по сути четыре:
+
+- `Engine` — подключение к базе;
+- `Table` — описание таблицы;
+- `insert`/`select`/`update`/`delete` — помощники вместо SQL-строк;
+- `.c` — доступ к колонкам.
+
+Всё остальное — знакомый по прошлой главе SQL.
## Установка
@@ -18,32 +41,34 @@ pip install sqlalchemy
## Engine: подключение
-`Engine` это объект, отвечающий за связь с БД. Создаётся один раз на приложение:
+`Engine` — это объект, отвечающий за связь с БД. Создаётся один раз на приложение, а адрес базы задаётся строкой подключения:
-```python-executable
+```python
from sqlalchemy import create_engine
-engine = create_engine('sqlite:///tasks.db', echo=True)
+engine = create_engine('sqlite:///core_tasks.db')
print("Engine готов")
-# Вывод: Engine готов
+
```
-Параметр `echo=True` включает вывод выполняемых SQL-запросов в консоль. Удобно в обучении и при отладке, в production его выключают.
+У `create_engine` есть параметр `echo=True`: с ним каждый выполняемый SQL печатается в консоль. Удобно при отладке, в production выключают.
Строка подключения для других СУБД:
-- `postgresql://user:pass@host:5432/dbname`
-- `mysql+pymysql://user:pass@host/dbname`
-- `sqlite:///file.db`
+- `postgresql://user:pass@host:5432/dbname`
+- `mysql+pymysql://user:pass@host/dbname`
+- `sqlite:///file.db`
## Описание таблицы
В Core структура таблицы описывается объектом `Table` — Python-эквивалент SQL-команды `CREATE TABLE`:
-```python-executable
+```python
from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, Boolean
-engine = create_engine('sqlite:///tasks.db')
+engine = create_engine('sqlite:///core_tasks.db')
metadata = MetaData()
tasks_table = Table(
@@ -58,105 +83,127 @@ tasks_table = Table(
metadata.create_all(engine)
print("Таблица tasks готова")
-# Вывод: Таблица tasks готова
+
```
-`MetaData` это коллекция всех `Table`-объектов приложения. `metadata.create_all(engine)` создаёт сразу все таблицы из коллекции, которых ещё нет в БД.
+`nullable=False` — тот же `NOT NULL` из прошлой главы: поле обязательное. `MetaData` — коллекция всех `Table`-объектов приложения. `metadata.create_all(engine)` создаёт сразу все таблицы из коллекции, которых ещё нет в БД.
+
+## Те же четыре операции, но выражениями
+
+Те же CREATE, READ, UPDATE, DELETE, что и в прошлой главе, — только вместо SQL-строк Python-выражения. Для каждой операции в Core есть помощник: `insert()`, `select()`, `update()`, `delete()`.
-## CRUD: четыре операции
+Новое в знакомой четвёрке: пакетная вставка списком, доступ к колонкам через `.c` и Python-операторы в `.where()`. База в примерах создаётся в памяти: строка подключения `sqlite:///:memory:` даёт чистую базу при каждом запуске.
+
+### INSERT и SELECT
+
+```python
+from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, Boolean
+from sqlalchemy import insert, select
-Для каждой операции в Core есть готовый помощник: `insert()`, `select()`, `update()`, `delete()`. Дальше предполагаем, что `engine` и `tasks_table` уже определены, как выше.
+# Подготовка: движок и таблица из примеров выше
+engine = create_engine('sqlite:///:memory:')
+metadata = MetaData()
-### INSERT
+tasks_table = Table(
+ 'tasks', metadata,
+ Column('id', Integer, primary_key=True),
+ Column('title', String, nullable=False),
+ Column('completed', Boolean, default=False),
+)
-```python-executable
-from sqlalchemy import insert
+metadata.create_all(engine)
with engine.connect() as connection:
result = connection.execute(
insert(tasks_table),
[
- {'title': 'Изучить SQLAlchemy Core', 'completed': False},
- {'title': 'Написать приложение', 'completed': False},
- {'title': 'Протестировать код', 'completed': False},
+ {'title': 'Изучить SQLAlchemy Core'},
+ {'title': 'Написать приложение'},
],
)
connection.commit()
+ print(f"Добавлено строк: {result.rowcount}")
+
-print(f"Добавлено строк: {result.rowcount}")
-# Вывод: Добавлено строк: 3
-```
-
-Значения передаются списком словарей — это batch-вставка одним запросом. SQLAlchemy сам подставит параметры безопасно.
-
-### SELECT
-
-```python-executable
-from sqlalchemy import select
-
-with engine.connect() as connection:
result = connection.execute(select(tasks_table))
for row in result:
print(row.id, row.title, row.completed)
-# Вывод:
-# 1 Изучить SQLAlchemy Core 0
-# 2 Написать приложение 0
-# 3 Протестировать код 0
+
```
-Доступ к колонкам по имени (`row.title`), а не по индексу как в `sqlite3`. Условие фильтрации добавляется через `.where()`:
+Значения в `insert` передаются списком словарей: это batch-вставка одним запросом, и параметры SQLAlchemy подставит безопасно сам.
-```python-executable
-from sqlalchemy import select
+Колонку `completed` мы не указываем, потому что сработало `default=False` из описания таблицы.
-with engine.connect() as connection:
- result = connection.execute(
- select(tasks_table).where(tasks_table.c.id == 1)
- )
- row = result.first()
- print(row.title)
-# Вывод: Изучить SQLAlchemy Core
-```
+Строки читаются доступом по имени (`row.title`), а не по индексу, как в `sqlite3`. И `completed` вернулся как `False`, а не `0`: SQLAlchemy знает тип колонки (`Boolean`) и сам приводит значение к Python-типу.
-`tasks_table.c.id` это «колонка `id` таблицы `tasks`». Сравнения (`==`, `>`, `<`, `.in_()`, `.like()`) превращаются в SQL автоматически.
+### UPDATE и DELETE
-### UPDATE
+Условие фильтрации добавляется методом `.where()`, строки обновляет помощник `update()`, удаляет — `delete()`:
+
+```python
+from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, Boolean
+from sqlalchemy import insert, select, update, delete
-```python-executable
-from sqlalchemy import update
+# Подготовка: движок, таблица и две задачи
+engine = create_engine('sqlite:///:memory:')
+metadata = MetaData()
+
+tasks_table = Table(
+ 'tasks', metadata,
+ Column('id', Integer, primary_key=True),
+ Column('title', String, nullable=False),
+ Column('completed', Boolean, default=False),
+)
+
+metadata.create_all(engine)
with engine.connect() as connection:
- result = connection.execute(
+ connection.execute(insert(tasks_table), [
+ {'title': 'Изучить SQLAlchemy Core'},
+ {'title': 'Написать приложение'},
+ ])
+ connection.commit()
+
+ connection.execute(
update(tasks_table)
.where(tasks_table.c.id == 1)
.values(completed=True)
)
+ connection.execute(
+ delete(tasks_table).where(tasks_table.c.id == 2)
+ )
connection.commit()
-print(f"Обновлено строк: {result.rowcount}")
-# Вывод: Обновлено строк: 1
+ result = connection.execute(select(tasks_table))
+ for row in result:
+ print(row.id, row.title, row.completed)
+
```
-### DELETE
+`tasks_table.c.id` — «колонка `id` таблицы `tasks`», а `==` превращается в SQL-сравнение автоматически; так же работают `>`, `<`, `.in_()` и `.like()`.
-```python-executable
-from sqlalchemy import delete
+Финальный `select` подтверждает: первая задача осталась одна и уже с `completed=True`. Обе операции сработали.
-with engine.connect() as connection:
- result = connection.execute(
- delete(tasks_table).where(tasks_table.c.id == 3)
- )
- connection.commit()
+## Проверка понимания
-print(f"Удалено строк: {result.rowcount}")
-# Вывод: Удалено строк: 1
-```
+**Главное преимущество SQLAlchemy Core перед сырыми SQL-строками в `sqlite3`?**
-## Что дальше?
+1. Запросы выполняются быстрее — Производительность примерно одинаковая, иногда Core даже чуть медленнее за счёт построения SQL из выражения. Главное преимущество не в скорости.
-В следующей статье возьмём **SQLAlchemy ORM** — слой выше Core, где таблицы становятся Python-классами, строки — объектами, и вам почти не нужно думать в терминах SQL. Хорошо подходит для типичной бизнес-логики; Core остаётся в арсенале для случаев, когда нужен точный контроль над запросом.
+2. **Правильный ответ:** Защита от SQL-инъекций и переносимость между разными СУБД — Значение уходит в драйвер отдельно от текста запроса, подставлять его в строку вручную не нужно, а один и тот же Python-код работает с PostgreSQL, MySQL и SQLite — Core сам подстраивает SQL под выбранную СУБД.
----
+3. Возможность вообще не писать SQL — Знание SQL по-прежнему нужно: Core напрямую соответствует SQL-операциям select, insert, update, delete. Просто синтаксис Python вместо строк.
-**Главное преимущество SQLAlchemy Core перед сырыми SQL-строками в `sqlite3`?**
+4. Автоматическая генерация документации к схеме — Core не генерирует документацию. Главные преимущества — безопасность и переносимость между СУБД.
+В следующей статье возьмём **SQLAlchemy ORM** — слой выше Core, где таблицы становятся Python-классами, строки — объектами, и вам почти не нужно думать в терминах SQL. Хорошо подходит для типичной бизнес-логики; Core остаётся в арсенале для случаев, когда нужен точный контроль над запросом.
diff --git a/course/module-3/sqlalchemy-orm-basics/article.md b/course/module-3/sqlalchemy-orm-basics/article.md
index 2bcab9d..f92e89d 100644
--- a/course/module-3/sqlalchemy-orm-basics/article.md
+++ b/course/module-3/sqlalchemy-orm-basics/article.md
@@ -1,14 +1,26 @@
+---
+meta:
+ title: "SQLAlchemy ORM: работа с БД через Python-объекты"
+ description: "Модели как классы, строки как объекты: DeclarativeBase и Mapped, Session, CRUD через атрибуты и связи relationship."
+---
+
# SQLAlchemy ORM: работа с БД через Python-объекты
-В Core мы строили SQL из Python-выражений: `select(tasks).where(tasks.c.id == 1)`. Это уже сильно лучше сырых SQL-строк, но в коде всё равно остаются «таблица + колонка», а не привычные объекты.
+В Core мы строили SQL из Python-выражений вида `select(...).where(...)`. Это уже сильно лучше сырых SQL-строк, но в коде всё равно остаются «таблица + колонка», а не привычные объекты.
+
+**ORM** (Object-Relational Mapping) идёт на шаг дальше: таблица описывается как Python-класс, строка таблицы — экземпляр этого класса, а изменение атрибута объекта автоматически отражается в БД. Получается работа с БД на языке обычных Python-объектов.
+
+**Python-объект:** `Task(id=1, title="Изучить SQLite")`
+
+**строка в tasks:** `1 | Изучить SQLite`
-**ORM** (Object-Relational Mapping) идёт на шаг дальше: таблица описывается как Python-класс, строка таблицы это экземпляр этого класса, а изменение атрибута объекта автоматически отражается в БД. Получается работа с БД на языке обычных Python-объектов.
+Атрибуты объекта соответствуют столбцам строки, а Session синхронизирует обе стороны
## Модель: класс как таблица
-В SQLAlchemy 2.0+ модели описываются через `DeclarativeBase` с аннотациями типов. Это современный стиль, который заменяет старый `declarative_base()`:
+В SQLAlchemy 2.0+ модели описываются через `DeclarativeBase` с аннотациями типов:
-```python-executable
+```python
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
@@ -25,48 +37,61 @@ class Task(Base):
def __repr__(self):
return f"Task(id={self.id}, title={self.title!r}, completed={self.completed})"
-engine = create_engine('sqlite:///orm_tasks.db')
+engine = create_engine('sqlite:///:memory:')
Base.metadata.create_all(engine)
print("Модель Task и таблица tasks готовы")
-# Вывод: Модель Task и таблица tasks готовы
+
```
Как это читать:
-- `class Task(Base)` — модель, наследник базового класса
-- `__tablename__ = 'tasks'` — имя таблицы в БД
-- `id: Mapped[int] = mapped_column(primary_key=True)` — колонка `id`, тип int, первичный ключ
-- `title: Mapped[str]` — колонка `title`, тип str, NOT NULL по умолчанию
-- `completed: Mapped[bool] = mapped_column(default=False)` — колонка `completed`, тип bool, по умолчанию `False`
+- `class Task(Base)` — модель; `Base` — общий родитель всех моделей, через него SQLAlchemy собирает список таблиц, как `MetaData` в Core;
+- `__tablename__` — имя таблицы в БД;
+- строки вида `title: Mapped[str]` — колонки: `Mapped[...]` помечает «это колонка таблицы», а тип в скобках становится типом колонки (`int` → INTEGER, `str` → VARCHAR, `bool` → BOOLEAN);
+- `mapped_column(...)` дописывают, только когда у колонки есть настройки — первичный ключ или значение по умолчанию;
+- `__repr__` — знакомый спецметод: как объект показывает себя при печати.
-Типы Python (`int`, `str`, `bool`) автоматически отображаются в SQL-типы (`INTEGER`, `VARCHAR`, `BOOLEAN`). Никаких отдельных `Column(Integer, ...)` как в Core.
+Поэтому `Base.metadata.create_all(engine)` выглядит знакомо: это та же `create_all`, что и в прошлой главе.
## Session: единица работы
-Для запросов в ORM используется `Session`. Это «единица работы»: она держит загруженные объекты в памяти, отслеживает изменения и одной командой сохраняет всё в БД.
+Для запросов в ORM используется `Session` — «единица работы»: она держит загруженные объекты в памяти, отслеживает изменения и одной командой сохраняет всё в БД.
-```python-executable
-from sqlalchemy.orm import Session
+Открывают её через `with Session(engine) as session:` — так сессия закроется сама, а `commit()` сохраняет накопленные изменения.
-with Session(engine) as session:
- # ... здесь работаем с объектами
- session.commit()
-
-print("Сессия закрыта")
-# Вывод: Сессия закрыта
-```
+## CRUD через объекты
-`with Session(...)` сам закроет сессию по выходу. `commit()` сохраняет накопленные изменения в БД.
+Третий раз те же четыре операции — но теперь вы не пишете ни SQL, ни выражений: меняете Python-объекты, а Session сам превращает это в нужные запросы.
-## CRUD через объекты
+CREATE, READ и DELETE узнаются с ходу; главное новое в ORM — отслеживание изменений и связи между таблицами.
-Дальше предполагаем, что `engine` и класс `Task` уже определены.
+База в примерах создаётся в памяти: строка подключения `sqlite:///:memory:` даёт чистую базу при каждом запуске.
### CREATE: создание
-```python-executable
-from sqlalchemy.orm import Session
+```python
+from sqlalchemy import create_engine
+from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session
+
+# Подготовка: модель и таблица из начала статьи
+class Base(DeclarativeBase):
+ pass
+
+class Task(Base):
+ __tablename__ = 'tasks'
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ title: Mapped[str]
+ completed: Mapped[bool] = mapped_column(default=False)
+
+ def __repr__(self):
+ return f"Task(id={self.id}, title={self.title!r}, completed={self.completed})"
+
+engine = create_engine('sqlite:///:memory:')
+Base.metadata.create_all(engine)
with Session(engine) as session:
task1 = Task(title="Изучить ORM")
@@ -75,99 +100,143 @@ with Session(engine) as session:
session.commit()
print(task1)
print(task2)
-# Вывод:
-# Task(id=1, title='Изучить ORM', completed=False)
-# Task(id=2, title='Написать код', completed=False)
+
```
Заметьте: `task1.id` после `commit()` уже заполнен. БД назначила его автоматически.
### READ: чтение
-В современном SQLAlchemy 2.0 запросы пишутся через `select()` + `session.execute()`. Старый `session.query(...)` ещё работает, но считается устаревшим.
+Запросы пишутся знакомым по Core `select()`, а выполняет их `session.execute()`:
+
+```python
+from sqlalchemy import create_engine, select
+from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session
+
+# Подготовка: модель, таблица и три задачи (одна уже выполнена)
+class Base(DeclarativeBase):
+ pass
+
+class Task(Base):
+ __tablename__ = 'tasks'
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ title: Mapped[str]
+ completed: Mapped[bool] = mapped_column(default=False)
+
+ def __repr__(self):
+ return f"Task(id={self.id}, title={self.title!r}, completed={self.completed})"
-```python-executable
-from sqlalchemy import select
-from sqlalchemy.orm import Session
+engine = create_engine('sqlite:///:memory:')
+Base.metadata.create_all(engine)
+
+with Session(engine) as session:
+ session.add_all([
+ Task(title="Изучить ORM"),
+ Task(title="Написать код"),
+ Task(title="Сдать проект", completed=True),
+ ])
+ session.commit()
with Session(engine) as session:
# Все строки
- tasks = session.execute(select(Task)).scalars().all()
+ stmt = select(Task)
+ tasks = session.execute(stmt).scalars().all()
for task in tasks:
print(task)
-# Вывод:
-# Task(id=1, title='Изучить ORM', completed=False)
-# Task(id=2, title='Написать код', completed=False)
+
+
+ # Одна запись по первичному ключу
+ task = session.get(Task, 1)
+ print(task)
+
+
+ # С фильтром: только невыполненные
+ pending = session.execute(select(Task).where(Task.completed == False)).scalars().all()
+ for task in pending:
+ print(task)
+
```
-`.scalars()` нужен потому что `select(Task)` возвращает строки-кортежи (даже если в каждом кортеже один элемент). `.scalars()` распаковывает их в объекты `Task`.
+`.scalars()` нужен, потому что `select(Task)` возвращает строки-кортежи: он распаковывает их в объекты `Task`.
-Получить одну запись по первичному ключу проще через `session.get`:
+Одну запись по первичному ключу быстрее всего достаёт `session.get`.
-```python-executable
-from sqlalchemy.orm import Session
+Фильтр пишется как в Core, только вместо `tasks_table.c.completed` — атрибут класса `Task.completed`: третья задача выполнена, и в выборку она не попала.
-with Session(engine) as session:
- task = session.get(Task, 1)
- print(task)
-# Вывод: Task(id=1, title='Изучить ORM', completed=False)
-```
+### UPDATE и DELETE
-С фильтром:
+Самая удобная часть ORM: меняем атрибут объекта, и Session сам понимает, что нужно обновить. Удаление — через `session.delete`:
-```python-executable
-from sqlalchemy import select
-from sqlalchemy.orm import Session
+```python
+from sqlalchemy import create_engine, select
+from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session
-with Session(engine) as session:
- stmt = select(Task).where(Task.completed == False)
- pending = session.execute(stmt).scalars().all()
- for task in pending:
- print(task)
-# Вывод:
-# Task(id=1, title='Изучить ORM', completed=False)
-# Task(id=2, title='Написать код', completed=False)
-```
+# Подготовка: модель, таблица и две задачи
+class Base(DeclarativeBase):
+ pass
+
+class Task(Base):
+ __tablename__ = 'tasks'
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ title: Mapped[str]
+ completed: Mapped[bool] = mapped_column(default=False)
-### UPDATE: изменение
+ def __repr__(self):
+ return f"Task(id={self.id}, title={self.title!r}, completed={self.completed})"
-Самая удобная часть ORM: меняем атрибут объекта, и Session сам понимает что нужно обновить:
+engine = create_engine('sqlite:///:memory:')
+Base.metadata.create_all(engine)
-```python-executable
-from sqlalchemy.orm import Session
+with Session(engine) as session:
+ session.add_all([Task(title="Изучить ORM"), Task(title="Написать код")])
+ session.commit()
with Session(engine) as session:
task = session.get(Task, 1)
task.completed = True
session.commit()
- print(task)
-# Вывод: Task(id=1, title='Изучить ORM', completed=True)
+
+ task2 = session.get(Task, 2)
+ session.delete(task2)
+ session.commit()
+
+ tasks = session.execute(select(Task)).scalars().all()
+ for task in tasks:
+ print(task)
+
```
-Никаких явных `UPDATE ... SET ... WHERE ...`. Session отслеживает изменённые атрибуты и при `commit()` отправляет нужный SQL.
+Никаких явных `UPDATE ... SET` и `DELETE ... WHERE`: Session отслеживает изменённые атрибуты и удалённые объекты и при `commit()` отправляет нужный SQL. Осталась одна задача, и она уже выполнена.
-### DELETE: удаление
+## Связи между таблицами
-```python-executable
-from sqlalchemy.orm import Session
+В реальных схемах таблицы связаны: у пользователя есть задачи, у поста комментарии.
-with Session(engine) as session:
- task = session.get(Task, 2)
- session.delete(task)
- session.commit()
- print("Задача удалена")
-# Вывод: Задача удалена
-```
+Связь держится на **внешнем ключе**: в таблице `user_tasks` есть столбец `user_id`, куда кладётся `id` пользователя-владельца, — так строка задачи знает, чья она. Запись `ForeignKey("users.id")` говорит базе, что значение в этом столбце обязано существовать в `users.id`, иначе задача окажется ничьей.
-## Связи между таблицами
+`relationship` — надстройка ORM над этим столбцом: вместо ручного поиска всех строк с нужным `user_id` вы пишете `user.tasks`.
-В реальных схемах таблицы связаны: у пользователя есть задачи, у поста комментарии. ORM описывает связи через `relationship`, и обращение к связанным записям выглядит как обращение к обычному атрибуту:
+`back_populates` связывает две стороны, чтобы `user.tasks` и `task.user` описывали одну и ту же связь, а не две независимые. В коде обращение к связанным записям выглядит как обращение к обычному атрибуту:
-```python-executable
-from sqlalchemy import ForeignKey
+```python
+from sqlalchemy import create_engine, ForeignKey
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session
-from sqlalchemy import create_engine
-from typing import List
class Base(DeclarativeBase):
pass
@@ -176,7 +245,7 @@ class User(Base):
__tablename__ = 'users'
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
- tasks: Mapped[List["UserTask"]] = relationship(back_populates="user")
+ tasks: Mapped[list["UserTask"]] = relationship(back_populates="user")
class UserTask(Base):
__tablename__ = 'user_tasks'
@@ -185,7 +254,7 @@ class UserTask(Base):
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
user: Mapped["User"] = relationship(back_populates="tasks")
-engine = create_engine('sqlite:///orm_users.db')
+engine = create_engine('sqlite:///:memory:')
Base.metadata.create_all(engine)
with Session(engine) as session:
@@ -200,32 +269,44 @@ with Session(engine) as session:
print(user.name)
for task in user.tasks:
print(f" {task.title}")
-# Вывод:
-# Анна
-# Изучить Python
-# Написать код
+
```
-`user.tasks` за кулисами выполняет SQL-запрос `SELECT ... FROM user_tasks WHERE user_id = ?`, но в коде это выглядит как обычный доступ к атрибуту. Это и есть главный комфорт ORM: реляционная связь читается как «у пользователя есть задачи».
+Имя `"UserTask"` в аннотации стоит в кавычках, потому что этот класс объявлен ниже по файлу: в момент чтения `User` Python его ещё не знает.
-## Сравнение трёх подходов
+`user.tasks` за кулисами выполняет SQL-запрос: выбирает из `user_tasks` строки с нужным `user_id`. Но в коде это выглядит как обычный доступ к атрибуту. Это и есть главный комфорт ORM: реляционная связь читается как «у пользователя есть задачи».
-| Аспект | sqlite3 | SQLAlchemy Core | SQLAlchemy ORM |
-| --------------------- | ---------------------- | ---------------------- | -------------------- |
-| Запрос | SQL-строка | Python-выражение | Python-объект |
-| Защита от инъекций | через `?` вручную | автоматически | автоматически |
-| Переносимость между БД | нет | есть | есть |
-| Связи | JOIN вручную | JOIN-выражения | `user.tasks` |
-| UPDATE | `UPDATE ... SET ...` | `update().values(...)` | `obj.field = ...` |
-| Контроль над SQL | максимальный | высокий | средний |
+## Сравнение трёх подходов
-Хорошее правило: ORM для типичной бизнес-логики, Core для сложных запросов где нужен контроль, raw SQL только когда первые два не справляются.
+| Аспект | sqlite3 | SQLAlchemy Core | SQLAlchemy ORM |
+| ---------------------- | -------------------- | ---------------------- | ----------------- |
+| Запрос | SQL-строка | Python-выражение | Python-объект |
+| Защита от инъекций | через `?` вручную | автоматически | автоматически |
+| Переносимость между БД | нет | есть | есть |
+| Связи | JOIN вручную | JOIN-выражения | `user.tasks` |
+| UPDATE | `UPDATE ... SET ...` | `update().values(...)` | `obj.field = ...` |
+| Контроль над SQL | максимальный | высокий | средний |
-## Что дальше?
+Хорошее правило:
-ORM это инструмент, который оптимизирует **типичные** случаи работы с БД. Если в проекте 95% запросов это «получи объект, поменяй поле, сохрани», ORM экономит кучу времени. Когда упираетесь в сложный запрос или performance-критичный путь, спускайтесь в Core или пишите SQL напрямую. Эти три уровня дополняют друг друга.
+- ORM — для типичной бизнес-логики;
+- Core — для сложных запросов, где нужен контроль;
+- сырой SQL — только когда первые два не справляются.
----
+## Проверка понимания
**Главное преимущество ORM перед Core?**
+1. Запросы выполняются быстрее — Наоборот, ORM добавляет небольшие накладные расходы по сравнению с Core (отслеживание объектов). Преимущество в удобстве, а не в скорости.
+
+2. **Правильный ответ:** Работа с обычными Python-объектами вместо SQL-выражений, автоматическое отслеживание изменений — Изменили атрибут объекта — Session сам сделает нужный UPDATE при commit(). Связи между таблицами читаются как атрибуты (user.tasks), а не как явные JOIN.
+
+3. Возможность работать без понимания SQL — Понимание SQL всё равно нужно: ORM выполняет SQL под капотом, и без понимания, что он делает, легко получить лишние медленные запросы. Преимущество ORM — удобство, а не «магия избавления от SQL».
+
+4. ORM проверяет схему БД во время выполнения — Это скорее особенность типизированных моделей (Mapped\[str]), но проверка происходит не во время выполнения, а в IDE или линтере. Главное преимущество ORM в работе с объектами вместо SQL.
+
+ORM — инструмент, который оптимизирует **типичные** случаи работы с БД. Если в проекте 95% запросов — «получи объект, поменяй поле, сохрани», ORM экономит кучу времени. Когда упираетесь в сложный запрос или узкое место по скорости, спускайтесь в Core или пишите SQL напрямую. Эти три уровня дополняют друг друга.
diff --git a/course/module-3/sqlite-python-basics/article.md b/course/module-3/sqlite-python-basics/article.md
index 5e60ec3..5cd11fd 100644
--- a/course/module-3/sqlite-python-basics/article.md
+++ b/course/module-3/sqlite-python-basics/article.md
@@ -1,16 +1,24 @@
-# SQLite в Python: основы работы с реляционными базами данных
+---
+meta:
+ title: "SQLite: первая база данных в Python"
+ description: "Подключение к SQLite из Python, создание таблицы, четыре операции CRUD и защита от SQL-инъекций через параметры."
+---
-Большинство production-приложений хранят данные не в файлах, а в **реляционных базах данных**: PostgreSQL, MySQL, SQLite. Это значит — рано или поздно вы будете писать SQL-запросы из Python и читать результаты обратно в код.
+# SQLite: первая база данных в Python
-В этой статье разберём базовый паттерн на примере **SQLite**. Это полноценная реляционная БД, встроенная в Python из коробки: ничего устанавливать не нужно, БД хранится в одном файле, и тот же подход работает с любой другой СУБД.
+Сообщения в вашем телефоне, история браузера, настройки половины приложений на ноутбуке — огромная их часть лежит в **SQLite**: полноценной реляционной БД без сервера, встроенной прямо в Python. Ставить ничего не нужно, вся база — один файл, а тот же подход потом работает с любой другой СУБД.
-Эта статья даст **общее представление** о работе с SQL из Python. Для глубокого изучения самого SQL рекомендуем [бесплатный курс SQL Academy](https://sql-academy.org/ru/guide) с практическими заданиями.
+На SQLite и разберём базовый приём работы с реляционной БД: как из Python отправить SQL-запрос и прочитать строки обратно в код.
## Подключение и cursor
-Для работы с SQLite в стандартной библиотеке есть модуль `sqlite3`. Базовый паттерн: создаём соединение, получаем `cursor` (через него выполняем запросы), закрываем соединение.
+Для работы с SQLite в стандартной библиотеке есть модуль `sqlite3`. Базовый паттерн — три шага:
-```python-executable
+- открыть **соединение** — открытую базу, как файл после `open()`;
+- получить **курсор** — он отправляет в базу запросы и держит результат последнего;
+- в конце закрыть соединение.
+
+```python
import sqlite3
# Подключаемся к БД (файл создастся автоматически)
@@ -21,61 +29,60 @@ cursor = connection.cursor()
connection.close()
print("Готово")
-# Вывод: Готово
+
```
-Удобнее использовать соединение как контекстный менеджер (`with`) — он сам закроет соединение и сохранит изменения:
-
-```python-executable
-import sqlite3
-
-with sqlite3.connect('tasks.db') as connection:
- cursor = connection.cursor()
- # ... запросы
-
-print("Готово")
-# Вывод: Готово
-```
+`Python-код` → SQL-запрос → `tasks.db` → строки-кортежи → `Python-код`
## Создание таблицы
-В реляционной БД данные лежат в **таблицах**. Каждая таблица описывается схемой: какие столбцы, какого типа, какие ограничения. Создаём через SQL-команду `CREATE TABLE`:
+В реляционной БД данные лежат в **таблицах**. Каждая таблица описывается схемой: какие столбцы, какого типа, какие ограничения. Создаём через SQL-команду `CREATE TABLE`.
+
+Дальше во всех примерах соединение обёрнуто в `with`: SQLite записывает изменения не мгновенно, их нужно подтверждать (по-английски commit), и `with` делает это сам при выходе из блока, а при ошибке откатывает. Соединение он, правда, не закрывает, но в коротких скриптах, как ниже, оно закроется вместе с программой; в долгоживущем коде зовите `close()`, как в первом примере.
-```python-executable
+```python
import sqlite3
with sqlite3.connect('tasks.db') as connection:
cursor = connection.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS tasks (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
+ id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
completed BOOLEAN DEFAULT FALSE
)
''')
print("Таблица tasks готова")
-# Вывод: Таблица tasks готова
+
```
Что значат части SQL:
-- `CREATE TABLE IF NOT EXISTS tasks` — создать таблицу `tasks`, если ещё нет
-- `id INTEGER PRIMARY KEY AUTOINCREMENT` — целочисленный первичный ключ с автоинкрементом
-- `title TEXT NOT NULL` — текстовое поле, обязательное
-- `completed BOOLEAN DEFAULT FALSE` — логическое поле, по умолчанию `False`
+- `CREATE TABLE IF NOT EXISTS tasks` — создать таблицу `tasks`, если её ещё нет
+- `id INTEGER PRIMARY KEY` — целочисленный первичный ключ; SQLite нумерует новые строки сам
+- `title TEXT NOT NULL` — текстовое поле, обязательное
+- `completed BOOLEAN DEFAULT FALSE` — логическое поле, по умолчанию `False`
## CRUD: четыре базовые операции
-CRUD это акроним от **C**reate / **R**ead / **U**pdate / **D**elete. Это четыре операции, которые покрывают почти всю работу с данными. Дальше предполагаем, что таблица `tasks` уже создана.
+CRUD — акроним от **C**reate / **R**ead / **U**pdate / **D**elete: четыре операции, которые покрывают почти всю работу с данными.
+
+Тренироваться будем на базе в памяти: `:memory:` вместо имени файла даёт чистую базу при каждом запуске. Первые строки внутри `with` — подготовка: таблица и две задачи.
### CREATE: добавление данных
-```python-executable
+```python
import sqlite3
-with sqlite3.connect('tasks.db') as connection:
+with sqlite3.connect(':memory:') as connection:
cursor = connection.cursor()
+ cursor.execute("CREATE TABLE tasks (id INTEGER PRIMARY KEY, title TEXT)")
+
cursor.execute(
"INSERT INTO tasks (title) VALUES (?)",
("Изучить SQLite",)
@@ -86,105 +93,121 @@ with sqlite3.connect('tasks.db') as connection:
)
print("Задачи добавлены")
-# Вывод: Задачи добавлены
-```
-
-Здесь принципиально важно: значения **не вставляются прямо в строку SQL** через f-string или конкатенацию. Вместо этого используется параметр `?`, а значение передаётся вторым аргументом в `execute`. Это защищает от **SQL-инъекций**:
-
-```python
-# ОПАСНО: пользовательский ввод склеивается с SQL
-user_input = "'; DROP TABLE tasks; --"
-cursor.execute(f"INSERT INTO tasks (title) VALUES ('{user_input}')")
-# выполнится: INSERT ... VALUES (''); DROP TABLE tasks; --')
-
-# БЕЗОПАСНО: значение передаётся отдельно
-cursor.execute("INSERT INTO tasks (title) VALUES (?)", (user_input,))
-# выполнится: INSERT ... VALUES ('\'; DROP TABLE tasks; --')
+
```
-Правило: **никогда не склеивайте пользовательский ввод в SQL-строку**, всегда используйте параметры через `?`.
+Значения передаются кортежем вторым аргументом `execute`, а в самом SQL вместо них стоит параметр `?`. Запятая в `("Изучить SQLite",)` обязательна: именно она делает скобки кортежем из одного элемента. Почему значения не вклеивают прямо в строку запроса — покажем сразу после того, как научимся читать данные.
### READ: чтение данных
-```python-executable
+```python
import sqlite3
-with sqlite3.connect('tasks.db') as connection:
+with sqlite3.connect(':memory:') as connection:
cursor = connection.cursor()
- cursor.execute("SELECT id, title, completed FROM tasks")
+ cursor.execute("CREATE TABLE tasks (id INTEGER PRIMARY KEY, title TEXT)")
+ cursor.execute("INSERT INTO tasks (title) VALUES ('Изучить SQLite'), ('Сделать покупки')")
+
+ cursor.execute("SELECT id, title FROM tasks")
rows = cursor.fetchall()
for row in rows:
print(row)
-# Вывод:
-# (1, 'Изучить SQLite', 0)
-# (2, 'Сделать покупки', 0)
+
```
-`cursor.fetchall()` возвращает все строки результата как список кортежей. Доступ к полям по индексу: `row[0]` это `id`, `row[1]` это `title` и т.д.
+`cursor.fetchall()` возвращает все строки результата как список кортежей: доступ к полям по индексу, `row[0]` — это `id`, `row[1]` — `title`.
-Если нужно достать только одну строку (например по id), используется `fetchone()`:
+Когда нужна только одна строка, вместо `fetchall()` используют `fetchone()`: он возвращает первую строку результата. Увидим его в деле чуть ниже.
-```python-executable
-import sqlite3
+### SQL-инъекции: почему `?`
-with sqlite3.connect('tasks.db') as connection:
- cursor = connection.cursor()
- cursor.execute("SELECT title FROM tasks WHERE id = ?", (1,))
- row = cursor.fetchone()
+Теперь можно показать, зачем параметр `?` нужен на самом деле. Представьте поле поиска, куда пользователь вводит название задачи, — а злоумышленник вводит кусок SQL:
-print(row)
-# Вывод: ('Изучить SQLite',)
+```python
+# ОПАСНО: пользовательский ввод склеивается с SQL
+search = "' OR '1'='1"
+cursor.execute(f"SELECT * FROM tasks WHERE title = '{search}'")
+# SQL превращается в: SELECT * FROM tasks WHERE title = '' OR '1'='1'
+# условие '1'='1' истинно всегда → вернутся ВСЕ задачи, а не только нужная
+
+# БЕЗОПАСНО: значение передаётся отдельно
+cursor.execute("SELECT * FROM tasks WHERE title = ?", (search,))
+# ищется задача с буквальным названием "' OR '1'='1" — лишнего не вернётся
```
+Звёздочка в `SELECT *` — «все колонки разом». Злоумышленник подставил кусок SQL в обычное поле поиска и получил все строки таблицы; тем же приёмом обходят проверку пароля или удаляют данные. Правило: **никогда не склеивайте пользовательский ввод в SQL-строку**, всегда используйте параметры через `?`.
+
### UPDATE: обновление данных
-```python-executable
+```python
import sqlite3
-with sqlite3.connect('tasks.db') as connection:
+with sqlite3.connect(':memory:') as connection:
cursor = connection.cursor()
+ cursor.execute("CREATE TABLE tasks (id INTEGER PRIMARY KEY, title TEXT, completed BOOLEAN DEFAULT FALSE)")
+ cursor.execute("INSERT INTO tasks (title) VALUES ('Изучить SQLite'), ('Сделать покупки')")
+
cursor.execute(
"UPDATE tasks SET completed = ? WHERE id = ?",
(True, 1)
)
-print("Задача 1 помечена выполненной")
-# Вывод: Задача 1 помечена выполненной
+ cursor.execute("SELECT id, title, completed FROM tasks WHERE id = ?", (1,))
+ print(cursor.fetchone())
+
```
-`WHERE id = ?` обязательно: без условия `UPDATE` обновит **все** строки в таблице.
+Первая задача теперь выполнена: логические значения SQLite хранит как `0` и `1`, и `completed` сменился с нуля на единицу. А `WHERE id = ?` обязательно: без условия `UPDATE` обновит **все** строки таблицы.
### DELETE: удаление данных
-```python-executable
+```python
import sqlite3
-with sqlite3.connect('tasks.db') as connection:
+with sqlite3.connect(':memory:') as connection:
cursor = connection.cursor()
+ cursor.execute("CREATE TABLE tasks (id INTEGER PRIMARY KEY, title TEXT)")
+ cursor.execute("INSERT INTO tasks (title) VALUES ('Изучить SQLite'), ('Сделать покупки')")
+
cursor.execute("DELETE FROM tasks WHERE id = ?", (2,))
-print("Задача 2 удалена")
-# Вывод: Задача 2 удалена
+ cursor.execute("SELECT id, title FROM tasks")
+ print(cursor.fetchall())
+
```
-Та же история: без `WHERE` `DELETE` удалит **всю** таблицу строк.
+Вторая задача исчезла — осталась одна строка. Та же история, что с `UPDATE`: без `WHERE` команда `DELETE` удалит **все** строки таблицы.
## Что осталось за кадром
В реальном production-коде есть несколько важных тем, которые мы здесь не разбираем подробно, но о которых стоит знать:
-- **Транзакции** (`BEGIN`/`COMMIT`/`ROLLBACK`): группа изменений выполняется атомарно — либо все, либо ни одной. `with sqlite3.connect(...)` коммитит автоматически при выходе из блока.
-- **Связи между таблицами** (`FOREIGN KEY`, `JOIN`): большинство реальных схем содержат несколько связанных таблиц (пользователи и их задачи, заказы и товары).
-- **Индексы**: ускоряют поиск по часто используемым колонкам.
+- **Транзакции** (`BEGIN`/`COMMIT`/`ROLLBACK`): группа изменений выполняется атомарно — либо все, либо ни одной. `with sqlite3.connect(...)` коммитит автоматически при выходе из блока.
+- **JOIN и выборки по нескольким таблицам сразу**: большинство реальных схем содержат несколько связанных таблиц (пользователи и их задачи, заказы и товары), и данные из них достают одним запросом.
+- **Индексы**: ускоряют поиск по часто используемым колонкам.
Эти темы покрывает [курс SQL Academy](https://sql-academy.org/ru/guide).
-## Что дальше?
+## Проверка понимания
-В следующей статье возьмём **SQLAlchemy Core**: это библиотека, которая позволяет строить SQL-запросы из Python-выражений вместо строк. SQL-инъекции там защищены автоматически, а один и тот же код работает с PostgreSQL, MySQL и SQLite.
+**Почему нужно передавать значения в `execute()` через параметр `?`, а не вставлять напрямую в SQL-строку?**
----
+1. **Правильный ответ:** Для защиты от SQL-инъекций — Текст запроса и значения едут в базу по разным каналам: запрос разбирается первым, а плейсхолдер — это дырка под значение. Что бы в неё ни положили, как SQL это уже не прочитается, поэтому враждебный ввод остаётся просто строкой в поле, а не командой.
-**Почему нужно передавать значения в `execute()` через параметр `?`, а не вставлять напрямую в SQL-строку?**
+2. Для ускорения работы запроса — Влияние на скорость есть (БД может закешировать план запроса), но это побочный эффект. Главная причина это безопасность.
+3. Чтобы SQLite мог автоматически преобразовать типы — Преобразование типов выполняется в обоих случаях. Главная причина параметризованных запросов это защита от SQL-инъекций.
+
+4. Это требование стандарта SQL — Плейсхолдер «?» — это особенность драйвера sqlite3, а не SQL-стандарта. В других СУБД плейсхолдеры другие (%s, :name). Главная причина их использования — безопасность.
+
+В следующей статье возьмём **SQLAlchemy Core**: это библиотека, которая позволяет строить SQL-запросы из Python-выражений вместо строк. SQL-инъекции там защищены автоматически, а один и тот же код работает с PostgreSQL, MySQL и SQLite.
diff --git a/course/module-3/threading-and-multiprocessing/article.md b/course/module-3/threading-and-multiprocessing/article.md
index 61a9e14..431e72c 100644
--- a/course/module-3/threading-and-multiprocessing/article.md
+++ b/course/module-3/threading-and-multiprocessing/article.md
@@ -1,6 +1,16 @@
+---
+meta:
+ title: "Потоки и процессы в Python: threading и multiprocessing"
+ description: "Модуль threading для I/O-bound задач и multiprocessing для CPU-bound. Создание, синхронизация через Lock и Queue, выбор подхода."
+---
+
# Потоки и процессы в Python
-Из вводной статьи: для I/O-bound задач нужны **потоки** (`threading`), для CPU-bound — **процессы** (`multiprocessing`). Здесь разберём оба модуля. API у них почти одинаковый: выучили один — считайте, что выучили оба.
+Восемь тяжёлых вычислений через `ThreadPoolExecutor` считаются секунд двенадцать. Меняете в нём одно слово, `Thread` на `Process`, и те же восемь укладываются в три-четыре. В этом зазоре и живёт вся разница между потоками и процессами.
+
+Расклад вы помните из вводной статьи: **потоки** (`threading`) для I/O, **процессы** (`multiprocessing`) для вычислений. Здесь разберём оба модуля. API у них почти одинаковый: выучили один — считайте, что выучили оба.
+
+, процессы изолированы и могут идти на разных ядрах CPU")
## threading: потоки внутри одного процесса
@@ -25,14 +35,14 @@ t2.join()
print("Все потоки завершены")
```
-- `target` — функция, которую выполняет поток
-- `args` — кортеж аргументов
-- `start()` — запускает поток
-- `join()` — блокирует основной поток до завершения этого
+- `target` — функция, которую выполняет поток
+- `args` — кортеж аргументов
+- `start()` — запускает поток
+- `join()` — блокирует основной поток до завершения этого
Если запустить, увидим примерно такой вывод:
-```
+```text
Поток A: засыпаю на 2 с
Поток B: засыпаю на 1 с
Поток B: завершён
@@ -44,7 +54,28 @@ print("Все потоки завершены")
## Защита общих данных: Lock
-Потоки делят память. Если два потока меняют одну переменную, **результат непредсказуем** (race condition). Защита — `Lock`:
+Потоки делят память. Если два потока меняют одну переменную без защиты, **результат непредсказуем** (race condition). Посмотрим: пять потоков увеличивают счётчик по миллиону раз каждый, значит должно получиться 5 000 000.
+
+```python
+import threading
+
+counter = 0
+
+def increment():
+ global counter
+ for _ in range(1_000_000):
+ counter += 1
+
+threads = [threading.Thread(target=increment) for _ in range(5)]
+for t in threads:
+ t.start()
+for t in threads:
+ t.join()
+
+print(counter) # например 3137095 — и при каждом запуске другое число
+```
+
+Ждали 5 000 000, а получили меньше — и в следующий раз число будет другим. Дело в том, что `counter += 1` не одно действие, а три (прочитать значение, прибавить единицу, записать обратно), и потоки успевают влезть между шагами, затирая чужие инкременты. Защита — `Lock`: пока один поток внутри `with lock`, остальные ждут своей очереди.
```python
import threading
@@ -54,7 +85,7 @@ lock = threading.Lock()
def increment():
global counter
- for _ in range(100_000):
+ for _ in range(1_000_000):
with lock: # автоматический acquire/release
counter += 1
@@ -64,12 +95,10 @@ for t in threads:
for t in threads:
t.join()
-print(counter) # 500000 — корректно
+print(counter) # 5000000 — теперь всегда верно
```
-Без `lock` итог будет случайным числом меньше 500_000: потоки «перетирают» инкременты друг друга. Контекстный менеджер `with lock:` это стандартный способ работы, он гарантирует освобождение даже при исключении.
-
-Кроме `Lock` в `threading` есть `Event`, `Semaphore`, `Condition`, `RLock`. На практике в 90% случаев хватает `Lock` и `Queue` (см. ниже). Остальное нужно для нетривиальной координации.
+`with lock:` — стандартный способ работы: он гарантирует освобождение блокировки даже при исключении. Кроме `Lock` в `threading` есть `Event`, `Semaphore`, `Condition`, `RLock`. На практике в 90% случаев хватает `Lock` и `Queue` (см. ниже). Остальное нужно для нетривиальной координации.
## Обмен данными между потоками: queue.Queue
@@ -193,33 +222,40 @@ from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
def task(x):
return x * x
-# Для I/O-bound: потоки
-with ThreadPoolExecutor(max_workers=4) as executor:
- results = list(executor.map(task, range(10)))
+# if __name__ нужен из-за ProcessPoolExecutor: он запускает процессы
+if __name__ == "__main__":
+ # Для I/O-bound: потоки
+ with ThreadPoolExecutor(max_workers=4) as executor:
+ results = list(executor.map(task, range(10)))
-# Для CPU-bound: процессы
-with ProcessPoolExecutor(max_workers=4) as executor:
- results = list(executor.map(task, range(10)))
+ # Для CPU-bound: процессы — меняется только класс executor
+ with ProcessPoolExecutor(max_workers=4) as executor:
+ results = list(executor.map(task, range(10)))
```
Это самый практичный способ распараллелить простые задачи. На современных проектах `concurrent.futures` встречается чаще, чем прямые `threading.Thread` или `multiprocessing.Process`.
## Когда что брать
-| Задача | Инструмент |
-| --- | --- |
-| Простые I/O-bound в существующем синхронном коде | `ThreadPoolExecutor` |
-| Когда нужно вручную управлять состоянием (Lock, Queue) | `threading` напрямую |
-| CPU-bound вычисления | `ProcessPoolExecutor` или `multiprocessing.Pool` |
-| Тысячи сетевых соединений | **asyncio** (следующие статьи) |
+| Задача | Инструмент |
+| ------------------------------------------------------ | ------------------------------------------------ |
+| Простые I/O-bound в существующем синхронном коде | `ThreadPoolExecutor` |
+| Когда нужно вручную управлять состоянием (Lock, Queue) | `threading` напрямую |
+| CPU-bound вычисления | `ProcessPoolExecutor` или `multiprocessing.Pool` |
+| Тысячи сетевых соединений | **asyncio** (следующие статьи) |
`asyncio` лучше для I/O-bound на больших объёмах: один поток, минимальные накладные расходы на переключение. Но требует переписать код в `async`-стиле.
-## Что дальше?
+## Проверка понимания
-В следующих двух статьях — **asyncio**: третий и самый эффективный для I/O способ организовать конкурентное выполнение. Особенно для веб-серверов, ботов и работы с API.
+**Почему `multiprocessing` подходит для CPU-bound задач, а `threading` нет?**
----
+1. Процессы быстрее запускаются, чем потоки — Наоборот, создание процесса дороже, чем создание потока. Преимущество multiprocessing в другом.
-**Почему `multiprocessing` подходит для CPU-bound задач, а `threading` нет?**
+2. **Правильный ответ:** У каждого процесса свой Python-интерпретатор и свой GIL, поэтому процессы реально работают параллельно на разных ядрах CPU — Верно. GIL разрешает выполнять байткод Python только одному потоку в рамках процесса. Несколько процессов запускают несколько интерпретаторов, каждый со своим GIL и своим ядром: это и есть истинный параллелизм.
+
+3. multiprocessing использует асинхронность, а threading нет — Ни один из них не использует асинхронность. Это совершенно отдельный подход (asyncio).
+4. threading нельзя использовать на многоядерных процессорах — threading работает на любом процессоре. Ограничение в том, что из-за GIL потоки не выполняют Python-код одновременно, но для I/O-bound задач это не мешает, так как GIL отпускается во время ожидания.
+
+В следующих двух статьях — **asyncio**: третий и самый эффективный для I/O способ организовать конкурентное выполнение. Особенно для веб-серверов, ботов и работы с API.
diff --git a/course/module-3/unittest-module/article.md b/course/module-3/unittest-module/article.md
index e9ee4f8..05fe2f6 100644
--- a/course/module-3/unittest-module/article.md
+++ b/course/module-3/unittest-module/article.md
@@ -1,10 +1,16 @@
+---
+meta:
+ title: "Модуль unittest: классический тестовый фреймворк"
+ description: "unittest — встроенный в Python xUnit-фреймворк: TestCase, методы assertEqual/assertTrue, setUp/tearDown. Что вы встретите в legacy-коде."
+---
+
# Модуль unittest: классический фреймворк
-В стандартной библиотеке Python есть свой фреймворк для тестов — `unittest`. Стиль классический ООП: тесты живут в классах, наследуются от `TestCase`, проверки делаются через специальные методы `assert*` (тем, кто знаком с JUnit в Java, всё это будет привычно). Большинство новых проектов выбирают `pytest`, но `unittest` всё ещё встречается:
+Открываете проект постарше, а тесты там не похожи на те, что вы только что писали: классы, наследование от `TestCase`, проверки через `self.assertEqual` вместо простого `assert`. Это `unittest` — тестовый фреймворк из стандартной библиотеки Python, классический ООП-стиль (тем, кто видел JUnit в Java, всё будет знакомо). Большинство новых проектов берут `pytest`, но `unittest` никуда не делся и встречается:
-- В legacy-коде (фреймворк существует с Python 2.1).
-- В проектах со строгим запретом на внешние зависимости.
-- В тестах самой стандартной библиотеки.
+- В legacy-коде (фреймворк существует с Python 2.1).
+- В проектах со строгим запретом на внешние зависимости.
+- В тестах самой стандартной библиотеки.
Знать его базу полезно — рано или поздно вы откроете чужой репозиторий с `unittest` и нужно будет разобраться.
@@ -29,10 +35,10 @@ if __name__ == "__main__":
Что отличается от `pytest`:
-- Тесты живут в **классе**, который наследуется от `unittest.TestCase`.
-- Имена методов начинаются с `test_`, как и в pytest.
-- Вместо обычного `assert` используются **специальные методы**: `self.assertEqual(a, b)` вместо `assert a == b`.
-- `unittest.main()` в конце файла — точка запуска при `python test_file.py`.
+- Тесты живут в **классе**, который наследуется от `unittest.TestCase`.
+- Имена методов начинаются с `test_`, как и в pytest.
+- Вместо обычного `assert` используются **специальные методы**: `self.assertEqual(a, b)` вместо `assert a == b`.
+- `unittest.main()` в конце файла — точка запуска при `python test_file.py`.
Запуск через стандартный `python` или через `python -m unittest`:
@@ -46,14 +52,14 @@ python -m unittest test_addition.py
`TestCase` предоставляет много специальных проверок. На практике хватает 6 самых частых:
-| Метод | Проверяет |
-| ---------------------------- | -------------------------- |
-| `assertEqual(a, b)` | `a == b` |
-| `assertNotEqual(a, b)` | `a != b` |
-| `assertTrue(x)` | `bool(x) is True` |
-| `assertFalse(x)` | `bool(x) is False` |
-| `assertIn(item, container)` | `item in container` |
-| `assertRaises(Exception)` | блок выбрасывает исключение |
+| Метод | Проверяет |
+| --------------------------- | --------------------------- |
+| `assertEqual(a, b)` | `a == b` |
+| `assertNotEqual(a, b)` | `a != b` |
+| `assertTrue(x)` | `bool(x) is True` |
+| `assertFalse(x)` | `bool(x) is False` |
+| `assertIn(item, container)` | `item in container` |
+| `assertRaises(Exception)` | блок выбрасывает исключение |
Пример `assertRaises` через контекстный менеджер:
@@ -95,17 +101,19 @@ class TestUserStorage(unittest.TestCase):
self.assertEqual(self.users["id"], 1)
```
+
+
Для каждого теста цикл `setUp → test_X → tearDown` запускается заново — это гарантирует, что тесты независимы друг от друга. Есть ещё `setUpClass`/`tearDownClass` для подготовки **один раз на класс**, но это уже частные случаи.
## unittest vs pytest
-| Что | unittest | pytest |
-| ------------------ | ------------------------------ | --------------------------------- |
-| Тесты | методы класса `TestCase` | обычные функции |
-| Проверки | `self.assertEqual()` и т.п. | стандартный `assert` |
-| Подготовка | `setUp` / `tearDown` | фикстуры с DI и scope |
-| Параметризация | вручную или через расширения | `@pytest.mark.parametrize` |
-| Зависимости | встроен | `pip install pytest` |
+| Что | unittest | pytest |
+| -------------- | ---------------------------- | -------------------------- |
+| Тесты | методы класса `TestCase` | обычные функции |
+| Проверки | `self.assertEqual()` и т.п. | стандартный `assert` |
+| Подготовка | `setUp` / `tearDown` | фикстуры с DI и scope |
+| Параметризация | вручную или через расширения | `@pytest.mark.parametrize` |
+| Зависимости | встроен | `pip install pytest` |
`pytest` лаконичнее, гибче и в новых проектах используется по умолчанию. Но `unittest` встроен и не требует ничего ставить — для скриптов и стандартной библиотеки это плюс.
@@ -124,11 +132,16 @@ class TestUserAPI(unittest.TestCase):
# ... тестируем код, использующий requests.get
```
-## Что дальше?
+## Проверка понимания
-В следующей (заключительной для модуля тестирования) статье — как измерять **покрытие** тестами через `pytest-cov` и автоматизировать запуск через **CI** (на примере GitHub Actions).
+**Что верно про модуль `unittest`?**
----
+1. **Правильный ответ:** Тестовые классы в unittest должны наследоваться от unittest.TestCase — Именно TestCase даёт доступ к методам assert\*, setUp/tearDown и интеграции с раннером.
-**Что верно про модуль `unittest`?**
+2. unittest использует стандартный assert Python для проверок — Нет, unittest использует специальные методы (self.assertEqual, self.assertTrue и т.д.). Стандартный assert это стиль pytest.
+
+3. setUp запускается один раз перед всеми тестами в классе — setUp запускается перед КАЖДЫМ тестом. Один раз на класс это setUpClass.
+4. Тестовые методы в unittest могут называться как угодно — Раннер находит только методы, чьё имя начинается с test. Метод с другим именем просто не запустится как тест.
+
+В следующей (заключительной для модуля тестирования) статье — как измерять **покрытие** тестами через `pytest-cov` и автоматизировать запуск через **CI** (на примере GitHub Actions).
diff --git a/interviewQuestions/1/ru.mdx b/interviewQuestions/1/ru.mdx
index 2a3e18b..3d4b30f 100644
--- a/interviewQuestions/1/ru.mdx
+++ b/interviewQuestions/1/ru.mdx
@@ -1,22 +1,22 @@
---
meta:
- title: 'Что такое Python и каковы его основные особенности?'
- description: 'Python — высокоуровневый интерпретируемый язык с динамической типизацией, автоматическим управлением памятью и кроссплатформенностью.'
+ title: "Что такое Python и каковы его основные особенности?"
+ description: "Python — высокоуровневый интерпретируемый язык с динамической типизацией, автоматическим управлением памятью и кроссплатформенностью."
---
> **Python** — это высокоуровневый интерпретируемый язык программирования общего назначения с акцентом на читаемость кода и простоту синтаксиса.
## Основные особенности:
-- **Интерпретируемый**: код выполняется построчно интерпретатором, без необходимости компиляции.
-- **Динамическая типизация**: тип переменной определяется автоматически во время выполнения, а не при объявлении.
-- **Автоматическое управление памятью**: встроенный сборщик мусора освобождает неиспользуемую память.
-- **Кроссплатформенность**: код работает на Windows, macOS и Linux без изменений.
-- **Мультипарадигменность**: поддерживает объектно-ориентированный, функциональный и процедурный стили.
-- **Обширная стандартная библиотека**: модули для работы с файлами, сетью, JSON и многим другим.
+- **Интерпретируемый**: код выполняется построчно интерпретатором, без необходимости компиляции.
+- **Динамическая типизация**: тип переменной определяется автоматически во время выполнения, а не при объявлении.
+- **Автоматическое управление памятью**: встроенный сборщик мусора освобождает неиспользуемую память.
+- **Кроссплатформенность**: код работает на Windows, macOS и Linux без изменений.
+- **Мультипарадигменность**: поддерживает объектно-ориентированный, функциональный и процедурный стили.
+- **Обширная стандартная библиотека**: модули для работы с файлами, сетью, JSON и многим другим.
## Где применяется:
-- **Веб-разработка**: Django, Flask, FastAPI.
-- **Анализ данных и ML**: NumPy, Pandas, scikit-learn.
-- **Автоматизация и скрипты**: обработка файлов, DevOps, тестирование.
+- **Веб-разработка**: Django, Flask, FastAPI.
+- **Анализ данных и ML**: NumPy, Pandas, scikit-learn.
+- **Автоматизация и скрипты**: обработка файлов, DevOps, тестирование.
diff --git a/interviewQuestions/10/ru.mdx b/interviewQuestions/10/ru.mdx
index 4f5f2f6..43466af 100644
--- a/interviewQuestions/10/ru.mdx
+++ b/interviewQuestions/10/ru.mdx
@@ -1,7 +1,7 @@
---
meta:
- title: 'Какие циклы существуют в Python и чем они отличаются?'
- description: 'В Python есть циклы for и while. Узнайте о break, continue, блоке else у циклов и функции range().'
+ title: "Какие циклы существуют в Python и чем они отличаются?"
+ description: "В Python есть циклы for и while. Узнайте о break, continue, блоке else у циклов и функции range()."
---
## Цикл for:
@@ -31,8 +31,8 @@ while count < 3:
## Управляющие операторы:
-- `break` — прерывает цикл.
-- `continue` — переходит к следующей итерации.
+- `break` — прерывает цикл.
+- `continue` — переходит к следующей итерации.
```python
for i in range(10):
diff --git a/interviewQuestions/11/ru.mdx b/interviewQuestions/11/ru.mdx
index 0130354..ea15ba0 100644
--- a/interviewQuestions/11/ru.mdx
+++ b/interviewQuestions/11/ru.mdx
@@ -1,7 +1,7 @@
---
meta:
- title: 'Что такое функция в Python и как её определить?'
- description: 'Функция — это именованный блок кода, определяемый через def. Узнайте о параметрах, возвращаемых значениях и значениях по умолчанию.'
+ title: "Что такое функция в Python и как её определить?"
+ description: "Функция — это именованный блок кода, определяемый через def. Узнайте о параметрах, возвращаемых значениях и значениях по умолчанию."
---
> **Функция** — это именованный блок кода, который можно вызывать многократно. Функции помогают избежать дублирования и делают код более читаемым.
diff --git a/interviewQuestions/12/ru.mdx b/interviewQuestions/12/ru.mdx
index 2126e8a..6e111c7 100644
--- a/interviewQuestions/12/ru.mdx
+++ b/interviewQuestions/12/ru.mdx
@@ -1,10 +1,10 @@
---
meta:
- title: 'Что такое *args и **kwargs?'
- description: '*args принимает произвольное количество позиционных аргументов, **kwargs — именованных. Узнайте, как использовать распаковку аргументов в Python.'
+ title: "Что такое *args и **kwargs?"
+ description: "*args принимает произвольное количество позиционных аргументов, **kwargs — именованных. Узнайте, как использовать распаковку аргументов в Python."
---
-## *args — произвольное число позиционных аргументов:
+## \*args — произвольное число позиционных аргументов:
Собирает все лишние позиционные аргументы в **кортеж**:
@@ -16,7 +16,7 @@ total(1, 2, 3) # 6
total(10, 20) # 30
```
-## **kwargs — произвольное число именованных аргументов:
+## \*\*kwargs — произвольное число именованных аргументов:
Собирает все лишние именованные аргументы в **словарь**:
diff --git a/interviewQuestions/13/ru.mdx b/interviewQuestions/13/ru.mdx
index 3ab11f2..3541bf3 100644
--- a/interviewQuestions/13/ru.mdx
+++ b/interviewQuestions/13/ru.mdx
@@ -1,7 +1,7 @@
---
meta:
- title: 'Что такое анонимная функция?'
- description: 'Lambda — это анонимная однострочная функция. Узнайте синтаксис lambda, её использование с sorted, map, filter и основные ограничения.'
+ title: "Что такое анонимная функция?"
+ description: "Lambda — это анонимная однострочная функция. Узнайте синтаксис lambda, её использование с sorted, map, filter и основные ограничения."
---
> **Lambda** — это анонимная (безымянная) функция, определяемая в одну строку. Она может принимать любое количество аргументов, но содержит только одно выражение.
@@ -46,6 +46,6 @@ evens = list(filter(lambda x: x % 2 == 0, numbers))
## Ограничения:
-- Только **одно выражение** — нельзя использовать многострочную логику, циклы или присваивания.
-- Снижает **читаемость** при сложных выражениях — лучше использовать обычную функцию.
-- Нет **имени** — сложнее отлаживать (в traceback отображается как ``).
+- Только **одно выражение** — нельзя использовать многострочную логику, циклы или присваивания.
+- Снижает **читаемость** при сложных выражениях — лучше использовать обычную функцию.
+- Нет **имени** — сложнее отлаживать (в traceback отображается как ``).
diff --git a/interviewQuestions/14/ru.mdx b/interviewQuestions/14/ru.mdx
index d2c9cb9..93985cd 100644
--- a/interviewQuestions/14/ru.mdx
+++ b/interviewQuestions/14/ru.mdx
@@ -1,17 +1,17 @@
---
meta:
- title: 'Что такое области видимости переменных в Python?'
- description: 'Области видимости в Python определяются правилом LEGB: Local, Enclosing, Global, Built-in. Узнайте о ключевых словах global и nonlocal.'
+ title: "Что такое области видимости переменных в Python?"
+ description: "Области видимости в Python определяются правилом LEGB: Local, Enclosing, Global, Built-in. Узнайте о ключевых словах global и nonlocal."
---
> В Python область видимости определяет, где переменная доступна. Python использует правило **LEGB** для поиска переменных.
## Правило LEGB:
-- **L — Local**: переменные внутри текущей функции.
-- **E — Enclosing**: переменные во внешней (вложенной) функции.
-- **G — Global**: переменные на уровне модуля.
-- **B — Built-in**: встроенные имена Python (`print`, `len`, `range`).
+- **L — Local**: переменные внутри текущей функции.
+- **E — Enclosing**: переменные во внешней (вложенной) функции.
+- **G — Global**: переменные на уровне модуля.
+- **B — Built-in**: встроенные имена Python (`print`, `len`, `range`).
```python
x = "глобальная" # Global
diff --git a/interviewQuestions/15/ru.mdx b/interviewQuestions/15/ru.mdx
index 00b853e..874656f 100644
--- a/interviewQuestions/15/ru.mdx
+++ b/interviewQuestions/15/ru.mdx
@@ -1,7 +1,7 @@
---
meta:
- title: 'Что такое генераторы списков и как их использовать?'
- description: 'Генератор списков — это компактный синтаксис для создания списков в Python. Узнайте о условиях, вложенных генераторах и сравнении с циклами.'
+ title: "Что такое генераторы списков и как их использовать?"
+ description: "Генератор списков — это компактный синтаксис для создания списков в Python. Узнайте о условиях, вложенных генераторах и сравнении с циклами."
---
> **Генератор списков** (list comprehension) — это краткий способ создания нового списка на основе существующей коллекции в одну строку.
diff --git a/interviewQuestions/16/ru.mdx b/interviewQuestions/16/ru.mdx
index 71d5ff7..c478a34 100644
--- a/interviewQuestions/16/ru.mdx
+++ b/interviewQuestions/16/ru.mdx
@@ -1,7 +1,7 @@
---
meta:
- title: 'Что такое генераторы и чем они отличаются от списков?'
- description: 'Генераторы используют yield для ленивых вычислений и экономят память. Узнайте разницу между генераторами и списками в Python.'
+ title: "Что такое генераторы и чем они отличаются от списков?"
+ description: "Генераторы используют yield для ленивых вычислений и экономят память. Узнайте разницу между генераторами и списками в Python."
---
> **Генератор** — это функция, которая возвращает элементы по одному с помощью `yield`, а не создаёт весь список в памяти сразу.
@@ -33,9 +33,9 @@ squares_gen = (x ** 2 for x in range(1000000))
## Ключевые отличия от списка:
-- **Память**: генератор хранит только текущий элемент, а не всю коллекцию.
-- **Одноразовость**: генератор можно пройти только один раз.
-- **Ленивость**: элементы вычисляются по запросу, а не заранее.
+- **Память**: генератор хранит только текущий элемент, а не всю коллекцию.
+- **Одноразовость**: генератор можно пройти только один раз.
+- **Ленивость**: элементы вычисляются по запросу, а не заранее.
```python
gen = (x for x in range(3))
@@ -45,5 +45,5 @@ print(list(gen)) # [] — уже исчерпан
## Когда использовать:
-- **Генератор** — при работе с большими данными, потоками, когда не нужно хранить все элементы.
-- **Список** — когда нужен многократный доступ, индексация или известно, что данных немного.
+- **Генератор** — при работе с большими данными, потоками, когда не нужно хранить все элементы.
+- **Список** — когда нужен многократный доступ, индексация или известно, что данных немного.
diff --git a/interviewQuestions/17/ru.mdx b/interviewQuestions/17/ru.mdx
index 9f65399..ea27054 100644
--- a/interviewQuestions/17/ru.mdx
+++ b/interviewQuestions/17/ru.mdx
@@ -1,7 +1,7 @@
---
meta:
- title: 'Как работают встроенные функции map, filter и zip?'
- description: 'map применяет функцию к каждому элементу, filter отбирает по условию, zip объединяет несколько коллекций. Примеры использования в Python.'
+ title: "Как работают встроенные функции map, filter и zip?"
+ description: "map применяет функцию к каждому элементу, filter отбирает по условию, zip объединяет несколько коллекций. Примеры использования в Python."
---
## map — применяет функцию к каждому элементу:
@@ -42,6 +42,6 @@ user_ages = dict(zip(names, ages))
## Особенности:
-- Все три функции возвращают **итераторы**, а не списки — для получения списка нужно обернуть в `list()`.
-- `zip` останавливается по **самой короткой** коллекции.
-- В большинстве случаев **list comprehension** считается более читаемой альтернативой `map` и `filter`.
+- Все три функции возвращают **итераторы**, а не списки — для получения списка нужно обернуть в `list()`.
+- `zip` останавливается по **самой короткой** коллекции.
+- В большинстве случаев **list comprehension** считается более читаемой альтернативой `map` и `filter`.
diff --git a/interviewQuestions/18/ru.mdx b/interviewQuestions/18/ru.mdx
index 4ebba64..1bbaf88 100644
--- a/interviewQuestions/18/ru.mdx
+++ b/interviewQuestions/18/ru.mdx
@@ -1,7 +1,7 @@
---
meta:
- title: 'Что такое распаковка в Python?'
- description: 'Распаковка позволяет присваивать элементы коллекции нескольким переменным. Узнайте о множественном присваивании, операторах * и ** в Python.'
+ title: "Что такое распаковка в Python?"
+ description: "Распаковка позволяет присваивать элементы коллекции нескольким переменным. Узнайте о множественном присваивании, операторах * и ** в Python."
---
> **Распаковка** — это механизм, позволяющий «разобрать» коллекцию на отдельные переменные.
@@ -25,7 +25,7 @@ a, b = b, a
print(a, b) # 2 1
```
-## Распаковка с * (звёздочкой):
+## Распаковка с \* (звёздочкой):
Собирает «остаток» элементов в список:
diff --git a/interviewQuestions/19/ru.mdx b/interviewQuestions/19/ru.mdx
index 0065a12..19404c8 100644
--- a/interviewQuestions/19/ru.mdx
+++ b/interviewQuestions/19/ru.mdx
@@ -1,7 +1,7 @@
---
meta:
- title: 'Как работает обработка исключений в Python?'
- description: 'Обработка исключений в Python выполняется через try/except/else/finally. Узнайте об иерархии исключений и правильной обработке ошибок.'
+ title: "Как работает обработка исключений в Python?"
+ description: "Обработка исключений в Python выполняется через try/except/else/finally. Узнайте об иерархии исключений и правильной обработке ошибок."
---
> **Исключение** — это ошибка, возникающая во время выполнения программы. Python позволяет перехватывать и обрабатывать исключения, чтобы программа не завершалась аварийно.
@@ -41,14 +41,14 @@ except (ValueError, TypeError) as e:
## Иерархия основных исключений:
-- `BaseException`
- - `Exception` — базовый класс для большинства исключений
- - `ValueError` — неверное значение
- - `TypeError` — неверный тип
- - `KeyError` — ключ не найден в словаре
- - `IndexError` — индекс за пределами списка
- - `FileNotFoundError` — файл не найден
- - `ZeroDivisionError` — деление на ноль
+- `BaseException`
+ - `Exception` — базовый класс для большинства исключений
+ - `ValueError` — неверное значение
+ - `TypeError` — неверный тип
+ - `KeyError` — ключ не найден в словаре
+ - `IndexError` — индекс за пределами списка
+ - `FileNotFoundError` — файл не найден
+ - `ZeroDivisionError` — деление на ноль
## Важное правило:
diff --git a/interviewQuestions/2/ru.mdx b/interviewQuestions/2/ru.mdx
index 5623a9a..92e6e7c 100644
--- a/interviewQuestions/2/ru.mdx
+++ b/interviewQuestions/2/ru.mdx
@@ -1,28 +1,28 @@
---
meta:
- title: 'Какие основные типы данных существуют в Python?'
- description: 'Основные типы данных в Python: int, float, str, bool и None. Узнайте их особенности и способы приведения типов.'
+ title: "Какие основные типы данных существуют в Python?"
+ description: "Основные типы данных в Python: int, float, str, bool и None. Узнайте их особенности и способы приведения типов."
---
Python предоставляет несколько встроенных типов данных:
## Числовые типы:
-- `int` — целые числа произвольной точности: `42`, `-7`, `1_000_000`
-- `float` — числа с плавающей точкой: `3.14`, `-0.5`, `1e10`
-- `complex` — комплексные числа: `3+4j`
+- `int` — целые числа произвольной точности: `42`, `-7`, `1_000_000`
+- `float` — числа с плавающей точкой: `3.14`, `-0.5`, `1e10`
+- `complex` — комплексные числа: `3+4j`
## Текстовый тип:
-- `str` — строка символов: `"Привет"`, `'Python'`
+- `str` — строка символов: `"Привет"`, `'Python'`
## Логический тип:
-- `bool` — принимает значения `True` или `False`
+- `bool` — принимает значения `True` или `False`
## Специальный тип:
-- `NoneType` — единственное значение `None`, означающее отсутствие значения
+- `NoneType` — единственное значение `None`, означающее отсутствие значения
## Приведение типов:
diff --git a/interviewQuestions/20/ru.mdx b/interviewQuestions/20/ru.mdx
index 639dbc1..4e40c6d 100644
--- a/interviewQuestions/20/ru.mdx
+++ b/interviewQuestions/20/ru.mdx
@@ -1,7 +1,7 @@
---
meta:
- title: 'Как создать собственное исключение?'
- description: 'Пользовательские исключения создаются наследованием от Exception. Узнайте, как и когда создавать свои исключения в Python.'
+ title: "Как создать собственное исключение?"
+ description: "Пользовательские исключения создаются наследованием от Exception. Узнайте, как и когда создавать свои исключения в Python."
---
> Собственные исключения создаются путём наследования от класса `Exception` (или его подклассов).
@@ -41,9 +41,9 @@ except ValidationError as e:
## Когда создавать свои исключения:
-- Когда встроенные исключения не описывают ошибку достаточно точно.
-- Для разделения бизнес-логики ошибок (например, `UserNotFoundError`, `PermissionDeniedError`).
-- Для удобного перехвата группы связанных ошибок через общий базовый класс.
+- Когда встроенные исключения не описывают ошибку достаточно точно.
+- Для разделения бизнес-логики ошибок (например, `UserNotFoundError`, `PermissionDeniedError`).
+- Для удобного перехвата группы связанных ошибок через общий базовый класс.
```python
class AppError(Exception):
diff --git a/interviewQuestions/21/ru.mdx b/interviewQuestions/21/ru.mdx
index 9fc6ec4..bd66ec2 100644
--- a/interviewQuestions/21/ru.mdx
+++ b/interviewQuestions/21/ru.mdx
@@ -1,7 +1,7 @@
---
meta:
- title: 'Что такое контекстный менеджер и как работает оператор with?'
- description: 'Контекстный менеджер автоматически управляет ресурсами через оператор with. Узнайте о методах __enter__ и __exit__ в Python.'
+ title: "Что такое контекстный менеджер и как работает оператор with?"
+ description: "Контекстный менеджер автоматически управляет ресурсами через оператор with. Узнайте о методах __enter__ и __exit__ в Python."
---
> **Контекстный менеджер** — это объект, который автоматически выполняет действия при входе в блок кода и при выходе из него (даже при возникновении ошибки).
@@ -26,8 +26,8 @@ with open("data.txt", "r") as file:
## Как работает:
-- `__enter__()` — вызывается при входе в блок `with`. Возвращаемое значение присваивается переменной после `as`.
-- `__exit__()` — вызывается при выходе из блока `with`, **даже если произошло исключение**.
+- `__enter__()` — вызывается при входе в блок `with`. Возвращаемое значение присваивается переменной после `as`.
+- `__exit__()` — вызывается при выходе из блока `with`, **даже если произошло исключение**.
## Создание своего контекстного менеджера:
@@ -51,7 +51,7 @@ with Timer():
## Где используются контекстные менеджеры:
-- Работа с **файлами** (`open`)
-- **Блокировки** в многопоточности (`threading.Lock`)
-- **Подключения к БД** (автоматический commit/rollback)
-- **Временные ресурсы** (временные файлы, сетевые соединения)
+- Работа с **файлами** (`open`)
+- **Блокировки** в многопоточности (`threading.Lock`)
+- **Подключения к БД** (автоматический commit/rollback)
+- **Временные ресурсы** (временные файлы, сетевые соединения)
diff --git a/interviewQuestions/22/ru.mdx b/interviewQuestions/22/ru.mdx
index d154789..5d9be80 100644
--- a/interviewQuestions/22/ru.mdx
+++ b/interviewQuestions/22/ru.mdx
@@ -1,17 +1,17 @@
---
meta:
- title: 'Как читать и записывать файлы в Python?'
- description: 'Для работы с файлами в Python используется функция open() с различными режимами. Узнайте о чтении, записи, кодировках и конструкции with.'
+ title: "Как читать и записывать файлы в Python?"
+ description: "Для работы с файлами в Python используется функция open() с различными режимами. Узнайте о чтении, записи, кодировках и конструкции with."
---
## Открытие файла:
Функция `open()` принимает путь к файлу и режим:
-- `'r'` — чтение (по умолчанию)
-- `'w'` — запись (перезаписывает файл)
-- `'a'` — дозапись в конец файла
-- `'rb'` / `'wb'` — чтение / запись в бинарном режиме
+- `'r'` — чтение (по умолчанию)
+- `'w'` — запись (перезаписывает файл)
+- `'a'` — дозапись в конец файла
+- `'rb'` / `'wb'` — чтение / запись в бинарном режиме
## Чтение файла:
diff --git a/interviewQuestions/23/ru.mdx b/interviewQuestions/23/ru.mdx
index e63557e..382725e 100644
--- a/interviewQuestions/23/ru.mdx
+++ b/interviewQuestions/23/ru.mdx
@@ -1,7 +1,7 @@
---
meta:
- title: 'Что такое классы и объекты в Python?'
- description: 'Класс — это шаблон для создания объектов. Узнайте о __init__, self, атрибутах экземпляра и класса в Python.'
+ title: "Что такое классы и объекты в Python?"
+ description: "Класс — это шаблон для создания объектов. Узнайте о __init__, self, атрибутах экземпляра и класса в Python."
---
> **Класс** — это шаблон (чертёж) для создания объектов. **Объект** — это конкретный экземпляр класса.
@@ -37,9 +37,9 @@ print(dog1.species) # Canis familiaris
## self:
-- `self` — ссылка на текущий экземпляр класса.
-- Передаётся автоматически при вызове метода.
-- Через `self` доступны атрибуты и методы объекта.
+- `self` — ссылка на текущий экземпляр класса.
+- Передаётся автоматически при вызове метода.
+- Через `self` доступны атрибуты и методы объекта.
## Атрибуты класса vs экземпляра:
diff --git a/interviewQuestions/24/ru.mdx b/interviewQuestions/24/ru.mdx
index 5780c5a..570d59a 100644
--- a/interviewQuestions/24/ru.mdx
+++ b/interviewQuestions/24/ru.mdx
@@ -1,7 +1,7 @@
---
meta:
- title: 'Что такое наследование и как оно работает в Python?'
- description: 'Наследование позволяет создавать классы на основе существующих. Узнайте о super(), переопределении методов и множественном наследовании в Python.'
+ title: "Что такое наследование и как оно работает в Python?"
+ description: "Наследование позволяет создавать классы на основе существующих. Узнайте о super(), переопределении методов и множественном наследовании в Python."
---
> **Наследование** — это механизм ООП, при котором дочерний класс получает атрибуты и методы родительского класса.
diff --git a/interviewQuestions/25/ru.mdx b/interviewQuestions/25/ru.mdx
index 1e1d21f..b6d9e8e 100644
--- a/interviewQuestions/25/ru.mdx
+++ b/interviewQuestions/25/ru.mdx
@@ -1,7 +1,7 @@
---
meta:
- title: 'Что такое инкапсуляция в Python?'
- description: 'Инкапсуляция ограничивает доступ к внутренним данным объекта. Узнайте о конвенциях _, __, name mangling и @property в Python.'
+ title: "Что такое инкапсуляция в Python?"
+ description: "Инкапсуляция ограничивает доступ к внутренним данным объекта. Узнайте о конвенциях _, __, name mangling и @property в Python."
---
> **Инкапсуляция** — это принцип ООП, при котором внутренние данные объекта скрываются от прямого доступа извне.
@@ -10,9 +10,9 @@ meta:
В Python нет строгих модификаторов доступа (`private`, `public`). Вместо этого используются конвенции:
-- `name` — публичный атрибут, доступен всем.
-- `_name` — «защищённый» (по конвенции), предназначен для внутреннего использования.
-- `__name` — «приватный», Python применяет **name mangling** (изменение имени).
+- `name` — публичный атрибут, доступен всем.
+- `_name` — «защищённый» (по конвенции), предназначен для внутреннего использования.
+- `__name` — «приватный», Python применяет **name mangling** (изменение имени).
```python
class BankAccount:
diff --git a/interviewQuestions/26/ru.mdx b/interviewQuestions/26/ru.mdx
index 845382b..7e0a560 100644
--- a/interviewQuestions/26/ru.mdx
+++ b/interviewQuestions/26/ru.mdx
@@ -1,7 +1,7 @@
---
meta:
- title: 'Что такое полиморфизм в Python?'
- description: 'Полиморфизм позволяет объектам разных классов иметь одинаковый интерфейс. Узнайте об утиной типизации и переопределении методов в Python.'
+ title: "Что такое полиморфизм в Python?"
+ description: "Полиморфизм позволяет объектам разных классов иметь одинаковый интерфейс. Узнайте об утиной типизации и переопределении методов в Python."
---
> **Полиморфизм** — это способность объектов разных классов реагировать на одинаковый вызов по-разному.
diff --git a/interviewQuestions/27/ru.mdx b/interviewQuestions/27/ru.mdx
index 7f07548..e5aa215 100644
--- a/interviewQuestions/27/ru.mdx
+++ b/interviewQuestions/27/ru.mdx
@@ -1,7 +1,7 @@
---
meta:
- title: 'Что такое статические методы и методы класса?'
- description: 'Статические методы (@staticmethod) не зависят от экземпляра, методы класса (@classmethod) получают класс первым аргументом. Сравнение в Python.'
+ title: "Что такое статические методы и методы класса?"
+ description: "Статические методы (@staticmethod) не зависят от экземпляра, методы класса (@classmethod) получают класс первым аргументом. Сравнение в Python."
---
## Обычный метод (метод экземпляра):
@@ -49,6 +49,6 @@ print(MathUtils.is_even(4)) # True
## Когда что использовать:
-- **Метод экземпляра** — когда нужен доступ к атрибутам объекта (`self`).
-- **@classmethod** — для альтернативных конструкторов или работы с атрибутами класса.
-- **@staticmethod** — для утилитных функций, логически связанных с классом, но не зависящих от экземпляра или класса.
+- **Метод экземпляра** — когда нужен доступ к атрибутам объекта (`self`).
+- **@classmethod** — для альтернативных конструкторов или работы с атрибутами класса.
+- **@staticmethod** — для утилитных функций, логически связанных с классом, но не зависящих от экземпляра или класса.
diff --git a/interviewQuestions/28/ru.mdx b/interviewQuestions/28/ru.mdx
index ca09dc5..93016d3 100644
--- a/interviewQuestions/28/ru.mdx
+++ b/interviewQuestions/28/ru.mdx
@@ -1,7 +1,7 @@
---
meta:
- title: 'Что такое магические методы?'
- description: 'Магические методы (__init__, __str__, __repr__, __eq__ и др.) определяют поведение объектов при стандартных операциях в Python.'
+ title: "Что такое магические методы?"
+ description: "Магические методы (__init__, __str__, __repr__, __eq__ и др.) определяют поведение объектов при стандартных операциях в Python."
---
> **Магические методы** (от англ. double underscore — двойное подчёркивание) — это специальные методы, которые определяют поведение объектов при стандартных операциях.
@@ -47,11 +47,11 @@ Point(1, 2) < Point(3, 4) # True
## Другие полезные магические методы:
-- `__len__` — поведение `len(obj)`
-- `__getitem__` — доступ по индексу `obj[key]`
-- `__contains__` — оператор `in`
-- `__add__` — оператор `+`
-- `__call__` — вызов объекта как функции `obj()`
+- `__len__` — поведение `len(obj)`
+- `__getitem__` — доступ по индексу `obj[key]`
+- `__contains__` — оператор `in`
+- `__add__` — оператор `+`
+- `__call__` — вызов объекта как функции `obj()`
```python
class Basket:
diff --git a/interviewQuestions/29/ru.mdx b/interviewQuestions/29/ru.mdx
index 4f232b8..8abb288 100644
--- a/interviewQuestions/29/ru.mdx
+++ b/interviewQuestions/29/ru.mdx
@@ -1,7 +1,7 @@
---
meta:
- title: 'Что такое декораторы и как они работают?'
- description: 'Декоратор — это функция, оборачивающая другую функцию для добавления поведения. Узнайте о создании декораторов и @wraps в Python.'
+ title: "Что такое декораторы и как они работают?"
+ description: "Декоратор — это функция, оборачивающая другую функцию для добавления поведения. Узнайте о создании декораторов и @wraps в Python."
---
> **Декоратор** — это функция, которая принимает другую функцию и возвращает новую, расширяя её поведение без изменения исходного кода.
@@ -63,8 +63,8 @@ print(add.__doc__) # Складывает два числа.
## Практические примеры декораторов:
-- Логирование вызовов функций
-- Замер времени выполнения
-- Кеширование результатов
-- Проверка прав доступа
-- Валидация аргументов
+- Логирование вызовов функций
+- Замер времени выполнения
+- Кеширование результатов
+- Проверка прав доступа
+- Валидация аргументов
diff --git a/interviewQuestions/3/ru.mdx b/interviewQuestions/3/ru.mdx
index abeeedd..c4a755c 100644
--- a/interviewQuestions/3/ru.mdx
+++ b/interviewQuestions/3/ru.mdx
@@ -1,14 +1,14 @@
---
meta:
- title: 'В чём разница между списком и кортежем?'
- description: 'Список — изменяемая коллекция, кортеж — неизменяемая. Узнайте ключевые различия в использовании list и tuple в Python.'
+ title: "В чём разница между списком и кортежем?"
+ description: "Список — изменяемая коллекция, кортеж — неизменяемая. Узнайте ключевые различия в использовании list и tuple в Python."
---
## Список:
-- **Изменяемый**: можно добавлять, удалять и изменять элементы.
-- Создаётся с помощью квадратных скобок `[]`.
-- Занимает больше памяти из-за возможности изменения размера.
+- **Изменяемый**: можно добавлять, удалять и изменять элементы.
+- Создаётся с помощью квадратных скобок `[]`.
+- Занимает больше памяти из-за возможности изменения размера.
```python
fruits = ["яблоко", "банан", "вишня"]
@@ -18,10 +18,10 @@ fruits[0] = "апельсин" # Изменение элемента
## Кортеж:
-- **Неизменяемый**: после создания нельзя изменить.
-- Создаётся с помощью круглых скобок `()`.
-- Работает быстрее и занимает меньше памяти.
-- Может использоваться как ключ словаря (так как хешируемый).
+- **Неизменяемый**: после создания нельзя изменить.
+- Создаётся с помощью круглых скобок `()`.
+- Работает быстрее и занимает меньше памяти.
+- Может использоваться как ключ словаря (так как хешируемый).
```python
point = (10, 20)
@@ -33,5 +33,5 @@ locations = {(55.75, 37.62): "Москва"}
## Когда что использовать:
-- `list` — когда коллекция будет изменяться (добавление, удаление элементов).
-- `tuple` — когда данные должны быть неизменяемыми (координаты, конфигурации, ключи словарей).
+- `list` — когда коллекция будет изменяться (добавление, удаление элементов).
+- `tuple` — когда данные должны быть неизменяемыми (координаты, конфигурации, ключи словарей).
diff --git a/interviewQuestions/30/ru.mdx b/interviewQuestions/30/ru.mdx
index 44ae881..f534c2e 100644
--- a/interviewQuestions/30/ru.mdx
+++ b/interviewQuestions/30/ru.mdx
@@ -1,7 +1,7 @@
---
meta:
- title: 'Что такое абстрактные классы и зачем они нужны?'
- description: 'Абстрактные классы задают интерфейс через @abstractmethod. Узнайте о модуле abc и создании абстрактных классов в Python.'
+ title: "Что такое абстрактные классы и зачем они нужны?"
+ description: "Абстрактные классы задают интерфейс через @abstractmethod. Узнайте о модуле abc и создании абстрактных классов в Python."
---
> **Абстрактный класс** — это класс, который нельзя создать напрямую. Он задаёт интерфейс (набор обязательных методов) для дочерних классов.
@@ -58,6 +58,6 @@ print(rect.perimeter()) # 16
## Зачем нужны:
-- **Контракт**: гарантируют, что дочерний класс реализует все необходимые методы.
-- **Документация**: явно показывают, какие методы должны быть реализованы.
-- **Раннее обнаружение ошибок**: если метод не реализован, Python выдаст `TypeError` при создании экземпляра, а не при вызове метода.
+- **Контракт**: гарантируют, что дочерний класс реализует все необходимые методы.
+- **Документация**: явно показывают, какие методы должны быть реализованы.
+- **Раннее обнаружение ошибок**: если метод не реализован, Python выдаст `TypeError` при создании экземпляра, а не при вызове метода.
diff --git a/interviewQuestions/31/ru.mdx b/interviewQuestions/31/ru.mdx
index 638afb0..d48c46c 100644
--- a/interviewQuestions/31/ru.mdx
+++ b/interviewQuestions/31/ru.mdx
@@ -1,6 +1,6 @@
---
meta:
- title: 'Как работает импорт модулей в Python?'
+ title: "Как работает импорт модулей в Python?"
description: 'Модули в Python импортируются через import и from...import. Узнайте о пакетах, псевдонимах и конструкции __name__ == "__main__".'
---
diff --git a/interviewQuestions/32/ru.mdx b/interviewQuestions/32/ru.mdx
index a6e02c1..1a30a0d 100644
--- a/interviewQuestions/32/ru.mdx
+++ b/interviewQuestions/32/ru.mdx
@@ -1,16 +1,16 @@
---
meta:
- title: 'Что такое виртуальное окружение и зачем оно нужно?'
- description: 'Виртуальное окружение изолирует зависимости проекта. Узнайте о venv, pip и requirements.txt для управления пакетами в Python.'
+ title: "Что такое виртуальное окружение и зачем оно нужно?"
+ description: "Виртуальное окружение изолирует зависимости проекта. Узнайте о venv, pip и requirements.txt для управления пакетами в Python."
---
> **Виртуальное окружение** — это изолированная среда Python, в которой устанавливаются пакеты только для конкретного проекта, не влияя на глобальную установку.
## Зачем нужно:
-- Разные проекты могут требовать **разные версии** одной библиотеки.
-- Избежать **конфликтов** между зависимостями проектов.
-- Зафиксировать точные версии пакетов для **воспроизводимости**.
+- Разные проекты могут требовать **разные версии** одной библиотеки.
+- Избежать **конфликтов** между зависимостями проектов.
+- Зафиксировать точные версии пакетов для **воспроизводимости**.
## Создание и использование:
diff --git a/interviewQuestions/33/ru.mdx b/interviewQuestions/33/ru.mdx
index e42f0dc..72f1a57 100644
--- a/interviewQuestions/33/ru.mdx
+++ b/interviewQuestions/33/ru.mdx
@@ -1,17 +1,17 @@
---
meta:
- title: 'Как работать с JSON в Python?'
- description: 'Модуль json позволяет сериализовать и десериализовать данные. Узнайте о json.dumps(), json.loads(), json.dump() и json.load() в Python.'
+ title: "Как работать с JSON в Python?"
+ description: "Модуль json позволяет сериализовать и десериализовать данные. Узнайте о json.dumps(), json.loads(), json.dump() и json.load() в Python."
---
> **JSON** (JavaScript Object Notation) — это текстовый формат обмена данными. Python предоставляет встроенный модуль `json` для работы с ним.
## Основные функции:
-- `json.dumps()` — Python-объект → JSON-строка
-- `json.loads()` — JSON-строка → Python-объект
-- `json.dump()` — Python-объект → JSON-файл
-- `json.load()` — JSON-файл → Python-объект
+- `json.dumps()` — Python-объект → JSON-строка
+- `json.loads()` — JSON-строка → Python-объект
+- `json.dump()` — Python-объект → JSON-файл
+- `json.load()` — JSON-файл → Python-объект
## Сериализация (Python → JSON):
@@ -49,11 +49,11 @@ with open("data.json", "r", encoding="utf-8") as f:
## Соответствие типов:
-| JSON | Python |
-|-----------|---------|
-| object | dict |
-| array | list |
-| string | str |
-| number | int/float |
+| JSON | Python |
+| ---------- | ---------- |
+| object | dict |
+| array | list |
+| string | str |
+| number | int/float |
| true/false | True/False |
-| null | None |
+| null | None |
diff --git a/interviewQuestions/34/ru.mdx b/interviewQuestions/34/ru.mdx
index cff2ab5..48b4769 100644
--- a/interviewQuestions/34/ru.mdx
+++ b/interviewQuestions/34/ru.mdx
@@ -1,7 +1,7 @@
---
meta:
- title: 'Что такое аннотации типов в Python?'
- description: 'Аннотации типов указывают ожидаемые типы данных. Узнайте о синтаксисе, модуле typing и зачем нужны аннотации типов в Python.'
+ title: "Что такое аннотации типов в Python?"
+ description: "Аннотации типов указывают ожидаемые типы данных. Узнайте о синтаксисе, модуле typing и зачем нужны аннотации типов в Python."
---
> **Аннотации типов** — это способ указать ожидаемые типы аргументов функций, возвращаемых значений и переменных. Они **не влияют** на выполнение программы, но помогают в разработке.
@@ -46,10 +46,10 @@ def parse(value: Union[int, str]) -> str:
## Зачем нужны аннотации:
-- **Документация**: тип ясен без чтения тела функции.
-- **IDE**: автодополнение и подсказки работают лучше.
-- **Поиск ошибок**: инструменты вроде `mypy` находят ошибки типов до запуска кода.
-- **Командная работа**: упрощают понимание кода другими разработчиками.
+- **Документация**: тип ясен без чтения тела функции.
+- **IDE**: автодополнение и подсказки работают лучше.
+- **Поиск ошибок**: инструменты вроде `mypy` находят ошибки типов до запуска кода.
+- **Командная работа**: упрощают понимание кода другими разработчиками.
## Важно:
diff --git a/interviewQuestions/35/ru.mdx b/interviewQuestions/35/ru.mdx
index 0fb4d2f..cb7ab87 100644
--- a/interviewQuestions/35/ru.mdx
+++ b/interviewQuestions/35/ru.mdx
@@ -1,7 +1,7 @@
---
meta:
- title: 'Чем отличается глубокое копирование от поверхностного?'
- description: 'Поверхностное копирование (copy) копирует ссылки на вложенные объекты, глубокое (deepcopy) — создаёт полностью независимую копию в Python.'
+ title: "Чем отличается глубокое копирование от поверхностного?"
+ description: "Поверхностное копирование (copy) копирует ссылки на вложенные объекты, глубокое (deepcopy) — создаёт полностью независимую копию в Python."
---
## Поверхностное копирование:
@@ -53,8 +53,8 @@ copy6 = copy.copy(lst)
## Когда что использовать:
-- **Поверхностное** — когда коллекция содержит только неизменяемые элементы (числа, строки).
-- **Глубокое** — когда есть вложенные изменяемые объекты (списки в списках, словари в списках).
+- **Поверхностное** — когда коллекция содержит только неизменяемые элементы (числа, строки).
+- **Глубокое** — когда есть вложенные изменяемые объекты (списки в списках, словари в списках).
```python
# Поверхностного достаточно
diff --git a/interviewQuestions/36/ru.mdx b/interviewQuestions/36/ru.mdx
index 5127582..566b9c0 100644
--- a/interviewQuestions/36/ru.mdx
+++ b/interviewQuestions/36/ru.mdx
@@ -1,15 +1,15 @@
---
meta:
- title: 'Что такое итераторы и протокол итерации?'
- description: 'Итератор — объект с методами __iter__ и __next__. Узнайте, как работает протокол итерации и цикл for под капотом в Python.'
+ title: "Что такое итераторы и протокол итерации?"
+ description: "Итератор — объект с методами __iter__ и __next__. Узнайте, как работает протокол итерации и цикл for под капотом в Python."
---
> **Итератор** — это объект, который возвращает элементы по одному через метод `__next__()` и сигнализирует об окончании через исключение `StopIteration`.
## Протокол итерации:
-- `__iter__()` — возвращает сам итератор.
-- `__next__()` — возвращает следующий элемент или выбрасывает `StopIteration`.
+- `__iter__()` — возвращает сам итератор.
+- `__next__()` — возвращает следующий элемент или выбрасывает `StopIteration`.
## Как работает цикл for:
@@ -53,9 +53,9 @@ for num in Countdown(5):
## Встроенные итерируемые объекты:
-- `list`, `tuple`, `str`, `dict`, `set` — **итерируемые** (имеют `__iter__`).
-- `range()`, `map()`, `filter()`, `zip()` — возвращают **итераторы**.
-- **Итератор** можно пройти только **один раз**, а **итерируемый объект** можно проходить многократно.
+- `list`, `tuple`, `str`, `dict`, `set` — **итерируемые** (имеют `__iter__`).
+- `range()`, `map()`, `filter()`, `zip()` — возвращают **итераторы**.
+- **Итератор** можно пройти только **один раз**, а **итерируемый объект** можно проходить многократно.
## На практике:
diff --git a/interviewQuestions/37/ru.mdx b/interviewQuestions/37/ru.mdx
index 362e3c2..63ba831 100644
--- a/interviewQuestions/37/ru.mdx
+++ b/interviewQuestions/37/ru.mdx
@@ -1,7 +1,7 @@
---
meta:
- title: 'В чём разница между is и ==?'
- description: 'Оператор == сравнивает значения, is — проверяет идентичность объектов в памяти. Узнайте о кешировании и правильном использовании в Python.'
+ title: "В чём разница между is и ==?"
+ description: "Оператор == сравнивает значения, is — проверяет идентичность объектов в памяти. Узнайте о кешировании и правильном использовании в Python."
---
## == (сравнение по значению):
@@ -46,7 +46,7 @@ print(x is y) # False — разные объекты
## Правило использования:
-- Используйте `is` только для сравнения с `None`:
+- Используйте `is` только для сравнения с `None`:
```python
# Правильно
@@ -58,5 +58,5 @@ if value == None:
print("Нет значения")
```
-- Для сравнения значений **всегда** используйте `==`.
-- `is` проверяет `id()` объектов — это полезно только для синглтонов (`None`, `True`, `False`).
+- Для сравнения значений **всегда** используйте `==`.
+- `is` проверяет `id()` объектов — это полезно только для синглтонов (`None`, `True`, `False`).
diff --git a/interviewQuestions/38/ru.mdx b/interviewQuestions/38/ru.mdx
index 7613952..950cd70 100644
--- a/interviewQuestions/38/ru.mdx
+++ b/interviewQuestions/38/ru.mdx
@@ -1,7 +1,7 @@
---
meta:
- title: 'Как работает управление памятью в Python?'
- description: 'Python управляет памятью через подсчёт ссылок и сборщик мусора. Узнайте, как Python автоматически освобождает неиспользуемую память.'
+ title: "Как работает управление памятью в Python?"
+ description: "Python управляет памятью через подсчёт ссылок и сборщик мусора. Узнайте, как Python автоматически освобождает неиспользуемую память."
---
> Python автоматически управляет памятью, освобождая разработчика от ручного выделения и освобождения памяти.
@@ -41,7 +41,7 @@ del a, b
## Основные моменты:
-- **Подсчёт ссылок** — основной механизм, работает мгновенно.
-- **Сборщик мусора** — дополнительный механизм для циклических ссылок.
-- **del** — удаляет ссылку на объект, а не сам объект.
-- Разработчику редко нужно вмешиваться в управление памятью — Python справляется сам.
+- **Подсчёт ссылок** — основной механизм, работает мгновенно.
+- **Сборщик мусора** — дополнительный механизм для циклических ссылок.
+- **del** — удаляет ссылку на объект, а не сам объект.
+- Разработчику редко нужно вмешиваться в управление памятью — Python справляется сам.
diff --git a/interviewQuestions/39/ru.mdx b/interviewQuestions/39/ru.mdx
index 6d777e9..e626d26 100644
--- a/interviewQuestions/39/ru.mdx
+++ b/interviewQuestions/39/ru.mdx
@@ -1,7 +1,7 @@
---
meta:
- title: 'Что такое замыкание в Python?'
- description: 'Замыкание — это вложенная функция, которая запоминает переменные из внешней области видимости. Узнайте о замыканиях и их применении в Python.'
+ title: "Что такое замыкание в Python?"
+ description: "Замыкание — это вложенная функция, которая запоминает переменные из внешней области видимости. Узнайте о замыканиях и их применении в Python."
---
> **Замыкание** — это вложенная функция, которая «запоминает» переменные из внешней функции, даже после того как внешняя функция завершилась.
@@ -23,9 +23,9 @@ print(triple(5)) # 15
## Условия для замыкания:
-- Есть **вложенная функция**.
-- Вложенная функция использует переменные из **внешней функции**.
-- Внешняя функция **возвращает** вложенную функцию.
+- Есть **вложенная функция**.
+- Вложенная функция использует переменные из **внешней функции**.
+- Внешняя функция **возвращает** вложенную функцию.
## Практические примеры:
diff --git a/interviewQuestions/4/ru.mdx b/interviewQuestions/4/ru.mdx
index 356ffe9..62356dd 100644
--- a/interviewQuestions/4/ru.mdx
+++ b/interviewQuestions/4/ru.mdx
@@ -1,14 +1,14 @@
---
meta:
- title: 'Чем отличаются set и frozenset?'
- description: 'set — изменяемое множество, frozenset — неизменяемое. Узнайте операции над множествами и особенности каждого типа в Python.'
+ title: "Чем отличаются set и frozenset?"
+ description: "set — изменяемое множество, frozenset — неизменяемое. Узнайте операции над множествами и особенности каждого типа в Python."
---
## set (множество):
-- **Изменяемая** неупорядоченная коллекция уникальных элементов.
-- Поддерживает добавление и удаление элементов.
-- Не может быть ключом словаря или элементом другого множества.
+- **Изменяемая** неупорядоченная коллекция уникальных элементов.
+- Поддерживает добавление и удаление элементов.
+- Не может быть ключом словаря или элементом другого множества.
```python
colors = {"красный", "зелёный", "синий"}
@@ -18,9 +18,9 @@ colors.discard("красный")
## frozenset (замороженное множество):
-- **Неизменяемая** версия множества.
-- Не поддерживает добавление и удаление элементов.
-- Является хешируемым — может быть ключом словаря.
+- **Неизменяемая** версия множества.
+- Не поддерживает добавление и удаление элементов.
+- Является хешируемым — может быть ключом словаря.
```python
immutable_set = frozenset([1, 2, 3])
diff --git a/interviewQuestions/40/ru.mdx b/interviewQuestions/40/ru.mdx
index d1feff4..c486dab 100644
--- a/interviewQuestions/40/ru.mdx
+++ b/interviewQuestions/40/ru.mdx
@@ -1,7 +1,7 @@
---
meta:
- title: 'Чем отличается многопоточность от многопроцессности в Python?'
- description: 'threading подходит для I/O задач, multiprocessing — для CPU задач. Узнайте о GIL и выборе подхода к параллелизму в Python.'
+ title: "Чем отличается многопоточность от многопроцессности в Python?"
+ description: "threading подходит для I/O задач, multiprocessing — для CPU задач. Узнайте о GIL и выборе подхода к параллелизму в Python."
---
## Многопоточность:
@@ -48,17 +48,17 @@ for p in processes:
GIL — это механизм в CPython, который позволяет **только одному потоку** выполнять Python-код одновременно. Из-за этого:
-- Потоки **не ускоряют** задачи с интенсивными вычислениями (CPU-bound).
-- Потоки **ускоряют** задачи с ожиданием ввода-вывода (I/O-bound): сетевые запросы, чтение файлов.
+- Потоки **не ускоряют** задачи с интенсивными вычислениями (CPU-bound).
+- Потоки **ускоряют** задачи с ожиданием ввода-вывода (I/O-bound): сетевые запросы, чтение файлов.
## Когда что использовать:
-- **threading** — для I/O-задач: скачивание файлов, API-запросы, работа с БД.
-- **multiprocessing** — для CPU-задач: обработка данных, вычисления, алгоритмы.
+- **threading** — для I/O-задач: скачивание файлов, API-запросы, работа с БД.
+- **multiprocessing** — для CPU-задач: обработка данных, вычисления, алгоритмы.
-| Критерий | threading | multiprocessing |
-|----------|-----------|----------------|
-| Память | Общая | Раздельная |
-| GIL | Ограничивает | Не влияет |
-| Накладные расходы | Низкие | Высокие |
-| Лучше для | I/O-bound | CPU-bound |
+| Критерий | threading | multiprocessing |
+| ----------------- | ------------ | --------------- |
+| Память | Общая | Раздельная |
+| GIL | Ограничивает | Не влияет |
+| Накладные расходы | Низкие | Высокие |
+| Лучше для | I/O-bound | CPU-bound |
diff --git a/interviewQuestions/5/ru.mdx b/interviewQuestions/5/ru.mdx
index 8064003..be191dc 100644
--- a/interviewQuestions/5/ru.mdx
+++ b/interviewQuestions/5/ru.mdx
@@ -1,16 +1,16 @@
---
meta:
- title: 'Что такое словарь и как он работает?'
- description: 'Словарь в Python — это коллекция пар ключ-значение на основе хеш-таблицы с O(1) доступом. Узнайте основные методы и особенности dict.'
+ title: "Что такое словарь и как он работает?"
+ description: "Словарь в Python — это коллекция пар ключ-значение на основе хеш-таблицы с O(1) доступом. Узнайте основные методы и особенности dict."
---
> **Словарь** — это изменяемая коллекция пар «ключ — значение», реализованная на основе хеш-таблицы.
## Основные характеристики:
-- Доступ, вставка и удаление элементов выполняются за **O(1)** в среднем случае.
-- Ключи должны быть **хешируемыми** (строки, числа, кортежи).
-- Начиная с Python 3.7, словари **сохраняют порядок вставки**.
+- Доступ, вставка и удаление элементов выполняются за **O(1)** в среднем случае.
+- Ключи должны быть **хешируемыми** (строки, числа, кортежи).
+- Начиная с Python 3.7, словари **сохраняют порядок вставки**.
```python
user = {
diff --git a/interviewQuestions/6/ru.mdx b/interviewQuestions/6/ru.mdx
index c657c4c..3afd5f9 100644
--- a/interviewQuestions/6/ru.mdx
+++ b/interviewQuestions/6/ru.mdx
@@ -1,17 +1,17 @@
---
meta:
- title: 'Что такое изменяемые и неизменяемые типы данных?'
- description: 'Изменяемые типы (list, dict, set) можно менять после создания, неизменяемые (int, str, tuple) — нет. Узнайте, почему это важно в Python.'
+ title: "Что такое изменяемые и неизменяемые типы данных?"
+ description: "Изменяемые типы (list, dict, set) можно менять после создания, неизменяемые (int, str, tuple) — нет. Узнайте, почему это важно в Python."
---
> В Python все объекты делятся на **изменяемые** и **неизменяемые** в зависимости от того, можно ли менять их содержимое после создания.
## Неизменяемые:
-- `int`, `float`, `bool`
-- `str`
-- `tuple`
-- `frozenset`
+- `int`, `float`, `bool`
+- `str`
+- `tuple`
+- `frozenset`
При «изменении» создаётся новый объект:
@@ -24,9 +24,9 @@ print(id(x)) # Другой id — это новый объект
## Изменяемые:
-- `list`
-- `dict`
-- `set`
+- `list`
+- `dict`
+- `set`
Объект изменяется «на месте»:
@@ -39,9 +39,9 @@ print(id(lst)) # Тот же id — объект изменился
## Почему это важно:
-- **Ключи словаря** могут быть только неизменяемыми объектами.
-- **Передача в функции**: изменяемые объекты могут быть изменены внутри функции, что может привести к неожиданным побочным эффектам.
-- **Значения по умолчанию**: не стоит использовать изменяемые объекты как значения по умолчанию в функциях.
+- **Ключи словаря** могут быть только неизменяемыми объектами.
+- **Передача в функции**: изменяемые объекты могут быть изменены внутри функции, что может привести к неожиданным побочным эффектам.
+- **Значения по умолчанию**: не стоит использовать изменяемые объекты как значения по умолчанию в функциях.
```python
# Распространённая ошибка
diff --git a/interviewQuestions/7/ru.mdx b/interviewQuestions/7/ru.mdx
index 4b0ae7b..6ac953b 100644
--- a/interviewQuestions/7/ru.mdx
+++ b/interviewQuestions/7/ru.mdx
@@ -1,13 +1,13 @@
---
meta:
- title: 'Как работает индексация и срезы в Python?'
- description: 'Индексация обращается к элементу по позиции, срезы позволяют извлекать части последовательности. Узнайте синтаксис [start:stop:step] в Python.'
+ title: "Как работает индексация и срезы в Python?"
+ description: "Индексация обращается к элементу по позиции, срезы позволяют извлекать части последовательности. Узнайте синтаксис [start:stop:step] в Python."
---
## Индексация:
-- Элементы нумеруются начиная с `0`.
-- Отрицательные индексы считаются с конца: `-1` — последний элемент.
+- Элементы нумеруются начиная с `0`.
+- Отрицательные индексы считаются с конца: `-1` — последний элемент.
```python
text = "Python"
@@ -20,9 +20,9 @@ text[-2] # 'o'
Синтаксис: `[start:stop:step]`
-- `start` — начальный индекс (включительно), по умолчанию `0`.
-- `stop` — конечный индекс (не включительно), по умолчанию длина последовательности.
-- `step` — шаг, по умолчанию `1`.
+- `start` — начальный индекс (включительно), по умолчанию `0`.
+- `stop` — конечный индекс (не включительно), по умолчанию длина последовательности.
+- `step` — шаг, по умолчанию `1`.
```python
nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
diff --git a/interviewQuestions/8/ru.mdx b/interviewQuestions/8/ru.mdx
index e376f0d..28d408d 100644
--- a/interviewQuestions/8/ru.mdx
+++ b/interviewQuestions/8/ru.mdx
@@ -1,7 +1,7 @@
---
meta:
- title: 'Что такое f-строки и чем они отличаются от других способов форматирования?'
- description: 'f-строки — самый удобный способ форматирования строк в Python. Сравнение f-string, .format() и оператора % для интерполяции значений.'
+ title: "Что такое f-строки и чем они отличаются от других способов форматирования?"
+ description: "f-строки — самый удобный способ форматирования строк в Python. Сравнение f-string, .format() и оператора % для интерполяции значений."
---
Python предлагает несколько способов форматирования строк:
@@ -33,9 +33,9 @@ print("Привет, %s! Тебе %d лет." % (name, age))
## Сравнение:
-- **f-строки** — самый читаемый и быстрый способ. Поддерживают любые выражения.
-- **.format()** — полезен, когда шаблон строки задаётся заранее.
-- **%** — устаревший подход, встречается в старом коде.
+- **f-строки** — самый читаемый и быстрый способ. Поддерживают любые выражения.
+- **.format()** — полезен, когда шаблон строки задаётся заранее.
+- **%** — устаревший подход, встречается в старом коде.
## Форматирование чисел:
diff --git a/interviewQuestions/9/ru.mdx b/interviewQuestions/9/ru.mdx
index 0f52051..70e2b0a 100644
--- a/interviewQuestions/9/ru.mdx
+++ b/interviewQuestions/9/ru.mdx
@@ -1,7 +1,7 @@
---
meta:
- title: 'Как работают условные конструкции if / elif / else?'
- description: 'Условные конструкции if/elif/else управляют ветвлением программы. Узнайте о тернарном операторе и truthy/falsy значениях в Python.'
+ title: "Как работают условные конструкции if / elif / else?"
+ description: "Условные конструкции if/elif/else управляют ветвлением программы. Узнайте о тернарном операторе и truthy/falsy значениях в Python."
---
Условные конструкции позволяют выполнять разные блоки кода в зависимости от условия.
@@ -31,9 +31,9 @@ status = "совершеннолетний" if age >= 18 else "несоверш
В Python следующие значения считаются **ложными**:
-- `False`, `None`
-- `0`, `0.0`
-- Пустые коллекции: `""`, `[]`, `()`, `{}`, `set()`
+- `False`, `None`
+- `0`, `0.0`
+- Пустые коллекции: `""`, `[]`, `()`, `{}`, `set()`
Всё остальное считается **истинным**: