Главная страница
Навигация по странице:

  • Реализация приложения — принтер погоды

  • Обработка исключений

  • Типизированный_Python_для_профессиональной_разработки. Профессиональной разработки


    Скачать 3.38 Mb.
    НазваниеПрофессиональной разработки
    АнкорPython
    Дата15.12.2022
    Размер3.38 Mb.
    Формат файлаpdf
    Имя файлаТипизированный_Python_для_профессиональной_разработки.pdf
    ТипДокументы
    #847528
    страница3 из 5
    1   2   3   4   5
    Реализация приложения — получение GPS координат
    Реализуем в первую очередь получение GPS координат, get_gps_coordinates.py
    :
    from weather_api_service import
    Weather def format_weather
    (
    weather
    :
    Weather
    )
    ->
    str
    :
    """Formats weather data in Тут будет печать данных погоды из структуры weather"
    from dataclasses import dataclass from subprocess import
    Popen
    ,
    PIPE from exceptions import
    CantGetCoordinates
    @dataclass(
    slots
    =
    True
    ,
    frozen
    =
    True
    )
    class
    Coordinates
    :
    longitude
    :
    float latitude
    :
    float def get_gps_coordinates
    ()
    ->
    Coordinates
    :
    """Returns current coordinates using MacBook GPS"""
    process
    =
    Popen
    ([
    "whereami"
    ],
    stdout
    =
    PIPE
    )
    (
    output
    ,
    err
    )
    =
    process communicate
    ()
    exit_code
    =
    process wait
    ()
    if err is not
    None or exit_code
    !=
    0
    :
    raise
    CantGetCoordinates output_lines
    =
    output decode
    ().
    strip
    ().
    lower
    ().
    split
    (
    "\n"
    )
    latitude
    =
    longitude
    =
    None for line in output_lines
    :
    if line startswith
    (
    "latitude:"
    ):
    latitude
    =
    float
    (
    line split
    ()[
    1
    ])
    Хочу обратить внимание тут вот на что. Если что-то пошло не такс процессом получения координат — мы не возвращаем какую-то ерунду вроде
    None
    . Мы возбуждаем (райзим, от англ. raise) исключение. Причём исключение не какое-то системное вроде
    ValueError
    , а наш собственный тип исключения, который мы назвали
    CantGetCoordinates и положили в специальный модуль, куда мы будем класть исключения Почему не
    ValueError
    , а свой тип исключений Чтобы разделять обычные питоновские
    ValueError от конкретно нашей ситуации с невозможностью получить координаты. Явное лучше неявного.
    Почему исключение, а не возврат
    None
    ? Потому что если у функции есть нормальный сценарий работы и ненормальный, то есть исключительный, то исключительный сценарий должен использовать исключения, а не возвращать какую-то ерунду вроде
    False
    ,
    0
    ,
    None
    , tuple()
    . Исключительная ситуация должна возбуждать исключение, и уже на уровне выше нашей функции мы должны решить, что с этой исключительной ситуацией делать. Код, который будет вызывать нашу функцию get_gps_coordinates
    , решит, что делать с исключительной ситуацией, на каком уровне и как эта ситуация должна быть обработана.
    Отлично. Функция отдаёт сейчас точные координаты, которые я не хочу раскрывать,
    давайте введём в приложение конфиг config.py ив нём зададим, использовать точные координаты или примерные. Я буду использовать примерные координаты.
    Погода от этого не изменится, просто в другой район города попаду line startswith
    (
    "longitude:"
    ):
    longitude
    =
    float
    (
    line split
    ()[
    1
    ])
    return
    Coordinates
    (
    longitude
    =
    longitude
    ,
    latitude
    =
    latitude
    )
    if
    __name__
    ==
    "__main__"
    :
    print
    (
    get_gps_coordinates
    ())
    class
    CantGetCoordinates
    (
    Exception
    ):
    """Program can't get current GPS coordinates"""
    USE_ROUNDED_COORDS
    =
    True
    Отлично. Обратите внимание — мы не полагаемся здесь на тона какой строке будет значение широты и долготы в выдаче команды whereami
    . Мы ищем нужную строку во всех возвращаемых строках, не полагаясь на то, будут это первые строки или нет.
    Получается более надёжное решение на случай смены порядка строк в Теперь проведём рефакторинг, поделив большую, делающую слишком много всего функцию get_gps_coordinates на несколько небольших простых функций dataclasses import dataclass from subprocess import
    Popen
    ,
    PIPE import config from exceptions import
    CantGetCoordinates
    @dataclass(
    slots
    =
    True
    ,
    frozen
    =
    True
    )
    class
    Coordinates
    :
    longitude
    :
    float latitude
    :
    float def get_gps_coordinates
    ()
    ->
    Coordinates
    :
    """Returns current coordinates using MacBook GPS"""
    process
    =
    Popen
    ([
    "whereami"
    ],
    stdout
    =
    PIPE
    )
    output
    ,
    err
    =
    process communicate
    ()
    exit_code
    =
    process wait
    ()
    if err is not
    None or exit_code
    !=
    0
    :
    raise
    CantGetCoordinates output_lines
    =
    output decode
    ().
    strip
    ().
    lower
    ().
    split
    (
    "\n"
    )
    latitude
    =
    longitude
    =
    None for line in output_lines
    :
    if line startswith
    (
    "latitude:"
    ):
    latitude
    =
    float
    (
    line split
    ()[
    1
    ])
    if line startswith
    (
    "longitude:"
    ):
    longitude
    =
    float
    (
    line split
    ()[
    1
    ])
    if config
    USE_ROUNDED_COORDS
    :
    # Добавили округление координат latitude
    ,
    longitude
    =
    map
    (
    lambda c
    :
    round
    (
    c
    ,
    1
    ),
    [
    latitude
    ,
    longitude
    ])
    return
    Coordinates
    (
    longitude
    =
    longitude
    ,
    latitude
    =
    latitude
    )
    if
    __name__
    ==
    "__main__"
    :
    print
    (
    get_gps_coordinates
    ())
    from dataclasses import dataclass from subprocess import
    Popen
    ,
    PIPE from typing import
    Literal import config from exceptions import
    CantGetCoordinates
    @dataclass(
    slots
    =
    True
    ,
    frozen
    =
    True
    )
    class
    Coordinates
    :
    latitude
    :
    float longitude
    :
    float def get_gps_coordinates
    ()
    ->
    Coordinates
    :
    """Returns current coordinates using MacBook GPS"""
    coordinates
    =
    _get_whereami_coordinates
    ()
    return
    _round_coordinates
    (
    coordinates
    )
    def
    _get_whereami_coordinates
    ()
    ->
    Coordinates
    :
    whereami_output
    =
    _get_whereami_output
    ()
    coordinates
    =
    _parse_coordinates
    (
    whereami_output
    )
    return coordinates def
    _get_whereami_output
    ()
    ->
    bytes
    :
    process
    =
    Popen
    ([
    "whereami"
    ],
    stdout
    =
    PIPE
    )
    output
    ,
    err
    =
    process communicate
    ()
    exit_code
    =
    process wait
    ()
    if err is not
    None or exit_code
    !=
    0
    :
    raise
    CantGetCoordinates return output def
    _parse_coordinates
    (
    whereami_output
    :
    bytes
    )
    ->
    Coordinates
    :
    try
    :
    output
    =
    whereami_output decode
    ().
    strip
    ().
    lower
    ().
    split
    (
    "\n"
    )
    except
    UnicodeDecodeError
    :
    raise
    CantGetCoordinates return
    Coordinates
    (
    latitude
    =
    _parse_coord
    (
    output
    ,
    "latitude"
    ),
    longitude
    =
    _parse_coord
    (
    output
    ,
    "longitude"
    )
    )
    def
    _parse_coord
    (
    output
    :
    list
    [
    str
    ],
    coord_type
    :
    Literal
    [
    "latitude"
    ]
    |
    Literal
    [
    "longitude"
    ])
    ->
    float
    :
    for line in output
    :
    if line startswith
    (
    f"
    {
    coord_type
    }
    :"
    ):
    return
    _parse_float_coordinate
    (
    line split
    ()[
    1
    ])
    else
    :
    Кода стало больше, функций стало больше, но код стал проще читаться и будет проще сопровождаться. Если бы мы сейчас писали тесты, то убедились бы ещё ив том, что этот код легче обложить тестами, чем предыдущий вариант с одной большой функцией, делающей всё подряд.
    Функции, имена которых начинаются с подчёркивания — не предназначены для вызова извне модуля, то есть они вызываются только соседними функциями модуля Почему много коротких функций это лучше, чем одна большая функция Потому что для того, чтобы понять, что происходит внутри функции на 50 строк, надо прочитать строк. А если эти 50 строк разбить на пару меньших функций и понятным образом эти пару функций назвать, тонам понадобится прочесть всего пару строк с вызовами этой пары функций и всё. Прочесть пару строк легче, чем 50. А если нам нужны детали реализации какой-то из этих меньших функций, мы всегда можем вне провалиться и посмотреть, что внутри.
    Функция get_gps_coordinates тут максимально проста — она получает координаты и затем округляет их и возвращает, всё. Два вызова понятно названных функций вместо длинного сложного кода, как было раньше.
    Также обратите внимание — абсолютно все функции типизированы, все принимаемые аргументы функций типизированы и все возвращаемые значения тоже типизированы. Причём типизированы максимально конкретными типами CantGetCoordinates def
    _parse_float_coordinate
    (
    value
    :
    str
    )
    ->
    float
    :
    try
    :
    return float
    (
    value
    )
    except
    ValueError
    :
    raise
    CantGetCoordinates def
    _round_coordinates
    (
    coordinates
    :
    Coordinates
    )
    ->
    Coordinates
    :
    if not config
    USE_ROUNDED_COORDS
    :
    return coordinates return
    Coordinates
    (
    *
    map
    (
    lambda c
    :
    round
    (
    c
    ,
    1
    ),
    [
    coordinates latitude
    ,
    coordinates longitude
    ]
    ))
    if
    __name__
    ==
    "__main__"
    :
    print
    (
    get_gps_coordinates
    ())
    Эта логика реализована без классов, на обычных функциях. Это нормально. Ненужно использовать ООП просто для того, чтобы у вас были классы. Оттого, что мы обернём несколько описанных здесь функций в класс — никакого нового полезного качества в нашем коде не появится, просто вместо функций будет класс. В таком случае вовсе ненужно использовать классы.
    Обратите внимание также, как в функции
    _parse_float_coordinate обработана ошибка, которая может возникать, если вдруг координаты не получается привести из строки к типу float
    — мы возбуждаем (райзим) исключение своего типа. В любой ситуации, когда нам не удалось получить координаты из результатов команды whereami мы получаем такое исключение и можем обработать
    (или не обрабатывать) его в коде, который будет вызывать нашу верхнеуровневую функцию get_gps_coordinates
    . Про работу с исключениями более подробно мы поговорим в отдельном материале.
    Реализация приложения — получение погоды с Отлично, у нас реализована структура и скелет приложения, а также полностью реализована логика получения текущих GPS координат — в точном или округлённом варианте. Реализуем теперь получение по этим координатам значения погоды с использованием API сервиса OpenWeather. Добавим шаблон URL для получения погоды в Значения широты и долготы будем потом подставлять в этот шаблон. Если нам понадобится изменить однажды этот шаблон URL для получения данных, мы сможем не искать его где-то глубоко в приложении, он лежит в конфиге. Все данные,
    которые предполагаются как конфигурационные, имеет смысл выносить в отдельное место, которое можно назвать конфигом или настройками приложения ключ для сервиса OpenWeather лучше сохранить в переменной окружения и не хранить в исходном коде проекта (тогда значение константы будет получаться как
    =
    True
    OPENWEATHER_API
    =
    "7549b3ff11a7b2f3cd25b56d21c83c6a"
    OPENWEATHER_URL
    =
    (
    "https://api.openweathermap.org/data/2.5/weather?"
    "lat={latitude}&lon={longitude}&"
    "appid="
    +
    OPENWEATHER_API
    +
    "&lang=ru&"
    "units=metric"
    )
    то так os.getenv("OPENWEATHER_API_KEY")
    , но сейчас мы этого делать не будем для упрощения запуска приложения.
    Итак, реализация работы с сервисом погоды OpenWeather, weather_api_service.py
    :
    from datetime import datetime from dataclasses import dataclass from enum import
    Enum import json from json decoder import
    JSONDecodeError import ssl from typing import
    Literal import urllib request from urllib error import
    URLError from coordinates import
    Coordinates import config from exceptions import
    ApiServiceError
    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
    :
    Coordinates
    )
    ->
    Weather
    :
    """Requests weather in OpenWeather API and returns it"""
    openweather_response
    =
    _get_openweather_response
    (
    longitude
    =
    coordinates longitude
    ,
    latitude
    =
    coordinates latitude
    )
    weather
    =
    _parse_openweather_response
    (
    openweather_response
    )
    return weather def
    _get_openweather_response
    (
    latitude
    :
    float
    ,
    longitude
    :
    float
    )
    ->
    str
    :
    ssl
    _create_default_https_context
    =
    ssl
    _create_unverified_context
    url
    =
    config
    OPENWEATHER_URL
    format
    (
    latitude
    =
    latitude
    ,
    longitude
    =
    longitude
    )
    try
    :
    return urllib request urlopen
    (
    url
    ).
    read
    ()
    except
    URLError
    :
    raise
    ApiServiceError def
    _parse_openweather_response
    (
    openweather_response
    :
    str
    )
    ->
    Weather
    :
    try
    :
    openweather_dict
    =
    json loads
    (
    openweather_response
    )
    except
    JSONDecodeError
    :
    raise
    ApiServiceError return
    Weather
    (
    temperature
    =
    _parse_temperature
    (
    openweather_dict
    ),
    weather_type
    =
    _parse_weather_type
    (
    openweather_dict
    ),
    sunrise
    =
    _parse_sun_time
    (
    openweather_dict
    ,
    "sunrise"
    ),
    sunset
    =
    _parse_sun_time
    (
    openweather_dict
    ,
    "sunset"
    ),
    city
    =
    _parse_city
    (
    openweather_dict
    )
    )
    def
    _parse_temperature
    (
    openweather_dict
    :
    dict
    )
    ->
    Celsius
    :
    return round
    (
    openweather_dict
    [
    "main"
    ][
    "temp"
    ])
    def
    _parse_weather_type
    (
    openweather_dict
    :
    dict
    )
    ->
    WeatherType
    :
    try
    :
    weather_type_id
    =
    str
    (
    openweather_dict
    [
    "weather"
    ][
    0
    ][
    "id"
    ])
    except
    (
    IndexError
    ,
    KeyError
    ):
    raise
    ApiServiceError weather_types
    =
    {
    "1"
    :
    WeatherType
    THUNDERSTORM
    ,
    "3"
    :
    WeatherType
    DRIZZLE
    ,
    "5"
    :
    WeatherType
    RAIN
    ,
    "6"
    :
    WeatherType
    SNOW
    ,
    "7"
    :
    WeatherType
    FOG
    ,
    "800"
    :
    WeatherType
    CLEAR
    ,
    "80"
    :
    WeatherType
    CLOUDS
    }
    for
    _id
    ,
    _weather_type in weather_types items
    ():
    if weather_type_id startswith
    (
    _id
    ):
    return
    _weather_type raise
    ApiServiceError def
    _parse_sun_time
    (
    openweather_dict
    :
    dict
    ,
    time
    :
    Literal
    [
    "sunrise"
    ]
    |
    Literal
    [
    "sunset"
    ])
    ->
    datetime
    :
    return datetime fromtimestamp
    (
    openweather_dict
    [
    "sys"
    ][
    time
    ])
    def
    _parse_city
    (
    openweather_dict
    :
    dict
    )
    ->
    str
    :
    Как и ранее, следуем подходу небольших функций, каждая из которых делает одно небольшое действие, а общий результат достигается за счёт компоновки этих небольших функций. Логику парсинга каждой нужной нам единицы информации выносим в отдельные небольшие функции — отдельно парсинг температуры,
    отдельно парсинг типа погоды и времени восхода и заката. Каждая функция названа глагольным словом — получить, распарсить и тд. Напомню, что функция это ничто иное как именованный блок кода, этот блок кода что-то делает и потому его имеет смысл называть именно глаголом, который опишет это действие.
    Тут стоит отметить, что для парсинга и одновременно валидации JSON данных удобно использовать библиотеку
    Pydantic
    . О ней было видео на канале
    «Диджитализируй!». Здесь мы не стали её использовать из-за возможно некоторой её избыточности для нашей простой задачи, а также чтобы ограничиться только стандартной библиотекой Осталось реализовать принтер, который выведет нужные нам значения погоды в консоль!
    Реализация приложения — принтер погоды
    Итак, файл weather_formatter.py
    :
    return openweather_dict
    [
    "name"
    ]
    if
    __name__
    ==
    "__main__"
    :
    print
    (
    get_weather
    (
    Coordinates
    (
    latitude
    =
    55.7
    ,
    longitude
    =
    37.6
    )))
    from weather_api_service import
    Weather def format_weather
    (
    weather
    :
    Weather
    )
    ->
    str
    :
    """Formats weather data in string"""
    return
    (
    f"
    {
    weather city
    }
    , температура
    {
    weather temperature
    }
    °C, "
    f"
    {
    weather Восход
    {
    weather sunrise Закат
    {
    weather sunset strftime
    (
    '%H:%M'
    )}
    \n"
    )
    if
    __name__
    ==
    "__main__"
    :
    from datetime import datetime from weather_api_service import
    WeatherType print
    (
    format_weather
    (
    Weather
    (
    temperature
    =
    25
    ,
    weather_type
    =
    WeatherType
    CLEAR
    ,
    sunrise
    =
    datetime fromisoformat
    (
    "2022-05-03 04:00:00"
    ),
    sunset
    =
    datetime fromisoformat
    (
    "2022-05-03 20:25:00"
    ),
    Обратите внимание на печать типа погоды — weather.weather_type
    . Так можно,
    потому что мы отнаследовали
    WeatherType от str и
    Enum
    , а не только от
    Enum
    . Если бы мы отнаследовали
    WeatherType только от
    Enum
    , то для получения строкового значения нужно было бы напрямую обратиться к атрибуту value
    , вот так:
    weather.weather_type.value
    При необходимости выводить на печать значения как-то иначе, всегда можно это реализовать водном месте приложения. Как всегда обратите внимание, здесь реализован блок if __name__ == "__main__":
    , который позволяет тестировать код при непосредственно прямом вызове этого файла python3.10 weather_formatter.py
    . При импорте функции format_weather код в этом блоке выполнен не будет.
    Обработка исключений
    В процессе работы приложения могут возникать 2 вида исключений, которые мы заложили в приложении — что-то может пойти не такс, через который мы получаем текущие GPS координаты. Его может не быть в системе или по какой-то причине он может выдать результат не того формата, что мы ожидаем. В таком случае возбуждается исключение Также что-то может пойти не так при запросе погоды по координатам. Тогда возбуждается исключение
    ApiServiceError
    . Обработаем и его. Файл weather
    :
    city
    =
    "Moscow"
    )))

    #!/usr/bin/env python3.10
    from exceptions import
    ApiServiceError
    ,
    CantGetCoordinates from coordinates import get_gps_coordinates from weather_api_service import get_weather from weather_formatter import format_weather def main
    ():
    try
    :
    coordinates
    =
    get_gps_coordinates
    ()
    except
    Не смог получить GPS координаты exit
    (
    1
    )
    try
    :
    weather
    =
    get_weather
    (
    coordinates
    )
    except
    Не смог получить погоду в API сервиса погоды exit
    (
    1
    )
    print
    (
    format_weather
    (
    weather
    ))
    if
    __name__
    ==
    "__main__"
    :
    main
    ()
    Проверяем работу приложения
    Всё готово, вжух! Проверяем работу приложения:
    Отлично!
    1   2   3   4   5


    написать администратору сайта