Enum

Дальше, как быть с полем weather_type? Что за строка там будет? Хочется, чтобы там была не просто любая строка, а строго один из заранее заданных вариантов. Тут мы будем хранить описание погоды — ясно, туманно, дождливо и т. п. Для этих целей существует структура Enum. Её название происходит от слова Enumeration, перечисление. Когда нам нужно перечислить какие-то заранее заданные варианты значений, то Enum это та самая структура, которая нам нужна.

Давайте создадим структуру типов погоды, отнаследовав её от Enum и заполнив всеми возможными типами погоды, которые мы возьмём из справочника с OpenWeather:

from datetime import datetime
from enum import Enum

class WeatherType(Enum):
    THUNDERSTORM = "Гроза"
    DRIZZLE = "Изморось"
    RAIN = "Дождь"
    SNOW = "Снег"
    CLEAR = "Ясно"
    FOG = "Туман"
    CLOUDS = "Облачно"

@dataclass(slots=True, frozen=True)
class Weather:
    temperature: Celsius
    weather_type: WeatherType
    sunrise: datetime
    sunset: datetime
    city: str

Каждый конкретный тип погоды описывается через атрибут WeatherType:

print(WeatherType.CLEAR)  # WeatherType.CLEAR
print(WeatherType.CLEAR.value)  # Ясно
print(WeatherType.CLEAR.name)  # CLEAR

В чём фишка Enum? Зачем наследовать наш класс от Enum, почему бы просто не сделать класс с такими же атрибутами класса? Допустим, у нас есть функция print_weather_type, которая печатает погоду:

from enum import Enum

class WeatherType(Enum):
    THUNDERSTORM = "Гроза"
    DRIZZLE = "Изморось"
    RAIN = "Дождь"
    SNOW = "Снег"
    CLEAR = "Ясно"
    FOG = "Туман"
    CLOUDS = "Облачно"

def print_weather_type(weather_type: WeatherType) -> None:
    print(weather_type.value)

print_weather_type(WeatherType.CLOUDS)  # Облачно

Как видите, тип для аргумента функции weather_type указан как WeatherType. А передаём туда мы не экземпляр WeatherType, а WeatherType.CLOUDS, при этом наш «проверятор» кода в IDE не ругается, ему всё нравится. Дело в том, что:

print(
    isinstance(WeatherType.CLOUDS, WeatherType)
)  # True

То есть WeatherType.CLOUDS является экземпляром самого типа WeatherType, и это позволяет нам таким образом использовать этот класс в подсказке типов. В функцию print_weather_type можно передать только то, что явным образом перечислено в Enum структуре WeatherType и ничего больше.

Если мы уберём наследование от Enum, то IDE сразу скажет нам о несоответствии типов:

from enum import Enum

class WeatherType:  # Убрали наследование от Enum 
    THUNDERSTORM = "Гроза"
    DRIZZLE = "Изморось"
    RAIN = "Дождь"
    SNOW = "Снег"
    CLEAR = "Ясно"
    FOG = "Туман"
    CLOUDS = "Облачно"

def print_weather_type(weather_type: WeatherType) -> None:
    print(weather_type)  # Вместо weather_type.value 

print_weather_type(WeatherType.CLOUDS)  # IDE подсветит ошибку типов

Здесь WeatherType.CLOUDS — это обычная строка со значением "Облачно", тип str, а не WeatherType. Тип str и тип WeatherType — разные, поэтому IDE определит и подсветит эту ошибку несоответствия типов.

В этом особенность Enum. Цель этой структуры — задавать перечисление возможных вариантов значений.

Ещё по Enum можно итерироваться в цикле, что иногда может быть удобно:

for weather_type in WeatherType:
    print(weather_type.name, weather_type.value)

И, конечно, Enum структуру можно разбирать с помощью новых возможностей Python — Pattern Matching:

def what_should_i_do(weather_type: WeatherType) -> None:
    match weather_type:
        case WeatherType.THUNDERSTORM | WeatherType.RAIN:
            print("Уф, лучше сиди дома")
        case WeatherType.CLEAR:
            print("О, отличная погодка")
        case _:
            print("Ну так, выходить можно")

