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 кода. Курс также можно проходить с планшета или телефона, -но некоторый функционал может быть недоступен или работать некорректно. Просим отнестись к этому с пониманием. +Курс рассчитан на компьютер или ноутбук — с клавиатурой писать код всё-таки удобнее всего. С планшета и телефона заниматься тоже можно, +но часть возможностей будет недоступна или заработает не так, как задумано. Просим отнестись к этому с пониманием. ## Как организован наш ресурс -![Как организован наш ресурс](https://python-academy.org/static/guidePage/intro-intro/connections.webp) +![Как организован наш ресурс](https://python-academy.org/static/guidePage/intro-intro/sections-ru.webp "Как организован наш ресурс") + На нашем ресурсе есть 3 основных раздела, каждый из которых будет по-разному полезен: -- **[Данный курс](/guide)** – это ваш пошаговый помощник при изучении Python. Здесь представлены последовательные, структурированные материалы, а также связанные упражнения для закрепления знаний. -- **[Тренажёр](/trainer)** – это инструмент проверки ваших знаний. Задания в нем проранжированы по сложности, поэтому вы можете подбирать подходящие для вашего текущего уровня. - Также прорешивание заданий из тренажёра необходимо для получения сертификата, подтверждающего знания в области Python. -- **[Песочница](/sandbox)** – это пространство для свободного экспериментирования с Python кодом и проверки полученных знаний. +- ** Данный курс** – это ваш пошаговый помощник при изучении Python. Здесь представлены последовательные, структурированные материалы, а также связанные упражнения для закрепления знаний. +- **Тренажёр** – это инструмент проверки ваших знаний. Задания в нем проранжированы по сложности, поэтому вы можете подбирать подходящие для вашего текущего уровня. + Также прорешивание заданий из тренажёра необходимо для получения сертификата, подтверждающего знания в области Python. +- **Песочница** – это пространство для свободного экспериментирования с Python кодом и проверки полученных знаний. + +## Премиум-аккаунт + +Базовый курс, тренажёр и песочница доступны бесплатно. Премиум-аккаунт — это необязательное дополнение для тех, кто хочет учиться глубже и пользоваться всеми возможностями платформы. + +С премиум-аккаунтом вы получаете: + +- доступ к готовым решениям заданий из тренажёра; +- продвинутые курсы «Backend на FastAPI с нуля» и «Продуктовая аналитика на Python»; +- расширенный доступ к AI-ассистенту и задания с собеседований в топовые компании; +- возможность выпускать именные сертификаты. -## Премиум аккаунт +Премиум действует 6 месяцев с момента оплаты. Весь прогресс, накопленный до покупки, сохранится и будет учитываться при получении сертификатов. После окончания срока действия премиума аккаунт снова станет бесплатным, а прогресс и уже выпущенные сертификаты останутся у вас. -Покупка [премиум аккаунта](/premium) – это необязательная часть, курс мы предоставляем для всех абсолютно бесплатно. Но с ним для вас открывается больше возможностей для -более продуктивного изучения, открывается доступ к заданиям из реальных собеседований в топовые компании и даётся возможность получить сертификат. К тому же это отличная возможность поддержать нас в развитии данного сервиса. +Покупая премиум-аккаунт, вы также поддерживаете развитие Python Academy и помогаете нам создавать новые учебные материалы и возможности. ## Прогресс и получение сертификата -Если вы авторизованы, то в разделе **[Тренажёр](/trainer)** можете увидеть прогресс-бар. +Если вы авторизованы, то в разделе **Тренажёр** можете увидеть прогресс-бар. ![Прогресс бар до получения сертификата](https://python-academy.org/static/guidePage/intro-intro/progress_bar_ru.jpg) 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 😊") + +Hi, I love Python 😊 + ``` -Если в нём есть активная синяя кнопка, то код можно запустить. При нажатии на кнопку код откроется в новой вкладке — в песочнице. +Строка со значком `>_` — это уже не код, а результат: то, что Python напечатал, выполнив строку выше. Так что читать блок нужно сверху вниз: сначала что мы попросили, потом что получили. + +Справа в шапке блока живут четыре значка: + +![Блок с исполняемым кодом](https://python-academy.org/static/guidePage/intro-structure-of-course/code-block-ru.webp "Блок с исполняемым кодом") + +Значок ▶ есть не у всех блоков — только у тех, чей код можно выполнить. Он открывает код в **песочнице**: это отдельная вкладка, где его можно менять и запускать сколько угодно раз. + +Одна тонкость про ▶: он отправляет в песочницу только свой блок, без соседних. Поэтому каждый пример самодостаточен — всё, что нужно коду, объявлено прямо в нём, и запускать блоки можно в любом порядке. -![Описание блока с исполняемым кодом](https://python-academy.org/static/guidePage/intro-structure-of-course/ru_codeplayground_description.png 'Описание блока с исполняемым кодом') +## Задания для самопроверки -### Задания для самопроверки +Не в каждой статье, но во многих — особенно в «Основах Python I» — в конце ждёт блок с задачами: -Настоятельно рекомендуется не пропускать задачи в конце каждой статьи. Они выглядят следующим образом: +![Задания для самопроверки](https://python-academy.org/static/guidePage/intro-structure-of-course/ru_exercises_description.png "Задания для самопроверки") -![Описание заданий для самопроверки](https://python-academy.org/static/guidePage/intro-structure-of-course/ru_exercises_description.png 'Описание заданий для самопроверки') +Не пропускайте их. Прочитать про цикл и написать цикл — разные вещи, и вторая обнаруживается только за клавиатурой 🎓. -Перед решением задач рекомендуется войти в свой аккаунт, так все ваши решения будут сохраняться. +Перед решением войдите в аккаунт, иначе решения не сохранятся. 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) + +15 + + +print("10" + "5") + +105 + +``` + +В первом случае `+` сложил числа, во втором — склеил две строки подряд. 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}") + +Солнечно: True, Тепло: True + print(f"Идем на пляж? {going_to_beach}") + +Идем на пляж? True + # Что если погода изменится? warm = False # Стало холодно going_to_beach = sunny and warm print(f"Солнечно: {sunny}, Тепло: {warm}") + +Солнечно: True, Тепло: False + print(f"Идем на пляж? {going_to_beach}") + +Идем на пляж? False + ``` -### 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}") + +Телефон красивый: True, Телефон дешевый: False + 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) + +Купим телефон? True + ``` -### 3. Оператор not (логическое НЕ) +### Оператор not (логическое НЕ) Инвертирует значение: `True` становится `False`, и наоборот: ```python have_homework = True print(f"У меня есть домашнее задание: {have_homework}") + +У меня есть домашнее задание: True + print(f"У меня НЕТ домашнего задания: {not have_homework}") + +У меня НЕТ домашнего задания: False + ``` ## Сравнение значений @@ -95,23 +109,57 @@ my_age = 25 friend_age = 30 print(f"Мой возраст: {my_age}, возраст друга: {friend_age}") + +Мой возраст: 25, возраст друга: 30 + print(f"Наш возраст одинаковый? {my_age == friend_age}") + +Наш возраст одинаковый? False + print(f"Наш возраст разный? {my_age != friend_age}") + +Наш возраст разный? True + print(f"Я младше? {my_age < friend_age}") + +Я младше? True + # Сравнение строк (по алфавиту) print(f"'apple' < 'banana': {'apple' < 'banana'}") + +'apple' < 'banana': True + +``` + +### Оператор 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}") + +Содержимое одинаковое? True + print(f"Это один и тот же объект? {my_scores is friend_scores}") + +Это один и тот же объект? False + print(f"same_list это тот же объект что и my_scores? {my_scores is same_list}") + +same_list это тот же объект что и my_scores? True + ``` +**Правило:** в подавляющем большинстве случаев нужен `==`. Оператор `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}") + +has_ticket and (has_passport or has_visa) = True + # Разберем вычисление пошагово: 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}") + +Шаг 1: has_passport or has_visa = True + step2 = has_ticket and step1 # Затем применяется оператор and print(f"Шаг 2: has_ticket and (результат шага 1) = {step2}") + +Шаг 2: has_ticket and (результат шага 1) = True + ``` -> 💡 **Совет**: Если сомневаетесь в порядке выполнения, используйте скобки! -> Они делают код более понятным и точно контролируют порядок вычислений. +> **Совет**: если сомневаетесь в порядке выполнения, используйте скобки. Они делают код более понятным и точно контролируют порядок вычислений. ## Преобразование в логический тип: что считается истиной? @@ -151,9 +207,21 @@ Python может преобразовать любое значение в ло ```python print(bool(100)) + +True + print(bool(0)) + +False + print(bool("Hello")) + +True + print(bool("")) + +False + ``` ### Что считается истинным и ложным? @@ -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"]) + +75 + +``` -- **Ключи должны быть уникальными** — нельзя иметь два одинаковых ключа в одном словаре -- **Ключи должны быть неизменяемыми** — в качестве ключей можно использовать строки, числа, кортежи, но не списки или словари -- **Значения могут быть любого типа** — числа, строки, списки, другие словари и т.д. -- **Начиная с 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"]) + +{'name': 'John', 'age': 30, 'city': 'New York'} + ``` -### 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) + +{'name': 'Anna', 'age': 28, 'city': 'Berlin'} + ``` -### 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"]) + +John + ``` -## Доступ к элементам словаря - -Доступ к значениям в словаре осуществляется через ключи: - -### Обращение по ключу +Но если ключа в словаре нет, скобки не возвращают пустоту, а останавливают программу с ошибкой `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")) + +None + -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) + +{'name': 'John', 'age': 30, 'city': 'New York'} + -# Изменение значения существующего ключа +# Ключ "age" уже есть — значение заменяется person["age"] = 31 print(person) - -# Использование метода update() для массового обновления -person.update({"age": 32, "job": "developer", "language": "Python"}) -print(person) + +{'name': 'John', 'age': 31, 'city': 'New York'} + ``` -### Удаление элементов +Когда нужно внести сразу несколько изменений, удобнее `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) + +{'name': 'John', 'age': 32, 'job': 'developer'} + ``` -## Методы словарей +## Проверка наличия ключа -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) + +True + + +print("phone" in person) + +False + +``` -# Получение всех значений -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) + +{'name': 'John', 'age': 30} + ``` -### Копирование словарей +Метод `pop()` удаляет ключ и заодно возвращает его значение — пригодится, когда удаляемое значение ещё нужно. Со вторым аргументом он не падает на отсутствующем ключе, а возвращает значение по умолчанию. ```python -import copy +person = {"name": "John", "age": 30} -# Словарь с вложенной структурой -original = {"name": "John", "settings": {"theme": "dark"}} +age = person.pop("age") +print(age) + +30 + -# Поверхностная и глубокая копии -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]) + +John : 90 +Mary : 75 +Kate : 80 + ``` -### Объединение словарей +Если внутри цикла нужны и ключ, и значение, метод `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) + +John : 90 +Mary : 75 +Kate : 80 + +``` -# Создаем копию 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) + +Сумма баллов: 245 + ``` -### Установка значения по умолчанию +## Частая задача: подсчёт + +Словарь хорошо подходит, чтобы что-то посчитать: ключ — это объект, значение — счётчик. Посчитаем, сколько раз встречается каждое слово. + +В лоб это выглядит так: для каждого слова проверяем, видели ли мы его раньше, и либо увеличиваем счётчик, либо заводим новый. ```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) + +{'one': 2, 'two': 2, 'three': 1} + +``` + +Здесь как раз помогает `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) + +{'one': 2, 'two': 2, 'three': 1} + ``` -## Итерация по словарям +## Что годится в ключи + +Ключи живут по двум правилам, и оба следуют из того, как словарь устроен внутри. -Существует несколько способов перебора элементов словаря: +- **Ключи уникальны.** Записать два одинаковых ключа нельзя: второе присваивание просто перезапишет первое. Это логично — иначе по ключу `"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 -Функции — это как готовые рецепты для вашего кода. Они позволяют вам создавать блоки кода, которые можно использовать многократно, делая ваши программы более организованными и легкими для понимания. +Чтобы не копировать один и тот же блок кода в нескольких местах программы, его описывают как функцию и потом вызывают по имени. Функция принимает входные значения, что-то с ними делает и обычно возвращает результат. ## Что такое функции? -> Функция — это именованный блок кода, который выполняет определенную задачу и может быть вызван из других частей программы. +> Функция — это именованный блок кода, который выполняет определённую задачу и может быть вызван из других частей программы. -Функции помогают: +Удобнее всего представлять функцию как «чёрный ящик»: на вход подаём аргументы, внутри что-то происходит, наружу возвращается результат. -- Организовать код в логические блоки -- Избежать повторения кода -- Сделать программу более читаемой -- Упростить отладку и поддержку +![Функция как чёрный ящик: на вход аргументы 5 и 3, внутри def add(a, b): return a + b, на выходе 8](https://python-academy.org/static/guidePage/functions/function-box-ru.webp "Функция как преобразование вход → выход") ## Создание и вызов функций ### Базовый синтаксис -Функция в 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}") + +5 + 3 = 8 + +``` + +`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) + +Привет! +None + ``` ## Параметры функций -### Позиционные и именованные параметры +### Позиционные и именованные аргументы 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="программирование") + +{'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` с именем и параметрами уже стоит в редакторе, а тело дописываете вы. + +### Откуда берутся значения + +Значения, с которыми работает функция, приходят в её параметрах. Придумывать или вводить их не нужно: каждый тест сам вызывает функцию со своими значениями. + +![Устройство теста: свои значения параметров, вызов функции и ожидаемый результат](https://python-academy.org/static/guidePage/functions/exercise-test-case-ru.webp "Устройство тест-кейса") + +На вкладке теста видно всё сразу: какие значения он подставит, как вызовет функцию и какой результат ждёт. У каждого теста значения свои, и пройти должны все. + +### Результат нужно вернуть + +Функция должна **возвращать** результат через `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} нечётное") + +42 чётное + ``` ### Проверка пароля -Условия помогают реализовать проверку данных на соответствие определенным критериям: +Условия помогают проверять данные на соответствие правилам. Здесь `.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) + +[1, 2, 3, 4, 5] + # Список разных типов данных mixed = [1, "привет", True, 3.14] print(mixed) + +[1, 'привет', True, 3.14] + # Вложенные списки (список списков) matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] print(matrix) + +[[1, 2, 3], [4, 5, 6], [7, 8, 9]] + ``` ### С помощью конструктора list() @@ -43,29 +67,23 @@ print(matrix) # Создание пустого списка empty_list = list() print(empty_list) + +[] + # Создание списка из строки (каждый символ становится элементом) chars = list("Python") print(chars) + +['P', 'y', 't', 'h', 'o', 'n'] + # Создание списка из других итерируемых объектов 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) + +[1, 2, 3] + ``` ## Доступ к элементам списка @@ -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)}") + +Всего фруктов: 3 + +``` + +Иногда вместе с элементом нужен и его индекс. Заводить для этого отдельную переменную-счётчик не нужно — есть `enumerate()`, который выдаёт пару «индекс и значение»: + +```python +fruits = ["яблоко", "банан", "вишня"] + +for index, fruit in enumerate(fruits): + print(f"Индекс {index}: {fruit}") + +Индекс 0: яблоко +Индекс 1: банан +Индекс 2: вишня + ``` ## Изменение элементов списка -В отличие от строк, списки являются изменяемыми, то есть вы можете изменять, добавлять и удалять элементы: +Главное отличие списков от строк: **списки изменяемые** (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) + +[1, 20, 30, 40, 5] + # Можно даже заменить несколько элементов на другое количество numbers = [1, 2, 3, 4, 5] numbers[1:4] = [20, 30] print(numbers) + +[1, 20, 30, 5] + ``` ## Основные методы списков @@ -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) + +[2, 3, 4, 5] + del numbers[1:3] print(numbers) + +[2, 5] + ``` ### Поиск и подсчет элементов @@ -204,15 +328,27 @@ fruits = ["яблоко", "банан", "вишня", "банан", "груша" # Проверка наличия элемента в списке print("банан" in fruits) + +True + print("арбуз" in fruits) + +False + # Поиск индекса первого вхождения элемента banana_index = fruits.index("банан") print(f"Индекс первого банана: {banana_index}") + +Индекс первого банана: 1 + # Подсчет количества вхождений элемента banana_count = fruits.count("банан") print(f"Количество бананов: {banana_count}") + +Количество бананов: 2 + ``` ### Сортировка и разворот @@ -222,21 +358,35 @@ print(f"Количество бананов: {banana_count}") numbers = [3, 1, 4, 1, 5, 9, 2] numbers.sort() print(f"Отсортированный список: {numbers}") + +Отсортированный список: [1, 1, 2, 3, 4, 5, 9] + # Сортировка в обратном порядке numbers.sort(reverse=True) print(f"Обратная сортировка: {numbers}") + +Обратная сортировка: [9, 5, 4, 3, 2, 1, 1] + # Если не хотите изменять исходный список, используйте sorted() original = [3, 1, 4, 1, 5] sorted_list = sorted(original) print(f"Оригинал: {original}") + +Оригинал: [3, 1, 4, 1, 5] + print(f"Отсортированная копия: {sorted_list}") + +Отсортированная копия: [1, 1, 3, 4, 5] + ``` ## Копирование списков -При работе со списками важно понимать, как работает присваивание и копирование: +Это одна из самых частых причин багов у новичков: присваивание списка **не создаёт копию**, обе переменные указывают на один и тот же объект в памяти. Если изменить список через одну переменную, изменение увидит и другая. + +![Присваивание vs копирование: при reference = original обе переменные указывают на одну коробку, при copy = original.copy() создаётся новая коробка](https://python-academy.org/static/guidePage/lists/copy-vs-reference-ru.webp "Присваивание vs копирование") ```python # Создаем список @@ -246,6 +396,9 @@ original = [1, 2, 3] reference = original reference.append(4) print(f"Оригинал после изменения ссылки: {original}") + +Оригинал после изменения ссылки: [1, 2, 3, 4] + # Правильные способы копирования списка: # 1. Метод copy() @@ -260,9 +413,51 @@ copy3 = list(original) # Проверим, что копии не связаны с оригиналом copy1.append(5) print(f"Оригинал: {original}") + +Оригинал: [1, 2, 3, 4] + print(f"Копия 1: {copy1}") + +Копия 1: [1, 2, 3, 4, 5] + +``` + +## Списковые включения + +Очень частая задача: взять один список и сделать из него другой. В лоб это несколько строк — завести пустой список, пройти циклом, добавлять по одному: + +```python +squares = [] +for i in range(10): + squares.append(i**2) +print(squares) + +[0, 1, 4, 9, 16, 25, 36, 49, 64, 81] + +``` + +Настолько частая, что для неё придумали короткую запись — **списковое включение**. Тот же результат в одну строку: + +```python +squares = [i**2 for i in range(10)] +print(squares) + +[0, 1, 4, 9, 16, 25, 36, 49, 64, 81] + +``` + +Читать её удобнее не слева направо: сначала `for i in range(10)` — откуда берём значения, потом `i**2` — что кладём в новый список. В конце можно добавить условие, и тогда попадут не все значения, а только подходящие: + +```python +even_squares = [i**2 for i in range(10) if i % 2 == 0] +print(even_squares) + +[0, 4, 16, 36, 64] + ``` +Соблазн запихнуть в одну строку всё подряд велик, но включение хорошо ровно до тех пор, пока читается с одного взгляда. Если внутрь просятся два условия и вложенный цикл — обычный `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}") + +Лучший студент: Анна с оценкой 95 + ``` ## Проверка понимания **Какой из следующих способов правильно создаст копию списка, не связанную с оригиналом?** + +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) + +P +y +t +h +o +n + ``` -В этом примере: +Читается почти как по-русски: «для каждого символа `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}") + +Число: 0 +Число: 1 +Число: 2 +Число: 3 +Число: 4 + ``` -Функция `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() + +0 1 2 + # range(start, stop): от start до stop-1 for i in range(2, 5): print(i, end=" ") print() + +2 3 4 + # 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}") + +1 3 5 7 9 + ``` ## Цикл while @@ -101,15 +95,16 @@ count = 1 while count <= 5: print(f"Цикл выполняется в {count}-й раз") count += 1 + +Цикл выполняется в 1-й раз +Цикл выполняется в 2-й раз +Цикл выполняется в 3-й раз +Цикл выполняется в 4-й раз +Цикл выполняется в 5-й раз + ``` -В этом примере: - -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 # Прерывает выполнение цикла + +Итерация 0 +Итерация 1 +Итерация 2 +Итерация 3 +Итерация 4 +Выходим из цикла! + ``` ## Управление циклами -Python предоставляет несколько инструментов для управления выполнением циклов. +Python предоставляет несколько инструментов для управления выполнением циклов. Два главных — `break` и `continue`. ### Оператор break Оператор `break` позволяет немедленно выйти из цикла, независимо от условия: +![Цикл проходит по числам 0–9, на 5 встречает break и выходит из цикла; числа 6–9 не посещаются](https://python-academy.org/static/guidePage/loops/break-ru.webp "break — выход из цикла") + ```python # Использование break для выхода из цикла for i in range(10): @@ -143,12 +148,18 @@ for i in range(10): if i == 5: print("\nДостигнуто число 5, выходим из цикла!") break + +0 1 2 3 4 5 +Достигнуто число 5, выходим из цикла! + ``` ### Оператор continue Оператор `continue` пропускает текущую итерацию и переходит к следующей: +![Цикл проходит по числам 0–5, на 3 встречает continue, перепрыгивает через 3 и продолжает с 4](https://python-academy.org/static/guidePage/loops/continue-ru.webp "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=" ") + +1 3 5 7 9 + ``` ### Блок 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") + +0 1 2 3 4 +Цикл завершился без 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() # Переход на новую строку после каждой строки таблицы + +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 + ``` > **Совет**: Вложенные циклы могут быть ресурсоемкими, особенно при большом количестве итераций. Используйте их осторожно и ищите более эффективные альтернативы для обработки больших объемов данных. -## Списковые включения (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) + +0.30000000000000004 + ``` -Для финансовых вычислений, где требуется точность, рекомендуется использовать модуль `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) + +False + ``` -Комплексные числа часто используются в инженерных расчетах, обработке сигналов и математических алгоритмах. +Если нужно сравнение с допуском, используйте `math.isclose(a, b)`: -## Арифметические операции +```python +import math +print(math.isclose(0.1 + 0.2, 0.3)) + +True + +``` -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 + +0 + +print(round(1.5)) + +2 + +print(round(2.5)) # ожидаем 3 + +2 + +print(round(3.5)) + +4 + ``` -Для работы с комплексными числами используется модуль `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) + +{1, 2, 3, 4, 5} + # Автоматическое удаление дубликатов duplicates = {1, 2, 2, 3, 3, 3, 4, 5, 5} print(duplicates) + +{1, 2, 3, 4, 5} + -# Множество с разными типами данных +# В одном множестве могут лежать разные неизменяемые типы mixed = {1, "привет", (1, 2, 3)} -print(mixed) +print(len(mixed)) # число, строка и кортеж — все три поместились + +3 + ``` > **Важно**: Нельзя создать пустое множество с помощью `{}`, так как это будет пустой словарь. Для создания пустого множества используйте `set()`. @@ -43,22 +58,23 @@ print(mixed) # Пустое множество empty_set = set() print(empty_set) + +set() + # Создание множества из списка numbers_set = set([1, 2, 2, 3, 4, 4, 5]) print(numbers_set) + +{1, 2, 3, 4, 5} + -# Создание множества из строки +# Создание множества из строки — повторяющиеся буквы схлопываются 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 буквы + +4 + ``` ## Основные операции с множествами @@ -69,98 +85,138 @@ print(squares) fruits = {"яблоко", "банан", "вишня"} print("яблоко" in fruits) + +True + print("груша" in fruits) + +False + ``` ### Добавление и удаление элементов +Порядок элементов в множестве произвольный, поэтому в примерах со строками ниже мы выводим их через `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)) # осталось на один меньше + +2 + # Очистка множества fruits.clear() print(fruits) + +set() + ``` -### Итерация по множеству +### Перебор множества + +Множество перебирают циклом `for`. Порядок при этом произвольный и может меняться от запуска к запуску — это и значит «неупорядоченное». Когда нужен предсказуемый порядок, сортируем через `sorted()`: ```python colors = {"красный", "синий", "зеленый"} -for color in colors: +for color in sorted(colors): print(color) - -# Порядок элементов не гарантирован! + +зеленый +красный +синий + ``` ## Математические операции над множествами +Три основные операции — объединение, пересечение и разность. Удобно визуализировать их диаграммами Венна: + +![Диаграммы Венна для трёх операций над множествами: объединение A | B, пересечение A & B, разность A - B](https://python-academy.org/static/guidePage/sets/set-operations-ru.webp "Операции над множествами") + ### Объединение (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) + +{1, 2, 3, 4, 5} + ``` +То же самое можно написать как `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) + +{3, 4} + ``` +То же самое можно написать как `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) + +{1, 2} + ``` +То же самое можно написать как `a.difference(b)`. + ## Операции сравнения множеств ```python @@ -170,18 +226,36 @@ c = {1, 2, 3} # Равенство множеств print(a == c) # Содержат одинаковые элементы + +True + # Подмножества print(a.issubset(b)) # Все элементы a есть в b + +True + print(a < b) # a является строгим подмножеством b + +True + # Надмножества print(b.issuperset(a)) # b содержит все элементы a + +True + print(b > a) # b является строгим надмножеством a + +True + # Проверка на отсутствие общих элементов d = {6, 7, 8} print(a.isdisjoint(d)) # Нет общих элементов + +True + ``` ## Неизменяемые множества (frozenset) @@ -192,16 +266,25 @@ print(a.isdisjoint(d)) # Нет общих элементов # Создание frozenset immutable_set = frozenset([1, 2, 3, 4]) print(immutable_set) + +frozenset({1, 2, 3, 4}) + # Попытка изменить frozenset вызывает ошибку try: immutable_set.add(5) except AttributeError as e: print(f"Ошибка: {e}") + +Ошибка: 'frozenset' object has no attribute 'add' + # frozenset можно использовать как ключ словаря или элемент другого множества normal_set = {frozenset([1, 2]), frozenset([3, 4])} -print(normal_set) +print(len(normal_set)) # два frozenset'а поместились внутрь + +2 + ``` ## Практические примеры использования множеств @@ -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) + +[1, 2, 3, 4, 5] + ``` ### 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)}") + +Только в группе 1: ['Анна', 'Мария', 'Петр'] + # Все уникальные элементы (объединение) 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])) + +True + print(are_all_unique([1, 2, 3, 3, 4])) + +False + ``` ## Ограничения и производительность @@ -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)) # число, строка и кортеж хешируемы — все три поместились + +3 + # Ошибка с изменяемыми типами данных try: invalid_set = {1, [2, 3], {"a": 1}} except TypeError as e: print(f"Ошибка: {e}") + +Ошибка: unhashable type: 'list' + ``` Можно добавлять: -- Числа (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} сек") + +Поиск в списке: 0.442 сек + +print(f"Поиск в множестве: {set_time:.5f} сек") + +Поиск в множестве: 0.00001 сек + ``` +Конкретные числа у вас получатся свои — они зависят от машины и загруженности, — но разрыв никуда не денется: десятки тысяч раз. Списку приходится перебирать элементы по одному, пока не найдёт нужный. Множество вместо перебора сразу вычисляет, где значение должно лежать, и проверяет только это место — и делает это одинаково быстро хоть на десяти элементах, хоть на миллионе. + Операции со сложностью 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) + +Hello, world + # Многострочные строки в тройных кавычках -multi_line = """Это многострочная строка. -Она может занимать несколько строк -и сохраняет все переносы строк.""" +multi_line = """Рецепт кофе: +1. Вскипятить воду +2. Залить молотый кофе +3. Подождать 4 минуты""" print(multi_line) + +Рецепт кофе: +1. Вскипятить воду +2. Залить молотый кофе +3. Подождать 4 минуты + ``` Выбор типа кавычек зависит от ситуации: -- Одинарные (`'`) и двойные (`"`) кавычки работают одинаково -- Если в строке уже есть одинарные кавычки, удобнее обернуть её в двойные, и наоборот -- Тройные кавычки (`"""` или `'''`) отлично подходят для создания многострочного текста +- Одинарные (`'`) и двойные (`"`) кавычки работают одинаково +- Если в строке уже есть одинарные кавычки, удобнее обернуть её в двойные, и наоборот +- Тройные кавычки (`"""` или `'''`) подходят для многострочного текста ### Экранирование символов @@ -43,46 +57,84 @@ print(multi_line) # Использование кавычек внутри строк с экранированием quote_inside = "Он сказал: \"Привет!\"" print(quote_inside) + +Он сказал: "Привет!" + path = "C:\\Program Files\\Python" print(path) + +C:\Program Files\Python + # Часто используемые последовательности экранирования 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) + +C:\Users\Documents\file.txt + +``` + +## Неизменяемость строк + +Строки в Python являются **неизменяемыми** (immutable). Это значит, что после создания строки её нельзя поменять «на месте»: любая операция, которая выглядит как изменение, на самом деле создаёт новую строку. + +Попытка изменить символ по индексу падает с ошибкой: +```python +language = "Python" + +try: + language[0] = "J" +except TypeError as e: + print(f"Ошибка: {e}") + +Ошибка: 'str' object does not support item assignment + ``` -Raw-строки значительно упрощают работу с путями в Windows и регулярными выражениями! +Чтобы получить «изменённую» строку, нужно собрать новую и присвоить её — либо в ту же переменную, либо в другую: -## Основные операции со строками +```python +language = "Python" +language = "J" + "ython" +print(language) + +Jython + +``` + +Это правило касается всех операций над строками: `+`, `.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)) + +6 + + +empty = "" +print(len(empty)) + +0 + +``` ### Доступ к отдельным символам -Строки в 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}") + +Первая буква: P +Вторая буква: y +Последняя буква: n + # Можно также использовать отрицательные индексы для отсчета с конца last_letter = word[-1] second_last_letter = word[-2] print(f"Последняя буква (с конца): {last_letter}") print(f"Предпоследняя буква: {second_last_letter}") + +Последняя буква (с конца): n +Предпоследняя буква: o + ``` ### Получение части строки (срезы) -Одна из самых мощных возможностей 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}") + +Первое слово: Python + second_word = message[7:] # от 7-го до конца print(f"Второе слово: {second_word}") + +Второе слово: Programming + prefix = message[:6] # от начала до 6-го (не включая 6-й) print(f"Префикс: {prefix}") + +Префикс: Python + every_second = message[::2] # каждый второй символ print(f"Каждая вторая буква: {every_second}") + +Каждая вторая буква: Pto rgamn + reversed_string = message[::-1] # строка задом наперед print(f"В обратном порядке: {reversed_string}") + +В обратном порядке: gnimmargorP nohtyP + ``` -Срезы очень гибкие: - -- Если не указать начальный индекс, срез начнется с начала строки -- Если не указать конечный индекс, срез продолжится до конца строки -- Отрицательный шаг позволяет обходить строку в обратном порядке - -## Неизменяемость строк: важная особенность! +Срезы гибкие: -Важно понимать, что строки в 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}") + +Верхний регистр: HELLO PYTHON WORLD + lower_case = text.lower() print(f"Нижний регистр: {lower_case}") + +Нижний регистр: hello python world + title_case = text.title() -print(f"Заглавные первые буквы: {title_case}") +print(f"Каждое слово с заглавной: {title_case}") + +Каждое слово с заглавной: Hello Python World + -capitalized = "hello world".capitalize() +capitalized = text.capitalize() print(f"Только первая буква заглавная: {capitalized}") + +Только первая буква заглавная: Hello python world + ``` -Эти методы часто используются для стандартизации ввода пользователя или для форматирования текста. - ### Поиск и замена в строках ```python -text = "Python - это отличный язык программирования" +text = "Python это отличный язык программирования" # Поиск подстроки position = text.find("отличный") print(f"Слово 'отличный' начинается с позиции: {position}") + +Слово 'отличный' начинается с позиции: 11 + count = text.count("о") print(f"Буква 'о' встречается {count} раза") + +Буква 'о' встречается 4 раза + # Проверка начала и конца строки starts_with = text.startswith("Python") print(f"Строка начинается с 'Python': {starts_with}") + +Строка начинается с 'Python': True + ends_with = text.endswith("!") print(f"Строка заканчивается на '!': {ends_with}") + +Строка заканчивается на '!': False + # Проверка наличия подстроки contains = "отличный" in text print(f"Строка содержит слово 'отличный': {contains}") + +Строка содержит слово 'отличный': True + # Замена подстрок new_text = text.replace("отличный", "прекрасный") print(f"Текст после замены: {new_text}") + +Текст после замены: Python это прекрасный язык программирования + ``` ### Разделение и объединение строк -Эти методы очень полезны при работе с данными: - ```python # Разделение строки на список слов -sentence = "Python - это отличный язык программирования" +sentence = "Python это отличный язык программирования" words = sentence.split() print(f"Список слов: {words}") + +Список слов: ['Python', 'это', 'отличный', 'язык', 'программирования'] + # Разделение по конкретному разделителю csv_data = "яблоко,банан,вишня" fruits = csv_data.split(",") print(f"Список фруктов: {fruits}") + +Список фруктов: ['яблоко', 'банан', 'вишня'] + # Объединение списка в строку words_to_join = ["Python", "это", "здорово"] joined_sentence = " ".join(words_to_join) print(f"Объединенное предложение: {joined_sentence}") + +Объединенное предложение: Python это здорово + # Объединение с другим разделителем path_parts = ["C:", "Users", "Username", "Documents"] path = "\\".join(path_parts) print(f"Путь к файлу: {path}") + +Путь к файлу: C:\Users\Username\Documents + ``` ### Удаление лишних символов @@ -283,80 +391,84 @@ text_with_spaces = " Python " # Удаление пробелов с обоих концов cleaned = text_with_spaces.strip() print(f"Без пробелов: '{cleaned}'") + +Без пробелов: 'Python' + left_cleaned = text_with_spaces.lstrip() print(f"Без пробелов слева: '{left_cleaned}'") + +Без пробелов слева: 'Python ' + right_cleaned = text_with_spaces.rstrip() print(f"Без пробелов справа: '{right_cleaned}'") + +Без пробелов справа: ' Python' + # Удаление конкретных символов text_with_dots = "...Python..." without_dots = text_with_dots.strip('.') print(f"Без точек: '{without_dots}'") + +Без точек: 'Python' + ``` -## Форматирование строк: красивый вывод +### Цепочки методов -Часто нам нужно вставлять значения переменных в текст. 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}'") + +'hello' + ``` -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) + +Привет, меня зовут Анна и мне 25 лет. + -# Форматирование чисел -pi = 3.1415 -formatted = "Число Пи: {:.2f}".format(pi) -print(formatted) +# Внутри {} можно вставлять любые выражения +price = 19.99 +quantity = 3 +total = f"Итого: {price * quantity:.2f} руб." +print(total) + +Итого: 59.97 руб. + ``` -### Старый стиль форматирования (%) - -Это самый старый способ, который вы можете встретить в старом коде: - -```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) + +(1, 2, 3) + # Преобразование строки в кортеж (каждый символ становится элементом) string_to_tuple = tuple("Python") print(string_to_tuple) - -# Преобразование множества в кортеж -set_to_tuple = tuple({1, 2, 3}) -print(set_to_tuple) + +('P', 'y', 't', 'h', 'o', 'n') + ``` ## Доступ к элементам кортежа @@ -91,10 +100,16 @@ fruits = ("apple", "banana", "cherry", "date", "elderberry") # Получение элементов по индексу first_fruit = fruits[0] print(first_fruit) + +apple + # Отрицательные индексы для доступа с конца кортежа last_fruit = fruits[-1] print(last_fruit) + +elderberry + ``` ### Срезы (slices) @@ -105,14 +120,23 @@ fruits = ("apple", "banana", "cherry", "date", "elderberry") # Первые три элемента first_three = fruits[:3] print(first_three) + +('apple', 'banana', 'cherry') + # От второго до четвертого middle = fruits[1:4] print(middle) + +('banana', 'cherry', 'date') + # Разворот кортежа reversed_tuple = fruits[::-1] print(reversed_tuple) + +('elderberry', 'date', 'cherry', 'banana', 'apple') + ``` ## Методы кортежей @@ -125,10 +149,16 @@ fruits = ("apple", "banana", "cherry", "banana", "date") # Подсчет количества вхождений элемента banana_count = fruits.count("banana") print(banana_count) + +2 + # Поиск индекса первого вхождения элемента banana_index = fruits.index("banana") print(banana_index) + +1 + ``` ## Операции с кортежами @@ -137,32 +167,79 @@ print(banana_index) # Узнать длину fruits = ("apple", "banana", "cherry") print(len(fruits)) + +3 + # Проверить наличие элемента print("apple" in fruits) + +True + print("mango" in fruits) + +False + # Конкатенация (объединение) кортежей more_fruits = ("pear", "orange") all_fruits = fruits + more_fruits print(all_fruits) + +('apple', 'banana', 'cherry', 'pear', 'orange') + # Повторение repeated = fruits * 2 print(repeated) + +('apple', 'banana', 'cherry', 'apple', 'banana', 'cherry') + # Распаковка a, b, c = fruits print(a, b, c) + +apple banana cherry + +``` + +## Сравнение кортежей + +Кортежи сравниваются **поэлементно**, слева направо, до первого отличия: + +```python +print((1, 2) < (1, 3)) + +True + +print((2, 0) < (1, 9)) + +False + ``` +В первом случае первые элементы равны (`1 == 1`), Python переходит ко вторым: `2 < 3`, значит весь кортеж меньше. Во втором сразу `2 > 1`, дальше можно не смотреть. + +Это позволяет одной командой отсортировать список кортежей по нескольким полям сразу: + +```python +people = [("Bob", 30), ("Anna", 25), ("Anna", 30)] +print(sorted(people)) + +[('Anna', 25), ('Anna', 30), ('Bob', 30)] + +``` + +Сортировка идёт сначала по первому элементу (имени), а при равенстве — по второму (возрасту). + ## Неизменяемость кортежей Важно понимать, что неизменяемость кортежей означает, что после создания кортежа вы не можете: -- Изменить существующие элементы -- Добавить новые элементы -- Удалить элементы +- Изменить существующие элементы +- Добавить новые элементы +- Удалить элементы ```python # Создаем кортеж @@ -177,6 +254,9 @@ coordinates = (10.5, 20.7, 30.9) # Но можно создать новый кортеж на основе существующего new_coordinates = (15.0,) + coordinates[1:] print(new_coordinates) + +(15.0, 20.7, 30.9) + ``` ### Важное замечание о вложенных изменяемых объектах @@ -188,6 +268,9 @@ tuple_with_list = (1, 2, [3, 4]) # Это работает, потому что мы изменяем список внутри кортежа tuple_with_list[2][0] = 30 print(tuple_with_list) + +(1, 2, [30, 4]) + # Но это вызовет ошибку - нельзя изменить сам кортеж # 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}") + +Name: Anna, Age: 30, Admin: True + ``` ### 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]}") + +Today is Friday + ``` ## Сравнение списков и кортежей +Технически кортежи и списки часто пересекаются по возможностям, но в 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}") + + Error: 'tuple' object does not support item assignment + ``` ## Проверка понимания **Какое из следующих утверждений о кортежах в 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. **Локальные переменные** - создаются внутри функций и доступны только внутри этих функций +- **глобальная** область: всё, что объявлено в основном теле программы; видно из любой функции +- **локальная** область: переменные, объявленные внутри функции; живут только пока функция выполняется -Представьте себе школу: глобальные переменные — это как объявления на главной доске объявлений, которые видны всем, а локальные переменные — это как записи на досках в отдельных классах, видные только ученикам конкретного класса. +Удобно представлять это как комнаты в квартире. Вещи на твоём личном столе доступны только тебе (это локальная переменная функции). А вот общее объявление на холодильнике видит вся семья (это глобальная переменная). -![Описание различий области видимости переменных](https://python-academy.org/static/guidePage/variable-scope/ru_scope.png 'Иллюстрация локальных и глобальных переменных') +![Глобальные и локальные переменные: основная программа и две функции](https://python-academy.org/static/guidePage/variable-scope/ru_scope.png "Локальные и глобальные переменные") -### Глобальные переменные +## Глобальные переменные -Глобальные переменные определяются вне функций и могут использоваться как внутри функций, так и вне их: +Глобальные переменные определяются вне функций и доступны как изнутри функций, так и снаружи: ```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() + +Сумма внутри функции: 30 + # Попытка обратиться к локальной переменной вне функции try: - print(f"Пытаемся получить доступ к result: {result}") + print(f"result: {result}") except NameError as e: print(f"Ошибка: {e}") + +Ошибка: name 'result' is not defined + ``` -Как видите, переменная `result` существует только внутри функции `calculate_sum()` и недоступна за её пределами. Это как если бы вы оставили свои вещи в гостиничном номере — они доступны, только пока вы находитесь в номере! 🏨 +Переменная `result` существует только внутри `calculate_sum()` и наружу не выходит. -### Приоритет локальных переменных +## Когда имена совпадают -Когда внутри функции создается переменная с тем же именем, что и глобальная переменная, Python отдает приоритет локальной версии: +Если внутри функции создаётся переменная с тем же именем, что и глобальная, локальная имеет приоритет. Это называется **shadowing**: локальная «заслоняет» глобальную, не трогая её: ```python x = "глобальная" def test_scope(): - x = "локальная" # Локальная переменная с тем же именем + x = "локальная" print(f"Внутри функции: x = {x}") test_scope() + +Внутри функции: x = локальная + print(f"Вне функции: x = {x}") + +Вне функции: x = глобальная + ``` -В этом примере создаются две разные переменные `x` (локальная и глобальная), -несмотря на одинаковое имя. Локальная переменная не влияет на глобальную и наоборот — они как полные тёзки, которые живут в разных городах! 👥 - -Это очень важно понимать, чтобы избежать путаницы в вашем коде! +Это две разные переменные с одинаковым именем. Они как полные тёзки, живущие в разных городах: имя совпадает, но это разные люди. -## Модификация глобальных переменных +## Изменение глобальной переменной из функции -А что если вам нужно изменить глобальную переменную внутри функции? По умолчанию Python интерпретирует присваивание значения переменной внутри функции как создание новой локальной переменной: +По умолчанию любое присваивание внутри функции Python считает созданием новой локальной переменной: ```python x = "глобальная" def modify_global(): - # Python создаст новую локальную переменную x - x = "новая локальная" # Эта переменная не меняет глобальную + x = "новая локальная" # Это локальная, глобальная не меняется print(f"Внутри функции: x = {x}") modify_global() + +Внутри функции: x = новая локальная + print(f"Вне функции: x = {x}") + +Вне функции: x = глобальная + ``` -Как видим, функция не изменила глобальную переменную. Для прямого изменения глобальной переменной внутри функции используется ключевое слово `global`. - -### Ключевое слово global - -Ключевое слово `global` указывает Python, что переменная должна быть взята из глобальной области видимости, а не создана локально: +Чтобы функция реально изменила глобальную переменную, нужно явно сказать `global x` в начале функции: ```python x = "глобальная" def modify_global(): - global x # Указываем, что используем глобальную переменную + global x x = "глобальная изменена" print(f"Внутри функции: x = {x}") modify_global() + +Внутри функции: x = глобальная изменена + print(f"Вне функции: x = {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) + +11 + +``` -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}") + +5 + 10 = 15 + ``` -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 работа с переменными максимально проста: +![Переменная как ярлык, указывающий на объект в памяти](https://python-academy.org/static/guidePage/variables/label-model-ru.webp 'name → "Иван"') -- Не нужно объявлять переменные перед использованием -- Не нужно указывать тип — 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} лет") + +В следующем году тебе будет 26 лет + ``` -## Множественное присваивание - -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}") + +x=10, y=20, z=30 + ``` -Для обмена значениями двух переменных без промежуточной используй простое присваивание: +Из той же логики растёт элегантный способ обменять значения двух переменных без временной: ```python -# Обмен значениями переменных a = 5 b = 10 -print(f"До обмена: a = {a}, b = {b}") +a, b = b, a +print(f"a = {a}, b = {b}") + +a = 10, b = 5 + +``` + +Справа сначала собирается кортеж `(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 = 10, тип: + x = "десять" # теперь x имеет тип str (строка) print(f"x = {x}, тип: {type(x)}") + +x = десять, тип: + x = [1, 2, 3] # теперь x имеет тип list (список) print(f"x = {x}, тип: {type(x)}") + +x = [1, 2, 3], тип: + ``` -Для определения текущего типа данных переменной используется встроенная функция `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) + +Алексей + ``` Три разные переменные: изменение регистра в имени переменной приводит к самостоятельной новой переменной. -## Проверка понимания +## Задания для самопроверки + +Сразу под этой статьёй есть блок **Задания для самопроверки**. В нём можно сразу попробовать применить то, что прочитали. + +### Что уже написано в редакторе + +В редакторе справа лежит заготовка: имена переменных из условия и знак `=`. Осталось вписать значения. + +![Структура задания: слева условие со значениями, справа заготовка кода, кнопка «Проверить» запускает проверку](https://python-academy.org/static/guidePage/variables/exercise-scaffold-ru.webp "Структура задания") + +Слева находится **условие**: что и какими значениями назвать. Плашки в условии (например, `John`, `25`, `True`) означают ровно те значения, которые ждут в коде. + +Кнопка **«Проверить»** запускает ваш код и сверяет результат с тестами. Нажимать её можно сколько угодно раз. + +### Что именно проверяется -Давайте проверим, насколько хорошо вы поняли особенности работы с переменными в Python: +Под редактором есть вкладки: «Результат» и рядом с ним «Тест #1», «Тест #2» и так далее. Задание засчитывается, только когда проходят все тесты. -**Какое из следующих утверждений наиболее точно описывает переменные в Python?** +Каждая вкладка с тестом показывает, что именно будет проверено: строку кода и значение, которое должно получиться. Например, «Тест #2» читает `user_age` и ждёт, что там окажется `25`. Открыть тесты можно в любой момент, в том числе до первой проверки. +![Как читать результат: у каждого теста своя вкладка, галочка означает зачёт, у непройденного теста видно полученное значение](https://python-academy.org/static/guidePage/variables/exercise-result-ru.webp "Структура результата") -## Заключение +После проверки на каждой вкладке появляется отметка: зелёная галочка у пройденных тестов, красный крестик у непройденных. Непройденный тест открывается сам, и под его кодом появляется блок «Полученный результат» со значением, которое оказалось на самом деле. Сравните его с ожидаемым, и расхождение сразу станет видно. -Отлично! Теперь вы знаете, что такое переменные в 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}") + +Алексей, Школа №1 + +print(f"{student2.name}, {student2.school}") + +Екатерина, Школа №1 + ``` -### Атрибуты класса - -> Атрибуты класса: переменные, объявленные прямо в теле класса (вне методов). Они общие для всех экземпляров. +Заметьте: `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 + +Алексей, Школа №5 + print(f"{student2.name}, {student2.school}") -# Вывод: Екатерина, Гимназия №5 + +Екатерина, Школа №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] + +[5] + s2 = Student("Иван") print(s2.grades) # ожидаем [] -# Вывод: [5] + +[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] + +[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) + +<__main__.Vector object at 0x7f9b1c2d3e50> + +print(v1 == v3) + +False + ``` -### Методы класса - -> Методы класса: получают сам класс первым параметром (обычно называется `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) + +Vector(3, 4) + +print(v1 + v2) + +Vector(4, 6) + +print(v1 == v3) + +True + ``` -Без `__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}") + +Число π: 3.141592653589793 + print(f"Число e: {math.e}") + +Число e: 2.718281828459045 + # Тригонометрические функции angle = math.pi / 4 # 45 градусов в радианах print(f"Синус 45°: {math.sin(angle):.4f}") + +Синус 45°: 0.7071 + print(f"Косинус 45°: {math.cos(angle):.4f}") + +Косинус 45°: 0.7071 + # Другие функции print(f"Факториал 5: {math.factorial(5)}") + +Факториал 5: 120 + print(f"Наибольший общий делитель 12 и 18: {math.gcd(12, 18)}") + +Наибольший общий делитель 12 и 18: 6 + ``` -### random — генерация случайных чисел +### random: генерация случайных чисел Модуль `random` предоставляет функции для генерации случайных чисел и выбора случайных элементов: @@ -50,124 +64,92 @@ import random # Генерация случайного целого числа в диапазоне print(f"Случайное число от 1 до 10: {random.randint(1, 10)}") + +Случайное число от 1 до 10: 7 + # Случайное число с плавающей точкой от 0 до 1 print(f"Случайное число от 0 до 1: {random.random():.4f}") + +Случайное число от 0 до 1: 0.3528 + # Выбор случайного элемента из последовательности fruits = ["яблоко", "банан", "апельсин", "груша"] print(f"Случайный фрукт: {random.choice(fruits)}") + +Случайный фрукт: апельсин + # Перемешивание последовательности numbers = [1, 2, 3, 4, 5] random.shuffle(numbers) print(f"Перемешанные числа: {numbers}") + +Перемешанные числа: [3, 1, 5, 2, 4] + ``` -### 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")) + +05.01.2023 + ``` -### 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()) + +[('яблоко', 3), ('банан', 2), ('вишня', 1)] + ``` -### 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) + +25 + +``` + +Строка `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()) + +Привет, меня зовут Анна, мне 25 лет. + +``` + +Заметили странность? В `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 лет. + +Привет, меня зовут Анна, мне 25 лет. + print(Person.greet(person)) -# Вывод: Привет, меня зовут Анна, мне 25 лет. + +Привет, меня зовут Анна, мне 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 + +Добавлена оценка: 5 + print(student.add_grade(4)) -# Вывод: Добавлена оценка: 4 + +Добавлена оценка: 4 + print(student.add_grade(5)) -# Вывод: Добавлена оценка: 5 + +Добавлена оценка: 5 + print(f"Средний балл: {student.average_grade()}") -# Вывод: Средний балл: 4.666666666666667 + +Средний балл: 4.7 + ``` -Метод `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 + +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 + +2026-05-20 14:30:25.123456 + # Только дата birthday = date(1990, 5, 15) print(birthday) -# Вывод: 1990-05-15 + +1990-05-15 + # Только время meeting = time(14, 30) print(meeting) -# Вывод: 14:30:00 + +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 + +2026 5 20 + print(now.weekday()) # 0 = понедельник, 6 = воскресенье -# Вывод: 2 + +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 + +2026-05-27 14:30:00 + -# Разница между моментами это timedelta +# Разница между моментами — timedelta deadline = datetime(2026, 6, 1) delta = deadline - now print(delta) -# Вывод: 11 days, 9:30:00 + +11 days, 9:30:00 + print(delta.days, delta.total_seconds()) -# Вывод: 11 985800.0 + +11 984600.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 + +20.05.2026 14:30 + print(now.strftime("%A, %d %B %Y")) -# Вывод: Wednesday, 20 May 2026 + +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 + +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) + +19:29:18 + +print(now - logged_at < timedelta(days=1)) + +True + +``` + +Срез `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 + +2026-05-20T14:30:00 + # Обратно parsed = datetime.fromisoformat("2026-05-20T14:30:00") print(parsed) -# Вывод: 2026-05-20 14:30:00 + +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 + +2026-05-20 14:30:00+00:00 + print(moscow_now) -# Вывод: 2026-05-20 17:30:00+03:00 + +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 + +1779373825.123456 + # Пауза на N секунд print("Начало") time.sleep(0.1) print("Прошло 0.1 секунды") -# Вывод: -# Начало -# Прошло 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 + +До вызова +После вызова +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 + +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 + +wrapper + ``` -В реальном коде это ломает отладку, логирование и работу 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. + +calculate_sum + ``` -Правило: пишете свой декоратор, всегда оборачивайте внутреннюю функцию через `@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) + +1000 + + +account.balance = -1_000_000 # а так — пожалуйста +print(account.balance) + +-1000000 + ``` -Класс не возражает: `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 + +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 + +1000 + account.balance = 500 # вызовет setter с проверкой print(account.balance) -# Вывод: 500 + +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)}") + +Радиус: 5, площадь: 78.54 + # area нельзя присвоить (нет сеттера) try: circle.area = 100 except AttributeError as e: print(f"Ошибка: {e}") -# Вывод: Ошибка: property 'area' of 'Circle' object has no setter + +Ошибка: 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("В файле ноль — делить нельзя") + +Файл data.txt не найден + ``` -Важно: 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}") + +Успешно! Число: 42 + ``` -Блок `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}") + +Сообщение: division by zero + ``` -## Вызов исключений +## Свои ошибки: 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 + +``` + +> Контекстный менеджер `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) + +Привет, мир! +Python - отличный язык программирования. + ``` ### Запись нескольких строк @@ -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:") + +Содержимое файла 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}") + +Текущий каталог: /home/pyodide + -```python -import os +# Создание пути +data_file = current_path / 'data' / 'info.txt' +print(f"Путь к файлу: {data_file}") + +Путь к файлу: /home/pyodide/data/info.txt + -# Текущий рабочий каталог -current_dir = os.getcwd() -print(f"Текущий каталог: {current_dir}") +# Получение имени файла и расширения +document_path = Path("path/to/document.pdf") +print(f"Имя файла: {document_path.stem}, расширение: {document_path.suffix}") + +Имя файла: document, расширение: .pdf + +``` -# Объединение путей (корректная работа с разделителями) -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 лет. + +Привет, меня зовут Анна, мне 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] + +Привет, меня зовут Анна, мне 20 лет. + +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 + +True + print(isinstance(student, Person)) -# Вывод: True - -# Отношения между классами -print(issubclass(Student, Person)) -# Вывод: True -print(issubclass(Person, Student)) -# Вывод: False + +True + +print(isinstance(person, Student)) + +False + ``` -Главное здесь: студент **является** человеком (`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)) + +25 + print(square_lambda(5)) + +25 + +``` -# Функция с несколькими аргументами -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']}") + +Bob: 92 +Alice: 85 +Charlie: 78 + +``` -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))) + +[0, 1, 2, -2, -3, 5, -8] + +``` -### 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) + +[2, 4, 6, 8, 10] + -# С лямбда-функцией (гораздо компактнее!) -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) + +[32.0, 50.0, 68.0, 86.0] + ``` -### 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) + +[2, 4, 6, 8, 10] + -# Фильтрация слов длиннее 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) + +['hello', 'howdy'] + ``` -### 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}") + +Площадь круга радиусом 5 равна 78.54 + ``` ### Импорт конкретных элементов +Забираем из модуля только нужное. Эти имена попадают прямо в вашу программу, и писать `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}") + +Квадратный корень из 16 равен 4.0 + + +y = floor(3.7) +print(f"Округление 3.7 вниз: {y}") + +Округление 3.7 вниз: 3 + ``` ### Импорт с переименованием +Тот же импорт целиком, но под коротким именем. Пригодится, когда полное имя длинное или уже занято вашей переменной: + ```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)}") + +Синус 45 градусов равен 0.7071 + ``` ## Виды библиотек в 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}") + +Случайное число: 7 + -# Модуль datetime для работы с датами и временем import datetime -# Получение текущей даты и времени current_date = datetime.datetime.now() print(f"Текущая дата и время: {current_date}") + +Текущая дата и время: 2023-07-15 14:30:45.123456 + ``` ## Поиск функций в документации @@ -103,28 +103,62 @@ print(f"Текущая дата и время: {current_date}") ### Использование функции help() +`help()` печатает всё, что модуль рассказывает о себе: назначение, список функций, описание каждой. У `math` справка длинная, поэтому ниже показано только её начало, а многоточие в конце — знак, что она на этом не кончается. + ```python -# Получение информации о модуле import math -help(math) # Выведет только начало справки, чтобы не перегружать пример +help(math) + +Help on module math: + +NAME + math + +MODULE REFERENCE + https://docs.python.org/3.12/library/math.html + + The following documentation is automatically generated from the Python + source files. It may be incomplete, incorrect or include features that + are considered implementation detail and may vary between Python + implementations. When in doubt, consult the module reference at the + location listed above. + +DESCRIPTION + This module provides access to the mathematical functions + defined by the C standard. + +FUNCTIONS + acos(x, /) + Return the arc cosine (measured in radians) of x. + + The result is between 0 and pi. +... + ``` ### Использование функции dir() ```python -# Получение списка всех доступных атрибутов и методов import random attributes = dir(random) # Выведем только первые 10 элементов для краткости print(attributes[:10]) + +['BPF', 'LOG4', 'NV_MAGICCONST', 'RECIP_BPF', 'Random', 'SG_MAGICCONST', 'SystemRandom', 'TWOPI', '_ONE', '_Sequence'] + ``` ## Проверка понимания -Давайте проверим, насколько хорошо вы усвоили тему библиотек: - **Какие из следующих способов импорта библиотек являются правильными в 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)) + +12.0 + +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()) + +Honda Civic: двигатель запущен + +print(my_car.is_running) + +False + +print(another_car.is_running) + +True + ``` -В этом примере: +Двигатель запустили только у второй машины, и `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()`, цикл не изменится ни на строчку. +![Иллюстрация: цикл for animal in animals: animal.speak() в центре, от него три стрелки к трём объектам — Dog (выдаёт Гав!), Cat (выдаёт Мяу!), Duck (выдаёт Кря!). Подпись наверху: один вызов, разное поведение](https://python-academy.org/static/guidePage/polymorphism/one-call-many-behaviors-ru.webp "Один вызов — разное поведение для разных классов") + ## Абстрактные классы -Часто хочется быть уверенным, что **все потомки точно реализовали нужный метод**. Например, базовый `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()) + +Площадь: 15, периметр: 16 +Площадь: 50.27, периметр: 25.13 + ``` Что нам дал `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) + +{ + "name": "Анна", + "age": 28, + "city": "Москва", + "languages": [ + "Python", + "JavaScript" + ] +} + -# Преобразование строки 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']}") + +Имя: Анна, языки: ['Python', 'JavaScript'] + ``` ### Запись 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']}") + + Иван: оценки [85, 90, 78] + Мария: оценки [92, 88, 95] + ``` -### Основные методы модуля 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="") + +Товар,Описание,Цена +Ноутбук,"лёгкий, тонкий",45000 + +``` -# Чтение из 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()) + +Привет, мир! Hello, world! + ``` -Как видите, попытка записать русский или китайский текст в 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' codec can't decode byte 0xd0 in position 0: ordinal not in range(128) + ``` -### Определение кодировки файла +Первый же байт кириллической буквы не укладывается в `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) + +{'Кофе': 600, 'Хлеб': 240, 'Молоко': 180} + +``` -## Что такое 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()) + +товар +Кофе 600 +Молоко 180 +Хлеб 240 + ``` -Примерный результат: - -```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()) + +товар +Кофе 600 +Хлеб 240 +Молоко 180 + ``` -### Основные команды 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 со своей папкой пакетов. Проекты перестают делить зависимости и не могут сломать друг друга. -#### Список установленных пакетов +![Два проекта, у каждого своё venv с собственными версиями пакетов: django 3.0 + requests 2.20 в проекте A, django 4.2 + requests 2.31 в проекте B](https://python-academy.org/static/guidePage/third-party-libraries/venv-isolation-ru.webp "Виртуальные окружения изолируют зависимости") -```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)) + +10 + +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) + +Привет, Иван! Тебе 25 лет. + ``` -В этом примере: - -- `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 спокойно продолжает работу. +![Иллюстрация: event loop с кодом await blocking_task() слева; стрелка run_in_executor к Thread Pool Executor справа, где выполняется time.sleep(2); внизу другие корутины продолжают работать; стрелка возврата результата](https://python-academy.org/static/guidePage/asyncio-advanced/run-in-executor-ru.webp "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()` завершается, исходная задача снова становится готовой. +![Иллюстрация: в центре Event Loop, вокруг три задачи (Task 1 — running, Task 2 — awaiting I/O, Task 3 — ready); стрелки между задачами и event loop показывают передачу управления](https://python-academy.org/static/guidePage/asyncio-basics/event-loop-ru.webp "Event loop передаёт управление между задачами на каждом await") + Важно: переключение происходит **только на `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. +![Иллюстрация: git push → GitHub Actions запускается автоматически → pytest + coverage → ветка результата passed (зелёный) или failed (красный)](https://python-academy.org/static/guidePage/coverage-and-ci/ci-pipeline-ru.webp "CI прогоняет тесты на каждом push") + ## Минимальный 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)) + +данные с site-1 +данные с site-2 +данные с site-3 + +print(f"Ушло {time.time() - start:.1f} с") + +Ушло 1.5 с + +``` + +Три запроса — полторы секунды, и процессор всё это время не делал ничего, просто ждал ответа. Дальше три разных способа уложить те же запросы в полсекунды. + ## Три понятия В разговорах о конкурентности постоянно мешают три похожих слова. Различие важное: -- **Конкурентность (concurrency)**: задачи могут переключаться между собой, создавая иллюзию одновременной работы. Один бариста за стойкой принимает заказ, ставит молоко греться, переходит к следующему клиенту, возвращается к молоку. Один исполнитель, несколько задач «в воздухе» одновременно. +- **Конкурентность (concurrency)**: задачи могут переключаться между собой, создавая иллюзию одновременной работы. Один бариста за стойкой принимает заказ, ставит молоко греться, переходит к следующему клиенту, возвращается к молоку. Один исполнитель, несколько задач «в воздухе» одновременно. -- **Параллелизм (parallelism)**: задачи выполняются **физически одновременно** на разных ядрах процессора. Несколько бариста, каждый делает свой кофе. Требует многоядерности. +- **Параллелизм (parallelism)**: задачи выполняются **физически одновременно** на разных ядрах процессора. Несколько бариста, каждый делает свой кофе. Требует многоядерности. -- **Асинхронность (asynchronicity)**: способ организации кода, при котором задача может «отложиться» в ожидании (например, ответа сервера), не блокируя весь поток. Это **способ достижения конкурентности** на одном потоке, без переключения ОС. +- **Асинхронность (asynchronicity)**: способ организации кода, при котором задача может «отложиться» в ожидании (например, ответа сервера), не блокируя весь поток. Это **способ достижения конкурентности** на одном потоке, без переключения ОС. Конкурентность это цель, параллелизм и асинхронность — два способа её достичь. +![Иллюстрация: три временные шкалы — параллелизм (две задачи одновременно на двух ядрах), конкурентность (одна шкала с чередующимися задачами), асинхронность (одна шкала, где паузы await заполняются другими задачами)](https://python-academy.org/static/guidePage/intro-concurrency-async/concurrency-models-ru.webp "Три модели конкурентности: параллелизм, переключение задач, асинхронность") + ## 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) + +True + ``` -### 2. Интеграционные тесты (Integration Tests) +Проверка вернула `True` — функция работает как ожидалось. Теперь представим, что кто-то поправил `add` и случайно задел логику: -Интеграционные тесты проверяют взаимодействие между несколькими модулями или компонентами системы. Например, как ваш модуль обработки заказов взаимодействует с модулем уведомлений или базой данных. Они помогают убедиться, что "кирпичики" правильно складываются вместе. +```python +def add(a, b): + return a + b + 1 # случайно закравшаяся ошибка -- **Пример**: Проверка того, что после успешного создания пользователя в базе данных (один модуль), система аутентификации (другой модуль) может его распознать. +print(add(2, 3) == 5) + +False + +``` -### 3. Функциональные / Сквозные тесты (Functional / End-to-End, E2E Tests) +Та же проверка мгновенно вернула `False`. Это и есть модульный тест: маленькая проверка, которая сама говорит, цела ли функция, — так и ловят регрессии. В следующей статье увидим, как оформлять такие проверки через `assert` и запускать их пачками. -Эти тесты проверяют всю систему или значительную ее часть с точки зрения пользователя. Они имитируют реальные пользовательские сценарии, проходя через все слои приложения — от пользовательского интерфейса (если он есть) до базы данных. +### Интеграционные тесты -- **Пример**: Полный сценарий регистрации нового пользователя на сайте: заполнение формы, отправка данных, получение письма с подтверждением, первый вход в систему. +Проверяют взаимодействие между несколькими модулями: например, что после создания пользователя в базе данных его распознаёт система входа. Модули могут быть исправны поодиночке и всё равно не сойтись друг с другом — это ловится именно здесь. -## Пирамида тестирования +### Сквозные тесты (E2E) -Пирамида тестирования — это модель, которая помогает визуализировать рекомендуемое соотношение различных типов тестов в проекте. +Проверяют систему целиком с точки зрения пользователя, проходя через все слои приложения. Пример — полный сценарий регистрации на сайте: заполнить форму, отправить данные, получить письмо, войти в систему. -![Описание пирамиды тестирования](https://python-academy.org/static/guidePage/intro-to-testing/ru_testing.jpg 'Описание пирамиды тестирования') +## Пирамида тестирования -**Идея пирамиды:** +Сколько каких тестов писать? Ответ обычно рисуют пирамидой: -- **Модульные тесты** составляют основу. Их должно быть больше всего, так как они быстрые, надежные и точно указывают на место ошибки. -- **Интеграционные тесты** находятся на следующем уровне. Их меньше, чем модульных, они проверяют взаимодействие компонентов. -- **Функциональные (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 с зелёной галочкой и charge.return_value = True, он передаётся аргументом в тест process_payment(service, 100) в центре, а настоящий payment_service справа перечёркнут красным (медленно, ненадёжно)](https://python-academy.org/static/guidePage/mocks-and-stubs/mock-substitution-ru.webp "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)) + +True + +print(m.charge.call_args) + +call(100) + +print(type(m.metod_kotorogo_net).__name__) + +Mock + +``` ## 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`. Никаких специальных классов, наследований, регистрации. +![Иллюстрация: папка project с файлами app.py, test_math.py, test_strings.py, helpers.py. pytest сканирует папку и находит файлы с префиксом test_, остальные пропускает](https://python-academy.org/static/guidePage/pytest-basics/pytest-discovery-ru.webp "pytest сам находит файлы test_*.py и функции test_*") + ## Запуск В терминале, в директории с тестами: @@ -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). Работает даже если тест упал. +![Иллюстрация: setup (код до yield) → тест → teardown (код после yield); три блока, соединённые стрелками](https://python-academy.org/static/guidePage/pytest-fixtures-parametrization/fixture-lifecycle-ru.webp "yield-фикстура: код до yield = setup, после yield = 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 готов + +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 готова + +Таблица 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}") + +Добавлено строк: 2 + -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 + +1 Изучить SQLAlchemy Core False +2 Написать приложение False + ``` -Доступ к колонкам по имени (`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) + +1 Изучить SQLAlchemy Core True + ``` -### 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 готовы + +Модель 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) + +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(id=1, title='Изучить ORM', completed=False) +Task(id=2, title='Написать код', completed=False) +Task(id=3, title='Сдать проект', completed=True) + + + # Одна запись по первичному ключу + task = session.get(Task, 1) + print(task) + +Task(id=1, title='Изучить ORM', completed=False) + + + # С фильтром: только невыполненные + pending = session.execute(select(Task).where(Task.completed == False)).scalars().all() + for task in pending: + print(task) + +Task(id=1, title='Изучить ORM', completed=False) +Task(id=2, title='Написать код', completed=False) + ``` -`.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) + +Task(id=1, title='Изучить ORM', completed=True) + ``` -Никаких явных `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 -# Написать код + +Анна + Изучить 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 готова + +Таблица 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) + +(1, 'Изучить SQLite') +(2, 'Сделать покупки') + ``` -`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()) + +(1, 'Изучить SQLite', 1) + ``` -`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()) + +[(1, 'Изучить SQLite')] + ``` -Та же история: без `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 у них почти одинаковый: выучили один — считайте, что выучили оба. + +![Иллюстрация: слева панель threading с одним Process, внутри которого Thread 1-4 с разделяемой памятью и иконкой GIL; справа multiprocessing с тремя отдельными процессами, у каждого свой Python и память, под ними ядра CPU; подпись: потоки делят память и GIL, процессы изолированы и истинно параллельны](https://python-academy.org/static/guidePage/threading-and-multiprocessing/threads-vs-processes-ru.webp "Потоки делят память внутри одного процесса (GIL), процессы изолированы и могут идти на разных ядрах 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) ``` +![Иллюстрация: класс TestUserStorage(unittest.TestCase), внутри последовательность шагов 1.setUp 2.test_has_name 3.tearDown 4.setUp 5.test_has_id 6.tearDown — для каждого теста setUp и tearDown запускаются заново](https://python-academy.org/static/guidePage/unittest-module/setup-teardown-ru.webp "setUp и tearDown запускаются для каждого теста") + Для каждого теста цикл `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()` Всё остальное считается **истинным**: