├── .gitignore
├── src
├── images
│ ├── cover.pdf
│ ├── cover.png
│ ├── ide_err.png
│ ├── bat_history.png
│ ├── weather_app.png
│ ├── bat_history2.png
│ ├── ide_autocomlete.png
│ ├── mypy_example01.png
│ ├── mypy_example02.png
│ ├── whereami_screenshot.png
│ └── validate_user_error_in_pycharm.png
├── weather
│ ├── run.md
│ ├── dict.md
│ ├── typeddict.md
│ ├── exceptions.md
│ ├── realize-printer.md
│ ├── literal.md
│ ├── namedtuple.md
│ ├── skeleton.md
│ ├── alias.md
│ ├── dataclass.md
│ ├── structure.md
│ ├── analysis.md
│ ├── intro.md
│ ├── realize-openweather.md
│ ├── enum.md
│ ├── realize-coordinates.md
│ └── interfaces.md
├── additional
│ ├── intro.md
│ ├── optional.md
│ ├── callable.md
│ ├── generics.md
│ ├── stub.md
│ ├── question.md
│ └── containers.md
├── resume
│ ├── course.md
│ ├── intro.md
│ └── changelog.md
├── type-hinting
│ ├── zen.md
│ ├── ide.md
│ ├── intro.md
│ ├── interp.md
│ ├── readability.md
│ └── early-errors-catching.md
├── mypy
│ └── intro.md
├── SUMMARY.md
└── intro
│ └── intro.md
├── book.toml
├── LICENSE.md
├── Makefile
├── README.md
├── Dockerfile
└── theme
└── index.hbs
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | book
3 | *.zip
4 | .obsidian
5 |
--------------------------------------------------------------------------------
/src/images/cover.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexey-goloburdin/typed-python-book/HEAD/src/images/cover.pdf
--------------------------------------------------------------------------------
/src/images/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexey-goloburdin/typed-python-book/HEAD/src/images/cover.png
--------------------------------------------------------------------------------
/src/images/ide_err.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexey-goloburdin/typed-python-book/HEAD/src/images/ide_err.png
--------------------------------------------------------------------------------
/src/images/bat_history.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexey-goloburdin/typed-python-book/HEAD/src/images/bat_history.png
--------------------------------------------------------------------------------
/src/images/weather_app.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexey-goloburdin/typed-python-book/HEAD/src/images/weather_app.png
--------------------------------------------------------------------------------
/src/images/bat_history2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexey-goloburdin/typed-python-book/HEAD/src/images/bat_history2.png
--------------------------------------------------------------------------------
/src/images/ide_autocomlete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexey-goloburdin/typed-python-book/HEAD/src/images/ide_autocomlete.png
--------------------------------------------------------------------------------
/src/images/mypy_example01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexey-goloburdin/typed-python-book/HEAD/src/images/mypy_example01.png
--------------------------------------------------------------------------------
/src/images/mypy_example02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexey-goloburdin/typed-python-book/HEAD/src/images/mypy_example02.png
--------------------------------------------------------------------------------
/src/images/whereami_screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexey-goloburdin/typed-python-book/HEAD/src/images/whereami_screenshot.png
--------------------------------------------------------------------------------
/src/images/validate_user_error_in_pycharm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexey-goloburdin/typed-python-book/HEAD/src/images/validate_user_error_in_pycharm.png
--------------------------------------------------------------------------------
/src/weather/run.md:
--------------------------------------------------------------------------------
1 | # Проверяем работу приложения
2 |
3 | Всё готово, вжух! Проверяем работу приложения:
4 |
5 | 
6 |
7 | Отлично!
8 |
--------------------------------------------------------------------------------
/src/additional/intro.md:
--------------------------------------------------------------------------------
1 | # Ещё о практических аспектах типизации
2 |
3 | Поговорим о других аспектах использования типизации в Python. Они так же
4 | важны, как и всё обсуждаемое выше, не пропускайте эту главу.
5 |
--------------------------------------------------------------------------------
/book.toml:
--------------------------------------------------------------------------------
1 | [book]
2 | authors = ["Alexey Goloburdin"]
3 | language = "ru"
4 | multilingual = false
5 | src = "src"
6 | title = "Типизированный Python для профессиональной разработки"
7 |
8 | [output.html]
9 |
10 | [output.pdf]
--------------------------------------------------------------------------------
/src/resume/course.md:
--------------------------------------------------------------------------------
1 | # Об образовательной программе Диджитализируй!
2 | Сейчас готовится к перезапуску курс [Основы компьютерных и веб-технологий с Python](https://course01.to.digital/). Курс — отличный, на Stepik Awards был признан лучший платным курсом 2021. Подписывайся, чтобы не пропустить старт. Новое издание курса будет на 30% больше и полезнее!
3 |
4 | А если ты читаешь это позднее июня 2022, то вполне вероятно, что курс уже вышел. Беги по ссылке!
5 |
6 |
--------------------------------------------------------------------------------
/src/additional/optional.md:
--------------------------------------------------------------------------------
1 | # Опциональные данные
2 |
3 | Для указания опциональных данных можно пользоваться вертикальной чертой:
4 |
5 | ```python
6 | def print_hello(name: str | None=None) -> None:
7 | print(f"hello, {name}" if name is not None else "hello anon!")
8 | ```
9 |
10 | Здесь параметр `name` функции `print_hello` является опциональным, что отражено а) в type hinting (напомню, вертикальная черта в подсказках типов означает ИЛИ) б) задано значение по умолчанию None.
11 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Исключительные права на книгу принадлежат автору Алексею Голобурдину.
2 |
3 | Разрешается:
4 |
5 | * загружать книгу, читать книгу, печатать книгу без изменений
6 | * в любом виде распространять ссылку на книгу https://t.me/t0digital/151
7 | * присылать запросы на изменения книги путём Merge Request на платформе GitHub
8 | * использование материалов книги с указанием автора и ссылки на книгу https://t.me/t0digital/151
9 |
10 | Не разрешается:
11 |
12 | * продавать книгу
13 | * распространять версии книги, отличающиеся от исходной версии, приведённой в этом GitHub репозитории и по ссылке https://t.me/t0digital/151
14 | * распростронять книгу не в виде ссылки на её актуальную версию https://t.me/t0digital/151
15 |
16 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env makefile
2 | IMAGE_NAME := tuod/book-renderer
3 | USER_ID := $$(id -u)
4 | GROUP_ID := $$(id -g)
5 | VERSION := 0.0.$(shell date '+%Y%m%d%H%M%S')
6 |
7 | all: pull render
8 |
9 | deploy: build publish
10 |
11 | pull:
12 | @docker pull ${IMAGE_NAME}:latest
13 |
14 | build:
15 | @docker build \
16 | --build-arg USER=${USER} \
17 | --build-arg USER_ID=${USER_ID} \
18 | --build-arg GROUP_ID=${GROUP_ID} \
19 | -t ${IMAGE_NAME}:latest .
20 |
21 | publish:
22 | @docker tag ${IMAGE_NAME}:latest ${IMAGE_NAME}:${VERSION}
23 | @docker push ${IMAGE_NAME}:${VERSION}
24 | @docker push ${IMAGE_NAME}:latest
25 |
26 | render:
27 | @docker run -i -v ${PWD}:/data -w /data \
28 | --user ${USER_ID}:${GROUP_ID} \
29 | --rm ${IMAGE_NAME}:latest bash -c "mdbook build"
30 |
--------------------------------------------------------------------------------
/src/type-hinting/zen.md:
--------------------------------------------------------------------------------
1 | # Zen of Python
2 |
3 | **Явное лучше неявного**. Когда у нас явно и конкретно указан тип данных — это хорошо и это соответствует дзену. Когда же нам приходится неявно домысливать, что тут, наверное, (*наверное!*) строка или `int` — это плохо и это не соответствует дзену.
4 |
5 | **Простое лучше сложного.** Подсказки типов это просто, во всяком случае точно проще, чем попытки описать тип данных в докстринге функции в том или ином формате.
6 |
7 | **Удобочитаемость важна**. Опять же, обсудили выше в примере с юзером, в котором непонятно какая структура без явной типизации.
8 |
9 | **Должен быть один — и желательно всего один — способ это сделать.** Это как раз механизм type hinting. Не надо описывать типы в докстрингах или в формате какого-то внешнего инструмента с птичьим языком. Есть родной механизм в языке программирования, который решает задачу описания типов данных.
10 |
--------------------------------------------------------------------------------
/src/additional/callable.md:
--------------------------------------------------------------------------------
1 | # Вызываемые объекты
2 |
3 | Как известно функции в Python это обычные объекты, которые можно передавать в другие функции, возвращать из других функций и т. п., поэтому для них тоже есть свой тип `Callable`:
4 |
5 | ```python
6 | from typing import Callable
7 |
8 | def mysum(a: int, b: int) -> int:
9 | return a + b
10 |
11 |
12 | def process_operation(operation: Callable[[int, int], int],
13 | a: int, b: int) -> int:
14 | return operation(a, b)
15 |
16 | print(process_operation(mysum, 1, 5)) # 6
17 | ```
18 |
19 | Здесь для аргумента `operation` функции `process_operation` проставлен тип `Callable[[int, int], int]`. Здесь `[int, int]` — это типы аргументов функции `operation`, получается, что у этой функции должно быть два аргумента и они оба должны иметь тип `int`. Последний `int` в определении типа `Callable[[int, int], int]` обозначает тип возвращаемого функцией значения.
20 |
--------------------------------------------------------------------------------
/src/resume/intro.md:
--------------------------------------------------------------------------------
1 | # Резюме
2 |
3 | Грамотное использование type hinting и осознанный выбор классов отделяет код новичка от кода растущего профессионала. Пользуйся подсказками типов, продумывай структуру твоего приложения и используемые типы данных, и тогда твои решения будут красивыми, приятно читаемыми, легко поддерживаемыми и надёжными.
4 |
5 | Ты дочитал досюда и разобрался с материалом? Отлично, молодец! Думаю, тебе стоит прислать нам резюме на почту: [join@to.digital](mailto:join@to.digital)
6 |
7 | ## Контакты
8 |
9 | У нас много хороших материалов в YouTube, если вдруг ты ещё не подписан —
10 | [YouTube-канал Диджитализируй!](https://www.youtube.com/channel/UC9MK8SybZcrHR3CUV4NMy2g)
11 |
12 | Много оперативной и текстовой полезной информации — в [Telegram-канале](https://t.me/t0digital)!
13 |
14 | Также, конечно, есть [VK-группа](https://vk.com/digitalize.team) и [Дзен](https://zen.yandex.ru/id/6235d32cb64df01e6e78c883).
15 |
--------------------------------------------------------------------------------
/src/type-hinting/ide.md:
--------------------------------------------------------------------------------
1 | # Помощь IDE при разработке
2 |
3 | Если мы пользуемся подсказками типов, то наша IDE уже на этапе написания кода будет подсказывать нам места с потенциальными ошибками, о чем говорилось выше, то есть мы на самом самом раннем этапе некоторые типы ошибок предотвратим, что хорошо, но еще и разрабатывать нам будет удобнее, потому что будет работать автодополнение и все плюшки нашей IDE. В реальности использовать код, который написан с типами, гораздо проще, приятнее, удобнее и быстрее, чем код без явно указанных типов, так как, если типы известны нашей IDE, то она всячески будет помогать нам, делать автодополнения, предлагать сразу правильные методы, выполнять проверки и так далее.
4 |
5 | > При использовании подсказок типов IDE в значительной степени помогает писать код, в полной мере работают подсказки, автодополнения и т. д. Чтобы IDE действительно помогала нам писать код, пользоваться подсказками типов просто необходимо.
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Типизированный Python для профессиональной разработки
2 |
3 |
4 |
5 | Здесь лежат md-файлы книги «Типизированный Python для профессиональной разработки».
6 | Книга собирается в [HTML](https://to.digital/typed-python) с помощью
7 | [mdbook](https://rust-lang.github.io/mdBook/).
8 |
9 | * [Книга в красивом PDF](https://t.me/t0digital/151). PDF был собран с
10 | помощью [Obsidian.md](https://obsidian.md/), в нём же происходило написание
11 | материала.
12 | * [Веб-версия](https://to.digital/typed-python/)
13 | * [Видео версия](https://www.youtube.com/watch?v=dKxiHlZvULQ)
14 |
15 | ---
16 |
17 | Самостоятельный рендер в HTML и PDF (движком chrome) можно выполнить командой ```make``` в корне репозитория. Должен быть установлен докер.
18 | На выходе в корне репозитория появится каталог book.
19 | Тестировалось на Ubuntu 22.10.
20 |
--------------------------------------------------------------------------------
/src/weather/dict.md:
--------------------------------------------------------------------------------
1 | # Обычный словарь dict
2 |
3 | Вторым вариантом структуры, которой тут можно воспользоваться — это словарь, просто обычный `dict`:
4 |
5 | ```python
6 | # Совсем плохо! Что за dict, что внутри в нём?
7 | def get_gps_coordinates() -> dict:
8 | return {"longitude": 10, "latitude": 20}
9 |
10 | # Так лучше, хотя бы прописаны типы для ключей и значений
11 | def get_gps_coordinates() -> dict[str, float]:
12 | return {"longitude": 10, "latitude": 20}
13 |
14 | coords = get_gps_coordinates()
15 | print(coords["longitude"]) # IDE не покажет опечатку в `longitude`
16 | ```
17 |
18 | Как видно, при вводе ключа словаря `longitude` IDE нам не подсказывает и нет никакой проверки на опечатки. Если мы опечатаемся в ключе словаря, то эта ошибка может дойти до рантайма и уже в рантайме упадёт ошибка `KeyError`. Хочется, чтобы IDE и статический анализатор кода вроде `mypy`, о котором поговорим позднее, помогали нам, а чтобы они нам помогали, надо чётко прописывать типы данных и `dict` это не то, что нам нужно.
19 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ubuntu:focal
2 |
3 | ENV LANG C
4 | ENV DEBIAN_FRONTEND noninteractive
5 |
6 | RUN apt update; \
7 | apt install -y \
8 | build-essential \
9 | pkg-config \
10 | libssl-dev \
11 | curl; \
12 | curl -s https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb \
13 | -o google-chrome-stable_current_amd64.deb \
14 | && dpkg -i google-chrome-stable_current_amd64.deb 2>/dev/null \
15 | || echo; \
16 | apt -fy install; \
17 | dpkg -i google-chrome-stable_current_amd64.deb; \
18 | apt-get clean all;
19 |
20 | ARG USER_ID
21 | ARG GROUP_ID
22 | ARG USER
23 |
24 | RUN groupadd -g ${GROUP_ID} docker \
25 | && adduser --uid ${USER_ID} --gid ${GROUP_ID} --disabled-password --gecos '' ${USER}
26 |
27 | USER ${USER}
28 |
29 | # Install Rust and cargo
30 | RUN curl -sSf https://sh.rustup.rs | sh -s -- -y -q --profile minimal;
31 |
32 | ENV PATH="/home/${USER}/.cargo/bin:${PATH}"
33 |
34 | RUN cargo install mdbook \
35 | && cargo install mdbook-pdf \
36 | && cargo install cargo-cache; \
37 | cargo cache -a
38 |
39 | WORKDIR /data
40 |
--------------------------------------------------------------------------------
/src/weather/typeddict.md:
--------------------------------------------------------------------------------
1 | # TypedDict
2 |
3 | Есть еще специальный типизированный `dict`. Если почему-то хочется иметь доступ к данным именно как к словарю, а не как к классу (то есть писать `coordinates["latitude"]` вместо `coordinates.latitude`), то можно воспользоваться типизированным словарём:
4 |
5 | ```python
6 | from typing import TypedDict
7 |
8 | class Coordinates(TypedDict):
9 | longitude: float
10 | latitude: float
11 |
12 | c = Coordinates(longitude=10, latitude=20)
13 | print(c["longitude"]) # Работает автодополнение в IDE
14 | print(c["longitudeRRR"]) # IDE покажет ошибку
15 | ```
16 |
17 | Я на практике не вижу большого смысла пользоваться именно типизированным словарём, но в целом можно найти какие-то сценарии для его использования. Например, уже есть много кода, который использует нашу структуру как словарь, но мы хотим добавить в эту структуру типизацию и при этом не переписывать код, который уже использует эту структуру. В таком сценарии как раз имеет смысл воспользоваться `TypedDict`.
18 |
19 | А когда мы пишем новый код, то именованные кортежи или датаклассы это наиболее часто используемые варианты. О датаклассах мы как раз сейчас и поговорим!
20 |
--------------------------------------------------------------------------------
/src/resume/changelog.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | Здесь приведены редакции этой книги и изменения в каждой. Последнюю, актуальную версию книги в формате PDF и EPUB можно скачать здесь: [t.me/t0digital/151](t.me/t0digital/151), а также читать на данном сайте.
4 |
5 | **20 марта 2023** Изменения:
6 |
7 | - Исправлены опечатки
8 |
9 | **8 июня 2022** Изменения:
10 |
11 | - Добавлена информация о TypeAlias
12 | - Исправлены найденные опечатки и ошибки
13 |
14 | **27 мая 2022 16:00** Изменения:
15 |
16 | - Обновлена глава о `Literal`
17 | - Добавлена информация о `pyright` — анализаторе кода в дополнение к `mypy`
18 | - Добавлены рекомендации в главу «Контейнеры — Iterable, Sequence, Mapping и другие»
19 | - Исправлены найденные опечатки
20 |
21 | **27 мая 2022 04:00** Изменения:
22 |
23 | - Добавилась информация о протоколах `typing.Protocol` в главе об интерфейсах
24 | - Добавилась информация о параметрах датаклассов `slots` и `frozen`
25 | - Добавлен пример в главу «Интерпретатор не проверяет подсказки типов»
26 | - Расширена глава о `Enum`, добавлено про наследование от `str`
27 | - Расширена глава «Подсказки типов нужны только в функциях?»
28 | - Исправлены найденные опечатки
29 |
30 | **26 мая 2022** Первая версия книги.
31 |
--------------------------------------------------------------------------------
/src/weather/exceptions.md:
--------------------------------------------------------------------------------
1 | # Обработка исключений
2 |
3 | В процессе работы приложения могут возникать 2 вида исключений, которые мы заложили в приложении — что-то может пойти не так с `whereami`, через который мы получаем текущие GPS-координаты. Его может не быть в системе или по какой-то причине он может выдать результат не того формата, что мы ожидаем. В таком случае возбуждается исключение `CantGetCoordinates`.
4 |
5 | Также что-то может пойти не так при запросе погоды по координатам. Тогда возбуждается исключение `ApiServiceError`. Обработаем и его. Файл `weather`:
6 |
7 | ```python
8 | #!/usr/bin/env python3.10
9 | from exceptions import ApiServiceError, CantGetCoordinates
10 | from coordinates import get_gps_coordinates
11 | from weather_api_service import get_weather
12 | from weather_formatter import format_weather
13 |
14 |
15 | def main():
16 | try:
17 | coordinates = get_gps_coordinates()
18 | except CantGetCoordinates:
19 | print("Не смог получить GPS-координаты")
20 | exit(1)
21 | try:
22 | weather = get_weather(coordinates)
23 | except ApiServiceError:
24 | print("Не смог получить погоду в API-сервиса погоды")
25 | exit(1)
26 | print(format_weather(weather))
27 |
28 |
29 | if __name__ == "__main__":
30 | main()
31 | ```
32 |
33 |
--------------------------------------------------------------------------------
/src/additional/generics.md:
--------------------------------------------------------------------------------
1 | # Дженерики
2 |
3 | Что если мы хотим написать обобщённую функцию, которая принимает на вход итерируемую структуру, то есть структуру, по которой можно итерироваться, и возвращает результат первой итерации?
4 |
5 | ```python
6 | from typing import TypeVar, Iterable
7 |
8 | T = TypeVar("T")
9 |
10 | def first(iterable: Iterable[T]) -> T | None:
11 | for element in iterable:
12 | return element
13 |
14 | print(first(["one", "two"])) # one
15 | print(first((100, 200))) # 200
16 | ```
17 |
18 | Как видите, типом данных в этой итерируемой структуре `iterable` могут быть любые данные, а наши type hinting в функции `first` говорят буквально, что функция принимает на вход итерабельную структуру данных, каждый элемент которой имеет тип `T`, и функция возвращает тот же тип `T`. Тип `T` при этом может быть любым.
19 |
20 | Это так называемые дженерики, то есть обобщённые типы.
21 |
22 | Причём имя `T` здесь это пример, он часто используется именно так, `T`, от *Type*, но название типа может быть и любым другим.
23 |
24 | Помимо дженериков можно сохранять отдельные типы для лучшей читаемости кода и подсказки читателю, что именно за данные здесь хранятся:
25 |
26 | ```python
27 | from dataclasses import dataclass
28 |
29 | Phone = str
30 |
31 | @dataclass
32 | class User:
33 | user_id: int
34 | phone: Phone
35 |
36 | def get_user_phone(user: User) -> Phone:
37 | return user.phone
38 | ```
39 |
40 | Мы уже использовали это в коде приложения погоды, когда задавали псевдоним для градусов Цельсия.
41 |
--------------------------------------------------------------------------------
/src/additional/stub.md:
--------------------------------------------------------------------------------
1 | # Stub файлы и работа с нетипизированными библиотеками
2 |
3 | Важно понимать, что type hinting работает не только для аргументов функций и возвращаемых значений. Мы можем просто создать переменную и указать ей тип:
4 |
5 | ```python
6 | book: str = "Тополек мой в красной косынке"
7 | ```
8 |
9 | В таком сценарии это избыточно — IDE и статический анализатор кода и так видят, что в переменной `book` хранится значение типа `str`. Однако в таком сценарии:
10 |
11 | ```python
12 | book: str = find_book_in_library("Тополек мой в красной косынке")
13 | ```
14 |
15 | функция поиска книги `find_book_in_library` может быть не нашей функцией, а функцией какой-то внешней библиотеки, которая не использует подсказки типов. То есть для функции может быть не проставлен тип возвращаемого значения. Чтобы IDE и статический анализатор знали, что тип данных, который будет храниться в `book`, это именно `str`, можно таким образом подсказать инструментам о верном типе. Иногда это бывает очень полезно, когда библиотека не использует подсказки типов, а возвращаемый тип данных какой-то сложный и мы хотим, чтобы IDE и mypy нам помогали анализировать наш код и типы.
16 |
17 | В то же время в Python существует механизм так называемых стаб-файлов, которые позволяют типизировать в том числе внешние библиотеки. Например, для django есть пакет в pip, который называется `django-stubs`. О стаб-файлах есть [видео](https://www.youtube.com/watch?v=KofihAoSp2U) на канале Диджитализируй!.
18 |
19 | Если вы используете нетипизированную библиотеку — можно поискать готовые стаб-файлы для неё, чтобы воспользоваться преимуществами типизированного Python.
20 |
--------------------------------------------------------------------------------
/src/weather/realize-printer.md:
--------------------------------------------------------------------------------
1 | # Реализация приложения — принтер погоды
2 |
3 | Итак, файл `weather_formatter.py`:
4 |
5 | ```python
6 | from weather_api_service import Weather
7 |
8 | def format_weather(weather: Weather) -> str:
9 | """Formats weather data in string"""
10 | return (f"{weather.city}, температура {weather.temperature}°C, "
11 | f"{weather.weather_type}\n"
12 | f"Восход: {weather.sunrise.strftime('%H:%M')}\n"
13 | f"Закат: {weather.sunset.strftime('%H:%M')}\n")
14 |
15 | if __name__ == "__main__":
16 | from datetime import datetime
17 | from weather_api_service import WeatherType
18 | print(format_weather(Weather(
19 | temperature=25,
20 | weather_type=WeatherType.CLEAR,
21 | sunrise=datetime.fromisoformat("2022-05-03 04:00:00"),
22 | sunset=datetime.fromisoformat("2022-05-03 20:25:00"),
23 | city="Moscow"
24 | )))
25 | ```
26 |
27 | Обратите внимание на печать типа погоды — `weather.weather_type`. Так можно, потому что мы отнаследовали `WeatherType` от `str` и `Enum`, а не только от `Enum`. Если бы мы отнаследовали `WeatherType` только от `Enum`, то для получения строкового значения нужно было бы напрямую обратиться к атрибуту `value`, вот так: `weather.weather_type.value` .
28 |
29 | При необходимости выводить на печать значения как-то иначе, всегда можно это реализовать в одном месте приложения. Как всегда обратите внимание, здесь реализован блок `if __name__ == "__main__":`, который позволяет тестировать код при непосредственно прямом вызове этого файла `python3.10 weather_formatter.py`. При импорте функции `format_weather` код в этом блоке выполнен не будет.
30 |
--------------------------------------------------------------------------------
/src/type-hinting/intro.md:
--------------------------------------------------------------------------------
1 | # Type hinting
2 |
3 | Что делает любая программа? Оперирует данными, то есть какие-то данные принимает на вход, какие-то данные отдаёт на выход, а внутри данные как-то трансформирует, обрабатывает и передаёт в разные функции, классы, модули и так далее. И весь вопрос в том, в каком виде и формате программа внутри себя эти данные передаёт! То есть — какие типы данных для этого используются. Часто одни и те же данные можно передавать внутри приложения строкой, списком, кортежем, словарём и массой других способов.
4 |
5 | Как все мы знаем, Python это язык с динамической типизацией. Что означает динамическая типизация? Что тип переменной определяется не в момент создания переменной, а в момент присваивания значения этой переменной. Мы можем сохранить в переменную строку, потом число, потом список, и это будет работать. Фактически интерпретатор Python сам выводит типы данных и мы их нигде не указываем, вообще не думаем об этом — просто используем то, что нам нужно в текущий момент.
6 |
7 | ```python
8 | user = "Пётр"
9 | user = 120560
10 | user = {
11 | "name": "Пётр",
12 | "username": "petr@email.com",
13 | "id": 120560
14 | }
15 | user = ("Пётр", "petr@email.com", 120560)
16 | ```
17 |
18 | Так зачем же вводить type hinting в язык с динамической типизацией? А я напомню, что в Python сейчас есть type hinting, то есть подсказки типов, они же есть в PHP, а в JS даже разработали TypeScipt, отдельный язык программирования, который является надстройкой над JS и вводит типизацию. Зачем это всё делается, для чего? Вроде скриптовые языки, не надо писать типы, думать о них, и всё прекрасно, а тут раз — и вводят какие-то типы данных.
19 |
20 | Зачем в динамически типизированном языке вводить явное указание типов?
21 |
--------------------------------------------------------------------------------
/src/weather/literal.md:
--------------------------------------------------------------------------------
1 | # Словарь с Literal ключами
2 |
3 | Плюс в описанном выше словаре ключом является строка, получается — любая строка? Но нет, в реальности не любая, а только одна из двух строк — `longitude` или `latitude`. Это можно отразить в type hinting с помощью `Literal`:
4 |
5 | ```python
6 | from typing import Literal
7 |
8 | def get_gps_coordinates() -> dict[Literal["longitude"] | Literal["latitude"], float]:
9 | return {"longitude": 10, "latitude": 20}
10 |
11 | print(
12 | get_gps_coordinates()["longitude"]
13 | )
14 |
15 | print(
16 | get_gps_coordinates()["longitudeRRR"] # Тут IDE покажет ошибку!
17 | )
18 | ```
19 |
20 | `Literal` позволяет указать не просто какой-то тип вроде `str`, а позволяет указать конкретное значение этого типа. В данном случае у нас ключом может быть либо строка со значением `"longitude"`, либо строка со значением `"latitude"`.
21 |
22 | Вот эта вертикальная черта обозначает ИЛИ, то есть или тип слева от черты, или тип справа от черты. Это синтаксис Python 3.10, в предыдущих версиях Python нужно было импортировать из `typing` специальный тип `Union`, который делал то же самое. Сейчас можно просто пользоваться вертикальной чертой для того, чтобы задать несколько возможных типов для переменной.
23 |
24 | В случае именно `Literal` для указания нескольких возможных значений можно обойтись без объединения нескольких типов через вертикальную черту и задать тип следующим образом:
25 |
26 | ```python
27 | def get_gps_coordinates() -> dict[Literal["longitude", "latitude"], float]:
28 | return {"longitude": 10, "latitude": 20}
29 | ```
30 |
31 | А если тип переменной представляет из себя, например, число или строку, тогда эти типы объединяются через вертикальную черту:
32 |
33 | ```python
34 | age: int | str
35 | ```
36 |
37 | Кстати, *literally* — по-русски означает «буквально». То есть, когда нам надо буквально задать конкретные значения в типе, мы можем это сделать при помощи типа `Literal`.
38 |
39 | В целом в таком формате использовать словарь здесь можно, но мне больше нравится вариант с именованным кортежем из этих двух вариантов.
40 |
--------------------------------------------------------------------------------
/src/weather/namedtuple.md:
--------------------------------------------------------------------------------
1 | # NamedTuple — именованный кортеж
2 |
3 | Можно воспользоваться именованным кортежем `NamedTuple`. В Python есть именованные кортежи в составе пакета `collections` и в составе `typing`. Чтобы можно было указать полям кортежа типы мы, конечно, воспользуемся импортом из `typing`:
4 |
5 | ```python
6 | from typing import NamedTuple
7 |
8 | class Coordinates(NamedTuple):
9 | latitude: float
10 | longitude: float
11 |
12 | def get_gps_coordinates() -> Coordinates:
13 | """Returns current coordinates using MacBook GPS"""
14 | return Coordinates(10, 20)
15 | ```
16 |
17 | Именованные кортежи — такие же кортежи, как и обычные `tuple`, но каждый элемент кортежа имеет имя, по которому мы можем к нему обращаться. Обращаться по имени ведь проще, чем по индексу. Индекс `0` нам мало что говорит о данных, которые лежат в этом индексе, а имя `longitude` прямо говорит нам, что тут хранится географическая долгота.
18 |
19 | Теперь пользоваться кодом проще и разночтений никаких нет:
20 |
21 | ```python
22 | coordinates = get_gps_coordinates()
23 | print(f"Широта:", coordinates.latitude) # Печать широты
24 | print(f"Долгота:", coordinates.longitudeRRR) # IDE подсветит ошибку опечатки
25 | ```
26 |
27 | В редакторе кода срабатывает автокомплит (*autocomplete*), то есть автодополнение кода. Мы начинаем набирать `coordinates.lat` и редактор подсказывает нам, что здесь должно быть `latitude`, можно просто выбрать то, что подсказывает редактор и ускорить набор текста, заодно устранив шанс возникновения опечаток:
28 |
29 | 
30 |
31 | А ещё, если по какой-то причине опечатки всё же возникли, то редактор подсветит нам места с такими проблемами:
32 |
33 |
34 | 
35 |
36 | При этом такой именованный кортеж по-прежнему является кортежем, то есть им можно пользоваться и так, с распаковкой:
37 |
38 | ```python
39 | latitude, longitude = get_gps_coordinates()
40 | ```
41 |
42 | А также, как и в случае с обычным кортежем, нельзя изменять значения элементов кортежа:
43 |
44 | ```python
45 | coordinates = get_gps_coordinates()
46 | coordinates.latitude = 10 # IDE подсветит ошибку тут
47 | ```
48 |
49 |
--------------------------------------------------------------------------------
/src/mypy/intro.md:
--------------------------------------------------------------------------------
1 | # Статические анализаторы mypy и pyright
2 |
3 | `mypy` это инструмент, который устанавливается отдельно как pip-пакет и запускается в проекте как часть тестов или CI/CD процесса. Перед сборкой и раскаткой приложения на сервер запускается проверка исходного Python-кода с `mypy` и если `mypy` находит ошибки, то процесс останавливается, разработчики исправляют найденные ошибки и процесс повторяется. Это приводит к тому, что до продакшн, то есть до рантайма и до живых пользователей соответственно ошибок долетает меньше, потому что многое выявляется на более ранних этапах.
4 |
5 | В директории проекта создадим и активируем виртуальное окружение, установим в него `mypy` и запустим проверку нашего кода:
6 |
7 | ```bash
8 | python3.10 -m venv env
9 | . ./env/bin/activate
10 | pip install mypy
11 | mypy ./weather
12 | ```
13 |
14 | 
15 |
16 | Как видим, `mypy` не нашёл проблем в нашем коде. Внесём специально ошибку в код и убедимся, что `mypy` её найдёт:
17 |
18 | 
19 |
20 | Запуск `mypy` можно встроить в процесс CI/CD, чтобы процесс разворачивания приложения на серверах не запускался, если проверки `mypy` не прошли. Таким образом до runtime не смогут дойти ошибки, связанные с некорректным использованием типов данных, и это здорово — надёжность приложения значительно возрастает!
21 |
22 | И ещё важно отметить, что используя mypy, вы можете проверять корректность своих тайп-хинтингов, которые вы указали. Пока учишься могут быть вопросы, правильно ли указан тип — вот можно указать тип у параметра функции, вызвать эту функцию с данными и посмотреть, как поведёт себя проверятор типов, встроенный в IDE, и как поведёт себя `mypy`.
23 |
24 | Помимо `mypy` пользуется популярностью анализатор `pyright`. Они работают по-разному, например, такой код валиден с точки зрения `mypy` (и с точки зрения анализатора, встроенного в PyCharm IDE), но невалиден с точки зрения `pyright`:
25 |
26 | ```python
27 | class User:
28 | def __init__(self):
29 | self.name: str = "Petr"
30 |
31 | def yo(self):
32 | self.name = {}
33 | ```
34 |
35 | Анализаторы кода продолжают развиваться и дорабатываться, они неидеальны, но проставлять подсказки типов — обязательное условие для серьёзных проектов. Анализаторы кода достаточно умны уже сейчас и станут ещё умнее в будущем.
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/type-hinting/interp.md:
--------------------------------------------------------------------------------
1 | # Интерпретатор не проверяет подсказки типов
2 |
3 | Важно! Здесь стоит отметить, что подсказки типов именно что подсказки. Они не проверяются интерпретатором Python. Они читаются людьми, это подсказка для людей, они читаются IDE, это подсказка для IDE, они могут читаться специальными средствами статического анализа кода вроде `mypy`, это подсказка для них. Но сам интерпретатор не проверяет типы. Если вы укажете type hinting для атрибута функции как `int`, а сами передадите строку — интерпретатор не свалит здесь ошибку, для него в этом не будет проблемы. Имейте это в виду.
4 |
5 | ```python
6 | def plus_two(num: int):
7 | print("мы в функции plus_two")
8 | return num + 2
9 |
10 | print(plus_two(5))
11 | # мы в функции plus_two
12 | # 7
13 | ```
14 |
15 | Мы имеем функцию `plus_two`, которая к переданному аргументу `num` типа `int` прибавляет число `2` и возвращает результат. В этом примере приведено правильное использование этой функции, она вызывается с целочисленным аргументом `5`. Программа работает корректно.
16 |
17 | Теперь вызовем функцию с неправильным типом аргумента:
18 |
19 | ```python
20 | print(plus_two("5"))
21 | # мы в функции plus_two
22 | # TypeError: can only concatenate str (not "int") to str
23 | ```
24 |
25 | С точки зрения проверки типов эта программа некорректна и на строке, где мы неправильным образом вызываем функцию `plus_two`, у нас покажется ошибка в нашем редакторе кода, также эту ошибку типов покажет и статический анализатор кода вроде `mypy`.
26 |
27 | Но интерпретатор именно эту ошибку не заметит. Он не проверяет типы, указанные в type hinting. Обратите внимание — несмотря на то, что функция вызывается явно с неправильным типом данных аргумента `num`, она всё равно запускается, так как `print("мы в функции plus_two")` срабатывает. Функция запускается и «падает» уже тогда, когда мы пытаемся сложить строку `"5"` и число `2`.
28 |
29 | Python — это по-прежнему язык с динамической типизацией, а подсказки типов являются именно что подсказками для разработчика, IDE и анализатора кода, эти подсказки призваны упростить жизнь разработчику и снизить количество ошибок в рантайме. Интерпретатор на подсказки типов внимания не обращает.
30 |
31 | **Итак, подводя промежуточный итог**: подсказки типов это очень важно и вам точно следует их изучить и ими пользоваться. А как подсказками типов пользоваться и какие есть варианты — поговорим подробнее дальше.
32 |
33 |
--------------------------------------------------------------------------------
/src/SUMMARY.md:
--------------------------------------------------------------------------------
1 |
2 | [О книге](./intro/intro.md)
3 |
4 | # Типизация в Python — зачем?
5 |
6 | - [Type hinting](./type-hinting/intro.md)
7 | - [Раннее выявление ошибок](./type-hinting/early-errors-catching.md)
8 | - [Читаемость, понятность и поддерживаемость кода](./type-hinting/readability.md)
9 | - [Помощь IDE при разработке](./type-hinting/ide.md)
10 | - [Zen of Python](./type-hinting/zen.md)
11 | - [Но — интерпретатор не проверяет подсказки типов!](./type-hinting/interp.md)
12 |
13 | # Практика
14 |
15 | - [Пишем программу погоды](./weather/intro.md)
16 | - [Накидываем структуру приложения](./weather/structure.md)
17 | - [Пишем каркас приложения](./weather/skeleton.md)
18 | - [NamedTuple — именованный кортеж](./weather/namedtuple.md)
19 | - [Обычный словарь dict](./weather/dict.md)
20 | - [Словарь с Literal ключами](./weather/literal.md)
21 | - [TypedDict](./weather/typeddict.md)
22 | - [Dataclass](./weather/dataclass.md)
23 | - [Alias для типа](./weather/alias.md)
24 | - [Enum](./weather/enum.md)
25 | - [Реализация приложения — получение GPS-координат](./weather/realize-coordinates.md)
26 | - [Реализация приложения — получение погоды с API OpenWeather](./weather/realize-openweather.md)
27 | - [Реализация приложения — принтер погоды](./weather/realize-printer.md)
28 | - [Обработка исключений](./weather/exceptions.md)
29 | - [Проверяем работу приложения](./weather/run.md)
30 | - [Использование интерфейсов и протоколов](./weather/interfaces.md)
31 | - [Анализ получившейся архитектуры кода](./weather/analysis.md)
32 |
33 | # Подробнее о типах
34 |
35 | - [Статические анализаторы mypy и pyright](./mypy/intro.md)
36 | - [Ещё о практических аспектах типизации](./additional/intro.md)
37 | - [Ещё о практических аспектах типизации](./additional/intro.md)
38 | - [Опциональные данные](./additional/optional.md)
39 | - [Контейнеры — Iterable, Sequence, Mapping и другие](./additional/containers.md)
40 | - [Дженерики](./additional/generics.md)
41 | - [Вызываемые объекты](./additional/callable.md)
42 | - [Stub-файлы и работа с нетипизированными библиотеками](./additional/stub.md)
43 | - [Подсказки типов нужны только в функциях?](./additional/question.md)
44 |
45 | # Резюме
46 |
47 | - [Резюме](./resume/intro.md)
48 | - [Об образовательной программе Диджитализируй!](./resume/course.md)
49 | - [Changelog](./resume/changelog.md)
50 |
--------------------------------------------------------------------------------
/src/weather/skeleton.md:
--------------------------------------------------------------------------------
1 | # Пишем каркас приложения
2 |
3 | Итак, накидывать функционал можно по-разному. Есть множество подходов. Например, есть TDD, *Test Driven Development*, согласно которому надо сначала написать много-много тестов, они все сначала падают с ошибками, а потом просто постепенно мы пишем код, который заставляет эти тесты постепенно работать.
4 |
5 | Мы по TDD сейчас не пойдём и тесты писать не будем, но в целом можете иметь в голове такой подход. Мы начнём постепенно с реализации, думая каждый раз, в каком слое должна лежать реализуемая сейчас функция или класс.
6 |
7 | Файл `weather`:
8 |
9 | ```python
10 | #!/usr/bin/env python3.10
11 | from coordinates import get_gps_coordinates
12 | from weather_api_service import get_weather
13 | from weather_formatter import format_weather
14 |
15 |
16 | def main():
17 | coordinates = get_gps_coordinates()
18 | weather = get_weather(coordinates)
19 | print(format_weather(weather))
20 |
21 |
22 | if __name__ == "__main__":
23 | main()
24 | ```
25 |
26 | То есть фактическая реализация логики будет инкапсулирована, то есть заключена в отдельные Python-модули `coordinates`, `weather_api_service` и `weather_formatter`, а модуль `weather` будет просто точкой входа в приложение, запускающей логику.
27 |
28 | Обратите внимание — при таком подходе у нас изначально не получится ситуации, что вся логика написана в одной каше, например, вообще без функций или в одной длинной километровой функции. Мы подумали о слоях нашего приложения, для каждого слоя создали отдельное место, в нашем случае Python-модуль, но впоследствии можно расширить до Python-пакета, и теперь будем создавать функции, в которые ляжет бизнес-логика нашего приложения.
29 |
30 | Файл `coordinates.py`:
31 |
32 | ```python
33 | def get_gps_coordinates():
34 | """Returns current coordinates using MacBook GPS"""
35 | pass
36 | ```
37 |
38 | И вот тут было бы неплохо подумать, какой формат данных вернёт эта чудесная функция. Она очевидно должна вернуть координаты. Координаты это широта и долгота, то есть два числа. Какие есть варианты?
39 |
40 | Самый простой вариант — просто tuple с двумя float числами:
41 |
42 | ```python
43 | def get_gps_coordinates() -> tuple[float, float]:
44 | """Returns current coordinates using MacBook GPS"""
45 | pass
46 | ```
47 |
48 | Так, хорошо... А широта это нулевой элемент кортежа, а долгота это первый элемент, да? Или наоборот? Насколько хорошо, что приходится додумывать или читать внутренний код функции, чтобы понять, что она возвращает? В этом нет ничего хорошего. Надо явным образом прописать тип, чтобы разночтений не было и все типы были понятны по сигнатуре функции.
49 |
--------------------------------------------------------------------------------
/src/additional/question.md:
--------------------------------------------------------------------------------
1 | # Подсказки типов нужны только в функциях?
2 |
3 | В [чате Telegram-канала](https://t.me/t0digital) задали отличный вопрос — подсказки типов имеет смысл ставить только для аргументов функций и возвращаемых значений или вообще для всех переменных?
4 |
5 | И действительно. Как лучше?
6 |
7 | В большинстве сценариев подсказок типов достаточно только для аргументов и результатов функций. Если в нашем коде все функции типизированы таким образом, то получается, что IDE и статический анализатор кода понимают тип любой переменной в коде и могут выполнять все проверки.
8 |
9 | Объявляя переменную, мы либо задаём её значение в явном виде (и тогда тип переменной равен типу значения):
10 |
11 | ```python
12 | age = 33
13 | user = User(username="Иннокентий")
14 | ```
15 |
16 | Здесь тип переменной `age` равен типу значения `33`, то есть `int`, а тип переменной `user` равен `User`.
17 |
18 | Второй вариант создания переменной — присваивание ей значения, которое возвращается функцией:
19 |
20 | ```python
21 | user = get_user_by_username("Иннокентий")
22 | ```
23 |
24 | Если все функции в нашем коде типизированы, в том числе и функция `get_user_by_username`, то и в таких сценариях тип переменной очевиден. Какой тип данных функция возвращает, такой тип данных у переменной и будет.
25 |
26 | Получается, что если все функции, используемые в коде, типизированы, то как правило нет смысла проставлять типы для обычных переменных.
27 |
28 | Однако, иногда бывает так, что мы используем внешнюю библиотеку, функции которой нетипизированы. Если функция `get_user_by_username` в примере выше это функция внешней библиотеки и она нетипизирована, то IDE и статический анализатор кода не знают, какой тип данных вернёт эта функция и потому не знают, какой тип будет у переменной `user`. Тогда можно подсказать инструментам, явно указав тип:
29 |
30 | ```python
31 | user: User = get_user_by_username("Иннокентий")
32 | ```
33 |
34 | Теперь IDE и статический анализатор будут знать тип переменной и смогут выполнять все проверки. Отлично!
35 |
36 | Ещё один сценарий, при котором полезно задать переменной тип — когда мы инициализируем переменную пустым значением, но хотим указать, данные какого конкретно типа там будут.
37 |
38 | Например, в конструкторе класса мы инициализируем атрибут с начальным значением `{}`, то есть пустой словарь, но указываем, что в этом словаре ключами будут строки, значениями числа. То есть мы уточняем тип данных, сужаем его с просто словаря до словаря с конкретным типом ключей и значений:
39 |
40 | ```python
41 | class SomeClass:
42 | def __init__(self):
43 | self._some_dict: dict[str, int] = {}
44 |
45 | def some_method(self):
46 | self._some_dict["some_key"] = 123 # Всё ок по типам
47 | self._some_dict[123] = "some_key" # Ошибка типов!
48 | ```
49 |
50 |
--------------------------------------------------------------------------------
/src/type-hinting/readability.md:
--------------------------------------------------------------------------------
1 | # Читаемость, понятность и поддерживаемость кода
2 |
3 | Вернёмся к нашей функции:
4 |
5 | ```python
6 | def validate_user(user):
7 | """Проверяет юзера, райзит исключение, если с ним что-то не так"""
8 | validate_user_on_server(user)
9 | check_username(user)
10 | check_birthday(user)
11 | ```
12 |
13 | Представим, что ты пока не очень опытный программист, который только пришел в компанию, и тебе дали задачу добавить ещё одну проверку по юзеру, чтобы валидацию проходили только пользователи, созданные вчера или раньше. Ты этот код очевидно не писал, видишь его впервые и как тут всё работает ещё не знаешь.
14 |
15 | Тут `user` — это что? Это [словарь key-value](https://docs.python.org/3/tutorial/datastructures.html#dictionaries)? Это [ORM](https://ru.wikipedia.org/wiki/ORM)-объект? Это [Pydantic](https://pydantic-docs.helpmanual.io/) модель? У этого юзера тут есть поле `created_at`, дата создания, или нет? Оно нам в нашей задаче ведь нужно будет.
16 |
17 | Как ответить на эти вопросы? Перелопачивать код, который вызывает эту нашу функцию `validate_user`. А там тоже непонятно, что в `user` лежит. Там 100500 функций выше, и где и когда там появляется `user` и что в нём лежит — большой-большой вопрос; плюс мы нашли 2 сценария, в одном наша функция вызывается с `dict`'ом, то есть `user` это словарь, а в другом сценарии функция вызывается с ORM-моделью, и возможно еще какой-то код вызывает еще как-то иначе нашу горе-функцию `validate_user`. Вот как с этим жить? Вам может понадобиться конкретно перелопатить весь проект, чтобы понять, как добавить абсолютно простейшую проверку.
18 |
19 | А если бы здесь был такой код — то все вопросы решились бы мгновенно:
20 |
21 | ```python
22 | from dataclasses import dataclass
23 | import datetime
24 |
25 | @dataclass
26 | class User:
27 | username: int
28 | created_at: datetime.datetime
29 | birthday: datetime.datetime | None
30 |
31 | def validate_user(user: User):
32 | """Проверяет юзера, райзит исключение, если с ним что-то не так"""
33 | validate_user_on_server(user)
34 | check_username(user)
35 | check_birthday(user)
36 | ```
37 |
38 | Тут понятно, чем является структура `user`. Тут очевидно, что это класс и у него есть такие-то атрибуты, дата создания юзера, юзернейм и дата рождения юзера. Причем даты хранятся тут не как строки, а как datetime, то есть все вопросы у нас мгновенно снимаются.
39 |
40 | Чтение кода значительно облегчилось. Нам понятно, что за данные в `user`, у нас больше нет вопросов, как их обработать. Если вы хотите, чтобы вашим кодом было приятно пользоваться — подсказки типов это обязательный инструмент для вас. Что код принимает на вход? Что он отдаёт на выход? На эти вопросы отвечают подсказки типов.
41 |
42 | > Подсказки типов значительно улучшают читаемость кода и облегчают его сопровождение и поддержку.
43 |
--------------------------------------------------------------------------------
/src/weather/alias.md:
--------------------------------------------------------------------------------
1 | # Alias для типа
2 |
3 | После нашего отступления о структурах продолжим накидывать каркас приложения.
4 |
5 | В `coordinates.py` оставим структуру `dataclass` с параметрами `slots` и `frozen`, потому что не предусматривается изменение координат, которые вернёт нам GPS-датчик на ноутбуке.
6 |
7 | ```python
8 | from dataclasses import dataclass
9 |
10 | @dataclass(slots=True, frozen=True)
11 | class Coordinates:
12 | longitude: float
13 | latitude: float
14 |
15 | def get_gps_coordinates() -> Coordinates:
16 | return Coordinates(longitude=10, latitude=20)
17 | ```
18 |
19 | Возвращаемое значение вставили пока, просто чтобы не ругались проверки в редакторе. Потом напишем реализацию, которая запросит координаты у команды `whereami`, распарсит её результаты и вернёт как результат функции `get_gps_coordinates`.
20 |
21 | Составим `weather_api_service.py`:
22 |
23 | ```python
24 | from coordinates import Coordinates
25 |
26 | def get_weather(coordinates: Coordinates):
27 | """Requests weather in OpenWeather API and returns it"""
28 | pass
29 | ```
30 |
31 | Так, какой тип у погоды будет возвращаться? Тут главное не смотреть на формат данных в API-сервисе, потому что сервис и формат данных в нём вторичны, первичны наши потребности. Какие данные нам нужны? Нам нужна температура за бортом, наше место, общая характеристика погоды — ясно/неясно/снег/дождь и т. п., а также мне лично ещё интересно, во сколько сегодня восход солнца и закат солнца. Вот эти данные нам нужны, их пусть функция `get_weather` и возвращает. В каком формате?
32 |
33 | Так, ну давайте думать. Просто `tuple`? Точно нет. Вообще есть мнение, что если мы хотим использовать `tuple`, то стоит использовать `NamedTuple`, потому что в нём явно данные будут названы. Поэтому возможно `NamedTuple`.
34 |
35 | Просто `dict`? Точно нет. Не будет нормальных проверок в IDE и статическом анализаторе, не будет подсказок, и читателю кода непонятно, что там внутри словаря. `TypedDict`? Лучше, но мне нравится доставать данные как атрибуты класса, а не как ключи словаря. Поэтому `TypedDict` тоже не будем брать.
36 |
37 | Может `dataclass`? Можно.
38 |
39 | Итого `NamedTuple` или `dataclass`? Оба варианта ок, можно выбрать любой вариант, я, пожалуй, тут выберу `dataclass` с параметрами `frozen` и `slots` просто потому что распаковывать структуру как кортеж нам незачем, а по памяти `dataclass` с такими параметрами даже эффективнее кортежа.
40 |
41 | ```python
42 | from dataclasses import dataclass
43 | from datetime import datetime
44 | from typing import TypeAlias
45 |
46 | from coordinates import Coordinates
47 |
48 | Celsius: TypeAlias = int
49 |
50 | @dataclass(slots=True, frozen=True)
51 | class Weather:
52 | temperature: Celsius
53 | weather_type: str # Подумаем, как хранить описание погоды
54 | sunrise: datetime
55 | sunset: datetime
56 | city: str
57 |
58 | def get_weather(coordinates: Coordinates):
59 | """Requests weather in OpenWeather API and returns it"""
60 | pass
61 | ```
62 |
63 | Обратите внимание, как я обошёлся с температурой. Можно было прописать тип напрямую `int`, но я сделал *alias*, то есть псевдоним, для `int` с названием `Celsius` и теперь понятно, что у нас температура тут будет именно в градусах Цельсия, а не Фаренгейта или Кельвина. Тип `TypeAlias` как раз предназначен для указания таких *алиасов* типов.
64 |
65 | Также, если какая-то функция будет принимать на вход или возвращать температуру, то мы тоже укажем для температуры там конкретный тип `Celsius`, а не общий непонятный `int`.
66 |
--------------------------------------------------------------------------------
/src/weather/dataclass.md:
--------------------------------------------------------------------------------
1 | # Dataclass
2 |
3 | Ещё один вариант задания структуры — `dataclass`:
4 |
5 | ```python
6 | from dataclasses import dataclass
7 |
8 | @dataclass
9 | class Coordinates:
10 | longitude: float
11 | latitude: float
12 |
13 | def get_gps_coordinates() -> Coordinates:
14 | return Coordinates(10, 20)
15 | ```
16 |
17 | Это обычный класс, это не именованный кортеж, распаковывать его как кортеж уже нельзя, и также он не ведет себя как кортеж с точки зрения изменения каждого элемента. Это обычный класс.
18 |
19 | С ним работают проверки в IDE, автодополнения — это, пожалуй, самая часто используемая структура для таких задач:
20 |
21 | ```python
22 | print(get_gps_coordinates().latitude) # Автодополнение IDE для атрибута
23 | print(get_gps_coordinates().latitudeRRR) # IDE подсветит опечатку
24 | ```
25 |
26 | Когда использовать `NamedTuple`, когда `dataclass`? Как мы поймём чуть позже, сценарий именованных кортежей — это сценарий распаковки. Когда нам нужно использовать структуру именно как кортеж, тогда стоит задать её как `NamedTuple`. В остальных сценариях имеет смысл предпочесть `dataclass`.
27 |
28 | Давайте сравним количество памяти, которое занимает в оперативке именованный кортеж и датакласс. Для того, чтобы узнать, сколько памяти занимает переменная, воспользуемся библиотекой [Pympler](https://pympler.readthedocs.io/en/latest/).
29 |
30 | ```python
31 | from dataclasses import dataclass
32 | from typing import NamedTuple
33 |
34 | from pympler import asizeof
35 |
36 | @dataclass
37 | class CoordinatesDT:
38 | longitude: float
39 | latitude: float
40 |
41 | class CoordinatesNT(NamedTuple):
42 | longitude: float
43 | latitude: float
44 |
45 |
46 | coordinates_dt = CoordinatesDT(longitude=10.0, latitude=20.0)
47 | coordinates_nt = CoordinatesNT(longitude=10.0, latitude=20.0)
48 |
49 | print("dataclass", asizeof.asized(coordinates_dt).size) # 328 bytes
50 | print("namedtuple:", asizeof.asized(coordinates_nt).size) # 104 bytes
51 | ```
52 |
53 | То есть, как видим, именованный кортеж занимает значительно меньше памяти в оперативке, чем `dataclass`, в данном примере в 3 раза. Это понятно, то как по своей сути это более простая структура данных, её нельзя менять и потому именованный кортеж можно эффективно хранить в памяти.
54 |
55 | В то же время, если мы используем `dataclass` просто как фиксированную структуру для хранения неизменяемых данных, то можно сделать и его более эффективным:
56 |
57 | ```python
58 | from dataclasses import dataclass
59 | from pympler import asizeof
60 |
61 |
62 | @dataclass(slots=True, frozen=True)
63 | class CoordinatesDT2:
64 | longitude: float
65 | latitude: float
66 |
67 | coordinates_dt2 = CoordinatesDT2(longitude=10.0, latitude=20.0)
68 | print("dataclass with frozen and slots:", asizeof.asized(coordinates_dt2).size)
69 | # dataclass with frozen and slots: 96 bytes
70 | ```
71 |
72 | Обрати внимание — такая структура неизменна, как и кортеж (благодаря флагу `frozen=True`), то есть не получится после определения экземпляра класса изменить его атрибуты. Флаг `slots=True` автоматически добавляет `__slots__` нашему датаклассу (более быстрый доступ к атрибутам и более эффективное хранение в памяти).
73 |
74 | Таким образом, как мы видим по нашему тесту, по памяти такой `dataclass` получается даже эффективнее кортежа. Кортеж можно использовать, если вам важно использовать его с распаковкой, например, таким образом:
75 |
76 | ```python
77 | latitude, longitude = coordinates_nt
78 | ```
79 |
80 | Экземпляр датакласса, очевидно, с распаковкой работать не будет, так как это не кортеж.
81 |
82 |
--------------------------------------------------------------------------------
/src/weather/structure.md:
--------------------------------------------------------------------------------
1 | # Накидываем структуру приложения
2 |
3 | Итак, первое, что надо сделать — подумать, из каких слоёв будет состоять наше приложение. Бросаться писать код сразу не надо. Давайте подумаем, что будет делать наша программа, вот просто перечислим, не думая пока о коде, функциях, классах, о том, как именно мы будем это реализовывать, а просто подумаем, что будет делать программа, из каких функциональных блоков она будет состоять.
4 |
5 | Итак, наша программа погоды должна:
6 |
7 | * уметь получать текущие координаты устройства
8 | * запрашивать по этим координатам где-то погоду, в нашем случае на OpenWeather, но потенциально было бы здорово, если бы была возможность потом подцепить и какой-то другой сервис, если понадобится
9 | * результаты работы этого погодного сервиса надо распарсить, то есть разобрать, чтобы выдернуть оттуда нужные нам данные
10 | * и, наконец, наши данные надо отобразить в терминале.
11 |
12 | Получается, 4 блока тут есть, причём второй и третий функции мы можем объединить в один верхнеуровневый слой получения погоды из внешнего сервиса. Итого мы имеем следующие слои работы приложения:
13 |
14 | 1. Слой, запускающий приложение и связывающий остальные слои
15 | 2. Получение текущих координат
16 | 3. Получение по координатам погоды
17 | 4. Печать погоды
18 |
19 | Отлично.
20 |
21 | Создаём директорию и накидываем туда слои нашего приложения. Сразу создаём структуру. У нас есть 4 слоя нашего приложения, создадим под них сразу Python-модули, чтобы логика каждого слоя лежала сразу в них.
22 |
23 | * `weather` — входная точка приложения, сделаем её исполнимым файлом без расширения `.py`, чтобы можно было запускать её без указания интерпретатора
24 | * `coordinates.py` — получение текущих GPS-координат ноутбука
25 | * `weather_api_service.py` — работа с внешним сервисом прогноза погоды
26 | * `weather_formatter.py` — форматирование погоды, то есть «сборка» строки с данными погоды (например, для последующей печати этой строки в терминале)
27 |
28 | Создаём директорию для проекта, в моём случае `weather-yt`, переходим в неё и создаём в ней пустой файл `weather`, добавляем этому файлу права на выполнение с помощью `chmod`, и затем открываем этот файл в редакторе кода, в моём случае в `nvim`:
29 |
30 | ```bash
31 | mkdir weather-yt && cd $_
32 | true > weather
33 | chmod +x weather
34 | nvim weather
35 | ```
36 |
37 | Зададим заглушку в файле `weather`:
38 |
39 | ```python
40 | #!/usr/bin/env python3.10
41 | print("Hello world")
42 | ```
43 |
44 | Первая строка называется [шебанг](https://ru.wikipedia.org/wiki/%D0%A8%D0%B5%D0%B1%D0%B0%D0%BD%D0%B3_(Unix)), при помощи чего будет запускаться текущий файл. В нашем случае файл будет запускаться с помощью интерпретатора `python3.10`. Убедитесь, что у вас есть такой интерпретатор в системе, что путь к нему добавлен в переменную окружения `PATH`, убедитесь, что `python3.10` успешно запускается. Мы будем использовать здесь возможности актуальной на сегодня версии Python — 3.10. Проверим работу приложения:
45 |
46 | ```bash
47 | ./weather
48 | ```
49 |
50 | Отлично! Точка входа в приложение готова. Теперь сделаем, чтобы она запускалась откуда угодно из системы, прокинув симлинк (ярлык) на этот исполнимый файл в `/usr/local/bin/`:
51 |
52 | ```bash
53 | sudo ln -s $(pwd)/weather /usr/local/bin/
54 | ```
55 |
56 | Отлично, теперь мы можем узнавать погоду (запускать `weather`) из любой директории в системе.
57 |
58 | Создаём остальные модули приложения:
59 |
60 | ```bash
61 | true > coordinates.py
62 | true > weather_api_service.py
63 | true > weather_formatter.py
64 | ls -l
65 | ```
66 |
67 | Итак, у нас есть структура приложения, начинаем накидывать функционал.
68 |
--------------------------------------------------------------------------------
/src/weather/analysis.md:
--------------------------------------------------------------------------------
1 | # Анализ получившейся архитектуры кода
2 |
3 | Давайте посмотрим свежим взглядом на получившуюся архитектуру кода.
4 |
5 | Мы имеем 4 слоя приложения:
6 |
7 | 1. Модуль `weather`, запускающий приложение и связывающий остальные слои. Важно обратить внимание: этот файл не содержит никакой логики реализации, никакой бизнес-логики. Это точка входа в приложение. Она не знает ничего о деталях реализации всех остальных нижележащих слоёв приложения.
8 | 2. Модуль `coordinates` отвечает за получение координат из внешней команды `whereami`. Сюда инкапсулирована вся логика по работе с координатами. Эта логика ничего не знает о том, для чего эти координаты будут использованы затем в приложении. Модуль определяет структуру данных для хранения и передачи в приложение координат.
9 |
10 | Если нам понадобится получать координаты откуда-то иначе — мы перепишем логику этого модуля, никак не затронув все остальные модули приложения. Связь этого модуля с остальными — слабая, и это хорошо.
11 | 3. Модуль `weather_api_service` инкапсулирует в себе логику получения погоды по координатам. Он не знает, откуда были получены координаты, поступившие в этот модуль. Он не знает, как будет использоваться погода дальше в приложении. В этом модуле определена структура для хранения и передачи данных погоды в приложение.
12 |
13 | Если нам понадобится получать погоду в другом API-сервисе — мы заменим логику этого модуля и это никак не затронет остальные модули приложения. Связь этого модуля с остальными — слабая, и это хорошо.
14 | 4. Модуль `weather_formatter` отвечает за преобразование погоды в строку. Он ничего не знает о том, откуда погода была получена, была ли она получена по координатам GPS или по названию населённого пункта или как-то иначе, он не знает ничего кроме того, как преобразовать данные погоды в строку, всё. Связь этого модуля с остальными слабая, и это хорошо. В любой момент мы можем изменить логику форматирования данных погоды (добавив в неё иконки погоды, например), никак не затронув при этом все остальные модули приложения.
15 | 5. Модуль `history` инкапсулирует в себе логику по сохранению истории погоды. Этот модуль также независим от остальных модулей. Более того, реализована гибкая схема смены хранилища на любое другое через механизм интерфейсов. Ответственность за то, какое хранилище будет использовано для сохранения данных, лежит вовне этого модуля. Можно, например, данные погоды днём сохранять в текстовый плоский txt-файл, а данные ночной погоды — в JSON. Для этого не придётся ничего менять в самом модуле `history`. Чем меньше поводов менять код какого-то модуля, класса, функции — тем лучше.
16 |
17 | Получается, что мы реализовали всё приложение в виде слабозависимых друг от друга модулей. При этом эти модули могут использоваться и в составе других приложений, они *reusable*, то есть переиспользуемые. Скажем, модуль получения GPS-координат может использоваться в программе вычисления расстояния от текущей точки, где мы находимся, до Рима. Почему нет. Для этого не понадобится изменять этот модуль. Отлично!
18 |
19 | Если мы откроем код любого модуля, любой функции — нам сразу станет понятно, какие данные принимаются на вход и какие возвращаются на выход, причём понятно максимально точно. Это:
20 |
21 | 1. Облегчает чтение кода — все типы данных в явном виде и максимально конкретно прописаны, не надо их предугадывать.
22 | 2. Гарантируется отсутствие ошибок использования типов — IDE подсветит, если мы что-то используем не так, как нужно; также на ошибки укажет инструмент статического анализа вроде `mypy`, о котором мы поговорим ниже.
23 | 3. IDE поможет писать код всем, использующим наши разработанные модули. Будет работать автодополнение по полям и методам классов с учетом типов, которые мы указали.
24 |
25 | Финальный вариант исходного кода программы размещён на Github: https://github.com/alexey-goloburdin/weather
26 |
27 |
--------------------------------------------------------------------------------
/src/weather/intro.md:
--------------------------------------------------------------------------------
1 | # Пишем программу погоды
2 |
3 | Итак, давайте напишем консольную программу, которая будет показывать текущую погоду по нашим координатам. Чтоб не по IP-адресу как-то пытаться неточно вычислять местоположение, а именно по текущим реальным GPS-координатам. Чтобы программа показывала температуру за бортом, идёт ли там дождь/снег и время восхода-заката солнца. Для съёмки видео важно понимать, во сколько сегодня восход или закат солнца, чтобы ориентироваться на освещённость за окном.
4 |
5 | Итак, в первую очередь, нам надо понять, как получить доступ к текущим координатам, есть ли такая возможность. Решение будет для MacBook, [гуглим](https://www.google.com/search?q=python+mac+get+gps+coordinates): `python mac get gps coordinates`. [Первая ссылка](https://stackoverflow.com/questions/42831672/python-get-gps-location-on-macos) говорит о программе whereami, которая печатает текущие координаты в консоль
6 |
7 | ```bash
8 | whereami
9 | ```
10 |
11 | 
12 |
13 | Отлично, теперь мы можем получать наши текущие координаты, отправить их в какой-то сервис погоды через API, получить оттуда погоду и отобразить её.
14 |
15 | Команда работает по аналогии с `whoami` — та показывает, «кто я», а вот команда `whereami` показывает, «где я»:).
16 |
17 | Давайте найдём какой-то сервис погоды. Поисковый запрос `API прогноз погоды` привёл меня на проект [OpenWeather](https://home.openweathermap.org/). У них есть бесплатный доступ. Еще есть Яндекс погода в России, Gismeteo, но там, насколько я понял, для получения API-ключа надо куда-то писать на почту, для наших целей это слишком долго. Воспользуемся OpenWeather.
18 |
19 | Запрос на получение погоды по примерно моим координатам:
20 |
21 | ```bash
22 | http https://api.openweathermap.org/data/2.5/weather\?lat\=55.7\&lon\=37.5\&appid\=7549b3ff11a7b2f3cd25b56d21c83c6a\&lang\=ru\&units\=metric
23 | ```
24 |
25 | `httpie` — это удобная утилита работы с веб-сервисами, такая вариация на тему `curl`, можно установить на Mac OS с помощью [brew](https://brew.sh/index_ru) командой `brew install httpie`. Она выводит в раскрашенном виде JSON, например, что удобно.
26 |
27 | API-ключ, использующийся в запросе, получается сразу после регистрации, но активируется в течение, может быть, минут десяти.
28 |
29 | Результат запрос:
30 |
31 | ```json
32 | {
33 | "base": "stations",
34 | "clouds": {
35 | "all": 61
36 | },
37 | "cod": 200,
38 | "coord": {
39 | "lat": 55.7,
40 | "lon": 37.5
41 | },
42 | "dt": 1651521003,
43 | "id": 529334,
44 | "main": {
45 | "feels_like": 9.26,
46 | "grnd_level": 993,
47 | "humidity": 74,
48 | "pressure": 1013,
49 | "sea_level": 1013,
50 | "temp": 10.25,
51 | "temp_max": 12.01,
52 | "temp_min": 8.55
53 | },
54 | "name": "Moscow",
55 | "sys": {
56 | "country": "RU",
57 | "id": 47754,
58 | "sunrise": 1651455877,
59 | "sunset": 1651511306,
60 | "type": 2
61 | },
62 | "timezone": 10800,
63 | "visibility": 10000,
64 | "weather": [
65 | {
66 | "description": "облачно с прояснениями",
67 | "icon": "04n",
68 | "id": 803,
69 | "main": "Clouds"
70 | }
71 | ],
72 | "wind": {
73 | "deg": 180,
74 | "gust": 8.08,
75 | "speed": 2.69
76 | }
77 | }
78 | ```
79 |
80 | Так, отлично, мы умеем находить текущие координаты и умеем по ним получать температуру, состояние погоды — дождь/снег/облака, а также получать время восхода и заката солнца.
81 |
82 | Давайте напишем программу для этого! Чтобы запускаешь её и она писала наше местоположение и выводила эти данные — температуру, характеристику погоды (снег/облака/туман) и время восхода заката солнца.
83 |
--------------------------------------------------------------------------------
/src/intro/intro.md:
--------------------------------------------------------------------------------
1 | # Типизированный Python для профессиональной разработки
2 |
3 | [](https://t.me/t0digital/151)
4 |
5 | *Алексей Голобурдин,
6 | команда Диджитализируй!*
7 |
8 | *обложка — [Васильев Никита](https://vk.com/vasiliev.narisoval), nikita.vasiliev@math.msu.ru*
9 |
10 | > **PDF версию книги** можно бесплатно скачать здесь: [t.me/t0digital/151](https://t.me/t0digital/151)
11 |
12 | Цель этой книги — помочь тебе научиться писать более красивые, надёжные и легко сопровождаемые программы на Python. То, о чём мы здесь будем говорить, это не начальный уровень владения языком, предполагается, что ты уже минимально умеешь программировать, но хочешь научиться делать это лучше.
13 |
14 | И это — отличная цель, к которой мы вместе будем двигаться на протяжении ближайших часов!
15 |
16 | Этот материал есть также в видео формате на моём YouTube — [«Диджитализируй!»](https://www.youtube.com/watch?v=dKxiHlZvULQ).
17 |
18 |