what_should_i_do(WeatherType.CLOUDS)  # Ну так, выходить можно

Но нам здесь это пока не нужно.

Также часто полезным бывает отнаследовать класс перечисления от Enum и от str. Тогда значение можно использовать как строку без обращения к .value атрибуту:

# Наследование от str и Enum
class WeatherTypeStrEnum(str, Enum):
    FOG = "Туман"
    CLOUDS = "Облачно"

# Вариант без наследования от str
class WeatherTypeEnum(Enum):
    FOG = "Туман"
    CLOUDS = "Облачно"

print(WeatherTypeStrEnum.CLOUDS.upper())  # ОБЛАЧНО
print(WeatherTypeEnum.CLOUDS.upper())  # AttributeError
print(WeatherTypeEnum.CLOUDS.value.upper())  # ОБЛАЧНО

print(WeatherTypeStrEnum.CLOUDS == "Облачно")  # True
print(WeatherTypeEnum.CLOUDS == "Облачно")  # False
print(WeatherTypeEnum.CLOUDS.value == "Облачно")  # True

print(f"Погода: {WeatherTypeStrEnum.CLOUDS}")  # Погода: Облачно
print(f"Погода: {WeatherTypeEnum.CLOUDS}")  # Погода: WeatherTypeEnum.CLOUDS
print(f"Погода: {WeatherTypeEnum.CLOUDS.value}")  # Погода: Облачно

При этом тип WeatherTypeStrEnum и str — это всё же разные типы. Если аргумент функции ожидает WeatherTypeStrEnum, то передать туда str не получится. Типизация работает как надо:

def make_something_great_with_weather(weather: WeatherTypeStrEnum): pass

smth("Туман")  # Не пройдёт проверку типов
smth(WeatherTypeStrEnum.FOG)  # Ок, всё в порядке

Какие еще варианты для использования Enum можно придумать? Например, перечисление полов, мужской/женский. Перечисление статусов запросов, ответов, каких-то операций. Перечисление статусов заказов, например, если эти статусы зашиты в приложении, а не берутся из справочника БД. Перечисление дней недели (понедельник, вторник и т. д.).

Итак, полный код weather_api_service.py на текущий момент:

from datetime import datetime
from dataclasses import dataclass
from enum import Enum
from typing import TypeAlias

from coordinates import Coordinates

Celsius: TypeAlias = int

class WeatherType(str, Enum):
    THUNDERSTORM = "Гроза"
    DRIZZLE = "Изморось"
    RAIN = "Дождь"
    SNOW = "Снег"
    CLEAR = "Ясно"
    FOG = "Туман"
    CLOUDS = "Облачно"

@dataclass(slots=True, frozen=True)
class Weather:
    temperature: Celsius
    weather_type: WeatherType
    sunrise: datetime
    sunset: datetime
    city: str

def get_weather(coordinates: Coordinates) -> 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"
   )

Обратите внимание, как всё чётенько! Мы читаем описание функции get_weather и у нас не может быть непониманий, что эта функция принимает на вход и в каком формате, а также что она возвращает на выход и опять же в каком формате. Если в будущем мы будем работать не с OpenWeather API, а с каким-то другим сервисом погоды, то мы просто заменим слой общения с этим внешним сервисом, но пока наша функция get_weather будет возвращать структуру Weather, весь остальной, внешний по отношению к этой функции, код будет работать без изменений. Мы прописали интерфейс коммуникации функции get_weather с внешним миром и пока этот интерфейс поддерживается — неважно как и откуда получаются данные внутри этой функции, главное, чтобы они просто на выходе преобразовались в нужный нам формат.

Отлично, осталось реализовать заглушку для принтера, который будет печатать нашу погоду, weather_formatter.py:

from weather_api_service import Weather

def format_weather(weather: Weather) -> str:
    """Formats weather data in string"""
    return "Тут будет печать данных погоды из структуры weather"

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

Заполним полученный скелет приложения теперь реализацией.