Типизированный_Python_для_профессиональной_разработки. Профессиональной разработки
![]()
|
Интерпретатор не проверяет подсказки типов Важно! Здесь стоит отметить, что подсказки типов именно что подсказки. Они не проверяются интерпретатором Python. Они читаются людьми, это подсказка для людей, они читаются IDE, это подсказка для IDE, они могут читаться специальными средствами статического анализа кода вроде mypy , это подсказка для них. Носам интерпретатор не проверяет типы. Если вы укажете type hinting для атрибута функции как int , асами передадите строку — интерпретатор не свалит здесь ошибку, для него в этом не будет проблемы. Имейте это ввиду Мы имеем функцию plus_two , которая к переданному аргументу num типа int прибавляет число 2 и возвращает результат. В этом примере приведено правильное использование этой функции, она вызывается с целочисленным аргументом Программа работает корректно. Теперь вызовем функцию с неправильным типом аргумента: С точки зрения проверки типов эта программа некорректна и на строке, где мы неправильным образом вызываем функцию plus_two , у нас покажется ошибка в нашем редакторе кода, также эту ошибку типов покажет и статический анализатор кода вроде Но интерпретатор именно эту ошибку не заметит. Он не проверяет типы, указанные в type hinting. Обратите внимание — несмотря на то, что функция вызывается явно с неправильным типом данных аргумента num , она всё равно запускается, так как мы в функции plus_two") срабатывает. Функция запускается и падает уже тогда, когда мы пытаемся сложить строку "5" и число 2 Python — это по-прежнему язык с динамической типизацией, а подсказки типов являются именно что подсказками для разработчика, IDE и анализатора кода, эти подсказки призваны упростить жизнь разработчику и снизить количество ошибок в рантайме. Интерпретатор на подсказки типов внимания не обращает. Итак, подводя промежуточный итог подсказки типов это очень важно ивам точно следует их изучить и ими пользоваться. А как подсказками типов пользоваться и какие есть варианты — поговорим подробнее дальше мы в функции plus_two" ) return num + 2 ( plus_two ( 5 )) # мы в функции plus_two # 7 ( plus_two ( "5" )) # мы в функции plus_two # TypeError: can only concatenate str (not "int") to str Пишем программу погоды Подготовка Итак, давайте напишем консольную программу, которая будет показывать текущую погоду по нашим координатам. Чтоб не по IP адресу как-то пытаться неточно вычислять местоположение, а именно по текущим реальным GPS координатам. Чтобы программа показывала температуру за бортом, идёт литам дождь/снег и время восхода-заказа солнца. Для съёмки видео важно понимать, во сколько сегодня восходили закат солнца, чтобы ориентироваться на освещённость за окном. Итак, в первую очередь, нам надо понять, как получить доступ к текущим координатам, есть ли такая возможность. Решение будет для MacBook, гуглим : python mac get gps Первая ссылка говорит о программе которая печатает текущие координаты в консоль whereami Отлично, теперь мы можем получать наши текущие координаты, отправить их в какой-то сервис погоды через API, получить оттуда погоду и отобразить её. Команда работает по аналогии ста показывает, кто я, а вот команда whereami показывает, где я»:). Давайте найдём какой-то сервис погоды. Поисковый запрос API прогноз погоды привёл меня на проект OpenWeather . У них есть бесплатный доступ. Еще есть Яндекс погода в России, Gismeteo, нотам, насколько я понял, для получения API ключа надо куда-то писать на почту, для наших целей это слишком долго. Воспользуемся OpenWeather. Запрос на получение погоды по примерно моим координатам — это удобная утилита работы с веб-сервисами, такая вариация на тему можно установить нас помощью brew командой brew install httpie . Она выводит в раскрашенном виде JSON, например, что удобно ключ, использующийся в запросе, получается сразу после регистрации, но активируется в течение, может быть, минут десяти. Результат запрос https://api.openweathermap.org/data/2.5/weather \ ? lat \ = 55.7 \ & lon \ = 37.5 \ & appid \ = 7549b3ff11a7b2f3cd25b56d21c83c6a \ & lang \ = ru \ & unit s \ = metric { "base" : "stations" , "clouds" : { "all" : 61 }, "cod" : 200 , "coord" : { "lat" : 55.7 , "lon" : 37.5 }, "dt" : 1651521003 , "id" : 529334 , "main" : { "feels_like" : 9.26 , "grnd_level" : 993 , "humidity" : 74 , "pressure" : 1013 , Так, отлично, мы умеем находить текущие координаты и умеем по ним получать температуру, состояние погоды — дождь/снег/облака, а также получать время восхода и заката солнца. Давайте напишем программу для этого Чтобы запускаешь её иона писала наше местоположение и выводила эти данные — температуру, характеристику погоды (снег/облака/туман) и время восхода заката солнца. Накидываем структуру приложения Итак, первое, что надо сделать — подумать, из каких слоёв будет состоять наше приложение. Бросаться писать код сразу не надо. Давайте подумаем, что будет делать наша программа, вот просто перечислим, не думая пока о коде, функциях, "sea_level" : 1013 , "temp" : 10.25 , "temp_max" : 12.01 , "temp_min" : 8.55 }, "name" : "Moscow" , "sys" : { "country" : "RU" , "id" : 47754 , "sunrise" : 1651455877 , "sunset" : 1651511306 , "type" : 2 }, "timezone" : 10800 , "visibility" : 10000 , "weather" : [ { "description" : "облачно с прояснениями" , "icon" : "04n" , "id" : 803 , , справочник https : //openweathermap.org/weather-conditions#Weather-Condition-Codes-2 --> "main" : "Clouds" } ], "wind" : { "deg" : 180 , "gust" : 8.08 , "speed" : 2.69 } } классах, о том, как именно мы будем это реализовывать, а просто подумаем, что будет делать программа, из каких функциональных блоков она будет состоять. Итак, наша программа погоды должна: уметь получать текущие координаты устройства запрашивать по этим координатам где-то погоду, в нашем случае на, но потенциально было бы здорово, если бы была возможность потом подцепить и какой-то другой сервис, если понадобится результаты работы этого погодного сервиса надо распарсить, то есть разобрать, чтобы выдернуть оттуда нужные нам данные и, наконец, наши данные надо отобразить в терминале. Получается, 4 блока тут есть, причём второй и третий функции мы можем объединить в один верхнеуровневый слой получения погоды из внешнего сервиса. Итого мы имеем следующие слои работы приложения Слой, запускающий приложение и связывающий остальные слои Получение текущих координат Получение по координатам погоды Печать погоды Отлично. Создаём директорию и накидываем туда слои нашего приложения. Сразу создаём структуру. У насесть слоя нашего приложения, создадим под них сразу Python модули, чтобы логика каждого слоя лежала сразу в них — входная точка приложения, сделаем её исполнимым файлом без расширения .py , чтобы можно было запускать её без указания интерпретатора gps_coordinates.py — получение текущих GPS координат ноутбука weather_api_service.py — работа с внешним сервисом прогноза погоды weather_formatter.py — форматирование погоды, то есть сборка строки сданными погоды (например, для последующей печати этой строки в терминале) Создаём директорию для проекта, в моём случае weather-yt , переходим вне и создаём в ней пустой файл weather , добавляем этому файлу права на выполнение с помощью chmod , и затем открываем этот файл в редакторе кода, в моём случаев Зададим заглушку в файле Первая строка называется шебанг , при помощи чего будет запускаться текущий файл. В нашем случае файл будет запускаться с помощью интерпретатора python3.10 . Убедитесь, что у вас есть такой интерпретатор в системе, что путь к нему добавлен в переменную окружения PATH , убедитесь, что python3.10 успешно запускается. Мы будем использовать здесь возможности актуальной на сегодня версии Python — 3.10. Проверим работу приложения: Отлично! Точка входа в приложение готова. Теперь сделаем, чтобы она запускалась откуда угодно из системы, прокинув симлинк (ярлык) на этот исполнимый файл в /usr/local/bin/ : Отлично, теперь мы можем узнавать погоду (запускать weather ) из любой директории в системе. Создаём остальные модули приложения: Итак, у насесть структура приложения, начинаем накидывать функционал weather-yt && cd $_ true > weather chmod +x weather nvim weather #!/usr/bin/env python3.10 ( "Hello world" ) ./weather sudo ln -s $( pwd ) /weather /usr/local/bin/ true > gps_coordinates.py true > weather_api_service.py true > weather_formatter.py ls -l Пишем каркас приложения Итак, накидывать функционал можно по-разному. Есть множество подходов. Например, есть TDD, Test Driven Development, согласно которому надо сначала написать много-много тестов, они все сначала падают с ошибками, а потом просто постепенно мы пишем код, который заставляет эти тесты постепенно работать. Мы посейчас не пойдём и тесты писать не будем, нов целом можете иметь в голове такой подход. Мы начнём постепенно с реализации, думая каждый разв каком слое должна лежать реализуемая сейчас функция или класс. Файл То есть фактическая реализация логики будет инкапсулирована, то есть заключена в отдельные Python модули gps_coordinates , weather_api_service и weather_formatter , а модуль weather будет просто точкой входа в приложение, запускающей логику. Обратите внимание — при таком подходе у нас изначально не получится ситуации, что вся логика написана водной каше, например, вообще без функций или водной длинной километровой функции. Мы подумали о слоях нашего приложения, для каждого слоя создали отдельное место, в нашем случае Python модуль, но впоследствии можно расширить до Python пакета, и теперь будем создавать функции, в которые ляжет бизнес-логика нашего приложения python3.10 from coordinates import get_coordinates from weather_api_service import get_weather from weather_formatter import format_weather def main (): coordinates = get_coordinates () weather = get_weather ( coordinates ) ( format_weather ( weather )) if __name__ == "__main__" : main () Файл И вот тут было бы неплохо подумать, какой формат данных вернёт эта чудесная функция. Она очевидно должна вернуть координаты. Координаты это широта и долгота, то есть два числа. Какие есть варианты? Самый простой вариант — просто tuple с двумя float числами: Так, хорошо. А широта это нулевой элемент кортежа, а долгота это первый элемент, да? Или наоборот Насколько хорошо, что приходится додумывать или читать внутренний код функции, чтобы понять, что она возвращает В этом нет ничего хорошего. Надо явным образом прописать тип, чтобы разночтений не было и все типы были понятны по сигнатуре функции — именованный кортеж Можно воспользоваться именованным кортежем NamedTuple . Весть именованные кортежи в составе пакета collections ив составе typing . Чтобы можно было указать полям кортежа типы мы, конечно, воспользуемся импортом из Именованные кортежи — такие же кортежи, как и обычные tuple , но каждый элемент кортежа имеет имя, по которому мы можем к нему обращаться get_gps_coordinates (): """Returns current coordinates using MacBook GPS""" pass def get_gps_coordinates () -> tuple [ float , float ]: """Returns current coordinates using MacBook GPS""" pass from typing import NamedTuple class Coordinates ( NamedTuple ): latitude : float longitude : float def get_gps_coordinates () -> Coordinates : """Returns current coordinates using MacBook GPS""" return Coordinates ( 10 , 20 ) Обращаться по имени ведь проще, чем по индексу. Индекс 0 нам мало что говорит о данных, которые лежат в этом индексе, а имя longitude прямо говорит нам, что тут хранится географическая долгота. Теперь пользоваться кодом проще и разночтений никаких нет: В редакторе кода срабатывает автокомплит (autocomplete), то есть автодополнение кода. Мы начинаем набирать coordinates.lat и редактор подсказывает нам, что здесь должно быть latitude , можно просто выбрать то, что подсказывает редактор и ускорить набор текста, заодно устранив шанс возникновения опечаток: А ещё, если по какой-то причине опечатки всё же возникли, то редактор подсветит нам места с такими проблемами: При этом такой именованный кортеж по-прежнему является кортежем, то есть им можно пользоваться итак, с распаковкой: А также, как ив случае с обычным кортежем, нельзя изменять значения элементов кортежа = Широта coordinates latitude ) # Печать широты Долгота coordinates longitudeRRR ) # IDE подсветит ошибку опечатки latitude , longitude = get_gps_coordinates () coordinates = get_gps_coordinates () coordinates latitude = 10 # IDE подсветит ошибку тут Обычный словарь Вторым вариантом структуры, которой тут можно воспользоваться — это словарь, просто обычный Как видно, при вводе ключа словаря longitude IDE нам не подсказывает и нет никакой проверки на опечатки. Если мы опечатаемся в ключе словаря, то эта ошибка может дойти до рантайма и уже в рантайме упадёт ошибка KeyError . Хочется, чтобы и статический анализатор кода вроде mypy , о котором поговорим позднее, помогали нам, а чтобы они нам помогали, надо чётко прописывать типы данных и dict это не то, что нам нужно. Словарь с Literal ключами Плюс в описанном выше словаре ключом является строка, получается — любая строка Нонет, в реальности нелюбая, а только одна из двух строк — longitude или latitude . Это можно отразить в type hinting с помощью Literal : # Совсем плохо Что за dict, что внутри в нм get_gps_coordinates () -> dict : return { "longitude" : 10 , "latitude" : 20 } # Так лучше, хотя бы прописаны типы для ключей и значений def get_gps_coordinates () -> dict [ str , float ]: return { "longitude" : 10 , "latitude" : 20 } coords = get_gps_coordinates () ( coords [ "longitude" ]) # IDE не покажет опечатку в `longitude` from typing import Literal def get_gps_coordinates () -> dict [ Literal [ "longitude" ] | Literal [ "latitude" ], float ]: return { "longitude" : 10 , "latitude" : 20 } ( get_gps_coordinates [ "longitude" ] ) ( get_gps_coordinates [ "longitudeRRR" ] # Тут IDE покажет ошибку Literal позволяет указать непросто какой-то тип вроде str , а позволяет указать конкретное значение этого типа. В данном случае у нас ключом может быть либо строка со значением "longitude" , либо строка со значением "Вот эта вертикальная черта обозначает ИЛИ, то есть или тип слева от черты, или тип справа от черты. Это синтаксис Python 3.10, в предыдущих версиях Python нужно было импортировать из typing специальный тип Union , который делал тоже самое. Сейчас можно просто пользоваться вертикальной чертой для того, чтобы задать несколько возможных типов для переменной. В случае именно Literal для указания нескольких возможных значений можно обойтись без объединения нескольких типов через вертикальную черту и задать тип следующим образом: А если тип переменной представляет из себя, например, число или строку, тогда эти типы объединяются через вертикальную черту: Кстати, literally — по-русски означает буквально. То есть, когда нам надо буквально задать конкретные значения в типе, мы можем это сделать при помощи типа В целом в таком формате использовать словарь здесь можно, но мне больше нравится вариант с именованным кортежем из этих двух вариантов. TypedDict Есть еще специальный типизированный dict . Если почему-то хочется иметь доступ к данным именно как к словарю, а не как к классу (то есть писать coordinates["latitude"] вместо coordinates.latitude ), то можно воспользоваться типизированным словарём: def get_gps_coordinates () -> dict [ Literal [ "longitude" , "latitude" ], float ]: return { "longitude" : 10 , "latitude" : 20 } age : int | str from typing import TypedDict class Coordinates ( TypedDict ): Яна практике не вижу большого смысла пользоваться именно типизированным словарём, нов целом можно найти какие-то сценарии для его использования. Например, уже есть много кода, который использует нашу структуру как словарь, номы хотим добавить в эту структуру типизацию и при этом не переписывать код, который уже использует эту структуру. В таком сценарии как раз имеет смысл воспользоваться А когда мы пишем новый код, то именованные кортежи или датаклассы это наиболее часто используемые варианты. О датаклассах мы как раз сейчас и поговорим! Dataclass Ещё один вариант задания структуры — Это обычный класс, это не именованный кортеж, распаковывать его как кортеж уже нельзя, и также он не ведет себя как кортеж сточки зрения изменения каждого элемента. Это обычный класс. С ним работают проверки в IDE, автодополнения — это, пожалуй, самая часто используемая структура для таких задач longitude : float latitude : float c = Coordinates ( longitude = 10 , latitude = 20 ) ( c [ "longitude" ]) # Работает автодополнение в IDE ( c [ "longitudeRRR" ]) # IDE покажет ошибку from dataclasses import dataclass @dataclass class Coordinates : longitude : float latitude : float def get_gps_coordinates () -> Coordinates : return Coordinates ( 10 , 20 ) ( get_gps_coordinates (). latitude ) # Автодополнение IDE для атрибута print ( get_gps_coordinates (). latitudeRRR ) # IDE подсветит опечатку Когда использовать NamedTuple , когда dataclass ? Как мы поймём чуть позже, сценарий именованных кортежей — это сценарий распаковки. Когда нам нужно использовать структуру именно как кортеж, тогда стоит задать её как NamedTuple . В остальных сценариях имеет смысл предпочесть Давайте сравним количество памяти, которое занимает в оперативке именованный кортежи датакласс. Для того, чтобы узнать, сколько памяти занимает переменная, воспользуемся библиотекой Pympler То есть, как видим, именованный кортеж занимает значительно меньше памяти в оперативке, чем dataclass , в данном примере в 3 раза. Это понятно, то как по своей сути это более простая структура данных, её нельзя менять и потому именованный кортеж можно эффективно хранить в памяти. В тоже время, если мы используем dataclass просто как фиксированную структуру для хранения неизменяемых данных, то можно сделать и его более эффективным dataclasses import dataclass from typing import NamedTuple from pympler import asizeof @dataclass class CoordinatesDT : longitude : float latitude : float class CoordinatesNT ( NamedTuple ): longitude : float latitude : float coordinates_dt = CoordinatesDT ( longitude = 10.0 , latitude = 20.0 ) coordinates_nt = CoordinatesNT ( longitude = 10.0 , latitude = 20.0 ) ( "dataclass" , asizeof asized ( coordinates_dt ). size ) # 328 bytes print ( "namedtuple:" , asizeof asized ( coordinates_nt ). size ) # 104 bytes from dataclasses import dataclass from pympler import asizeof @dataclass( slots = True , frozen = True ) class CoordinatesDT2 : longitude : float latitude : float coordinates_dt2 = CoordinatesDT2 ( longitude = 10.0 , latitude = 20.0 ) ( "dataclass with frozen and slots:" , asizeof asized ( coordinates_dt2 ). size ) # dataclass with frozen and slots: 96 bytes Обрати внимание — такая структура неизменна, как и кортеж (благодаря флагу frozen=True ), то есть не получится после определения экземпляра класса изменить его атрибуты. Флаг slots=True автоматически добавляет __slots__ нашему датаклассу (более быстрый доступ к атрибутами более эффективное хранение в памяти). Таким образом, как мы видим по нашему тесту, по памяти такой dataclass получается даже эффективнее кортежа. Кортеж можно использовать, если вам важно использовать его с распаковкой, например, таким образом: Экземпляр датакласса, очевидно, с распаковкой работать не будет, так как это не кортеж для типа После нашего отступления о структурах продолжим накидывать каркас приложения. В gps_coordinates.py оставим структуру dataclass с параметрами slots и потому что не предусматривается изменение координат, которые вернёт нам датчик на ноутбуке. Возвращаемое значение вставили пока, просто чтобы не ругались проверки в редакторе. Потом напишем реализацию, которая запросит координаты у команды whereami , распарсит её результаты и вернёт как результат функции Составим weather_api_service.py : latitude , longitude = coordinates_nt from dataclasses import dataclass @dataclass( slots = True , frozen = True ) class Coordinates : longitude : float latitude : float def get_coordinates () -> Coordinates : return Coordinates ( longitude = 10 , latitude = 20 ) Так, какой типу погоды будет возвращаться Тут главное не смотреть на формат данных в API сервисе, потому что сервис и формат данных в нём вторичны, первичны наши потребности. Какие данные нам нужны Нам нужна температура за бортом, наше место, общая характеристика погоды — ясно/неясно/снег/дождь и тп, а также мне лично ещё интересно, во сколько сегодня восход солнца и закат солнца. Вот эти данные нам нужны, их пусть функция get_weather и возвращает. В каком формате? Так, ну давайте думать. Просто tuple ? Точно нет. Вообще есть мнение, что если мы хотим использовать tuple , то стоит использовать NamedTuple , потому что в нём явно данные будут названы. Поэтому возможно Просто dict ? Точно нет. Не будет нормальных проверок в IDE и статическом анализаторе, не будет подсказок, и читателю кода непонятно, что там внутри словаря. TypedDict ? Лучше, но мне нравится доставать данные как атрибуты класса, а не как ключи словаря. Поэтому TypedDict тоже не будем брать. Может dataclass ? Можно. Итого NamedTuple или dataclass ? Оба варианта ок, можно выбрать любой вариант, я, пожалуй, тут выберу dataclass с параметрами frozen и slots просто потому что распаковывать структуру как кортеж нам незачем, а по памяти dataclass с такими параметрами даже эффективнее кортежа coordinates import Coordinate def get_weather ( coordinates : Coordinate ): """Requests weather in OpenWeather API and returns it""" pass Обратите внимание, как я обошёлся с температурой. Можно было прописать тип напрямую int , ноя сделал alias, то есть псевдоним, для int с названием Celsius и теперь понятно, что у нас температура тут будет именно в градусах Цельсия, а не Фаренгейта или Кельвина. Также, если какая-то функция будет принимать на входили возвращать температуру, то мы тоже укажем для температуры там конкретный типа не общий непонятный Дальше, как быть с полем weather_type ? Что за строка там будет Хочется, чтобы там была непросто любая строка, а строго один из заранее заданных вариантов. Тут мы будем хранить описание погоды — ясно, туманно, дождливо и тп. Для этих целей существует структура Enum . Её название происходит от слова перечисление. Когда нам нужно перечислить какие-то заранее заданные варианты значений, то Enum это та самая структура, которая нам нужна. Давайте создадим структуру типов погоды, отнаследовав её от Enum и заполнив всеми возможными типами погоды, которые мы возьмём из справочника с dataclasses import dataclass from datetime import datetime from coordinates import Coordinate Celsius = int @dataclass( slots = True , frozen = True ) class Weather : temperature : Celsius weather_type : str # Подумаем, как хранить описание погоды sunrise : datetime sunset : datetime city : str def get_weather ( coordinates : Coordinate ): """Requests weather in OpenWeather API and returns it""" pass Каждый конкретный тип погоды описывается через атрибут В чём фишка Enum ? Зачем наследовать наш класс от Enum , почему бы просто не сделать класс с такими же атрибутами класса Допустим, у насесть функция print_weather_type , которая печатает погоду datetime import datetime from enum import Enum class WeatherType ( Enum ): THUNDERSTORM Гроза DRIZZLE Изморось RAIN Дождь SNOW Снег CLEAR Ясно FOG Туман CLOUDS Облачно frozen = True ) class Weather : temperature : Celsius weather_type : WeatherType sunrise : datetime sunset : datetime city : str print ( WeatherType CLEAR ) # WeatherType.CLEAR ( WeatherType CLEAR value ) # Ясно print ( WeatherType CLEAR name ) # CLEAR from enum import Enum class WeatherType ( Enum ): THUNDERSTORM Гроза DRIZZLE Изморось RAIN Дождь SNOW Снег CLEAR Ясно FOG Туман CLOUDS Облачно print_weather_type ( weather_type : WeatherType ) -> None : ( weather_type value ) Как видите, тип для аргумента функции weather_type указан как WeatherType . А передаём туда мы не экземпляра, при этом наш «проверятор» кода вне ругается, ему всё нравится. Дело в том, что: То есть WeatherType.CLOUDS является экземпляром самого типа WeatherType , и это позволяет нам таким образом использовать этот класс в подсказке типов. В функцию print_weather_type можно передать только то, что явным образом перечислено в Enum структуре WeatherType и ничего больше. Если мы уберём наследование от Enum , то IDE сразу скажет нам о несоответствии типов: Здесь WeatherType.CLOUDS — это обычная строка со значением "Облачно, типа не. Тип str и тип WeatherType — разные, поэтому IDE определит и подсветит эту ошибку несоответствия типов. В этом особенность Enum . Цель этой структуры — задавать перечисление возможных вариантов значений Облачно print ( isinstance ( WeatherType CLOUDS , WeatherType ) ) # True from enum import Enum class WeatherType : # Убрали наследование от Enum THUNDERSTORM Гроза DRIZZLE Изморось RAIN Дождь SNOW Снег CLEAR Ясно FOG Туман CLOUDS Облачно print_weather_type ( weather_type : WeatherType ) -> None : ( weather_type ) # Вместо weather_type.value print_weather_type ( WeatherType CLOUDS ) # IDE подсветит ошибку типов Ещё по Enum можно итерироваться в цикле, что иногда может быть удобно: И, конечно, Enum структуру можно разбирать с помощью новых возможностей Python — Pattern Нонам здесь это пока не нужно. Также часто полезным бывает отнаследовать класс перечисления от Enum и от Тогда значение можно использовать как строку без обращения к .value атрибуту weather_type in WeatherType : ( weather_type name , weather_type value ) def what_should_i_do ( weather_type : WeatherType ) -> None : match weather_type : case WeatherType THUNDERSTORM | Уф, лучше сиди дома О, отличная погодка" ) case _ : ( "Ну так, выходить можно Ну так, выходить можно Наследование от str и Enum class WeatherTypeStrEnum ( str , Enum ): FOG Туман CLOUDS Облачно Вариант без наследования от str class WeatherTypeEnum ( Enum ): FOG Туман CLOUDS Облачно ОБЛАЧНО AttributeError print ( WeatherTypeEnum CLOUDS value upper ()) # ОБЛАЧНО Облачно True print ( WeatherTypeEnum CLOUDS Облачно False print ( WeatherTypeEnum CLOUDS value Облачно True Погода { WeatherTypeStrEnum CLOUDS } " ) # Погода Облачно При этом тип WeatherTypeStrEnum и str — это всё же разные типы. Если аргумент функции ожидает WeatherTypeStrEnum , то передать туда str не получится. Типизация работает как надо: Какие еще варианты для использования Enum можно придумать Например, перечисление полов, мужской/женский. Перечисление статусов запросов, ответов, каких-то операций. Перечисление статусов заказов, например, если эти статусы зашиты в приложении, а не берутся из справочника БД. Перечисление дней недели (понедельник, вторники тд). Итак, полный код weather_api_service.py на текущий момент: ( f"Погода: { WeatherTypeEnum CLOUDS } " ) # Погода Погода { WeatherTypeEnum CLOUDS value } " ) # Погода Облачно def make_something_great_with_weather ( weather : WeatherTypeStrEnum ): pass Туман Не пройдёт проверку типов smth ( WeatherTypeStrEnum FOG ) # Ок, всё в порядке Обратите внимание, как всё чётенько! Мы читаем описание функции get_weather и у нас не может быть непониманий, что эта функция принимает на входи в каком формате, а также что она возвращает на выходи опять же в каком формате. Если в будущем мы будем работать нес, ас каким-то другим сервисом погоды, то мы просто заменим слой общения с этим внешним сервисом, но пока наша функция get_weather будет возвращать структуру Weather , весь остальной, внешний по отношению к этой функции, код будет работать без изменений. Мы прописали интерфейс коммуникации функции get_weather с внешним миром и пока этот интерфейс поддерживается — неважно как и откуда получаются данные внутри from datetime import datetime from dataclasses import dataclass from enum import Enum from coordinates import Coordinate Celsius = int class WeatherType ( str , Enum ): THUNDERSTORM Гроза DRIZZLE Изморось RAIN Дождь SNOW Снег CLEAR Ясно FOG Туман CLOUDS Облачно frozen = True ) class Weather : temperature : Celsius weather_type : WeatherType sunrise : datetime sunset : datetime city : str def get_weather ( coordinates : Coordinate ) -> Weather : """Requests weather in OpenWeather API and returns it""" return Weather ( temperature = 20 , weather_type = WeatherType CLEAR , sunrise = datetime fromisoformat ( "2022-05-04 04:00:00" ), sunset = datetime fromisoformat ( "2022-05-04 20:25:00" ), city = "Moscow" ) этой функции, главное, чтобы они просто на выходе преобразовались в нужный нам формат. Отлично, осталось реализовать заглушку для принтера, который будет печатать нашу погоду, Отлично, каркас приложения готов. Прописаны основные типы данных, что функции принимают на входи возвращают. По этим функциями типам уже сейчас понятно, как будет работать приложение, хотя мы ещё по сути ничего не реализовали. Заполним полученный скелет приложения теперь реализацией. |