Cat

Сравнение улучшения кода в Python и C++ (часть 1)

Данная статья посвящена теме улучшения кода в языках Python и C++, а именно обработке ошибок, валидации, аннотациям типов, повышению отказоустойчивости и переиспользованию кода.

Дополнительные материалы

Icon Link

Реклама

Icon Link
Сравнение Python и С/C++ Arduinum628 03 Декабрь 2024 Просмотров: 66

Краткое описание

Данная статья посвящена теме улучшения кода в языках Python и C++, а именно обработке ошибок, валидации, аннотациям типов, повышению отказоустойчивости и переиспользованию кода.

 

Вступление

Всем доброго дня! В предыдущей статье Сравнение hashmap C/C++ с dict Python (часть 3) из рубрики "Сравнение Python и C/C++" мы разобрали простую версию телефонной книги на C++. Сегодня мы с вами будем улучшать код телефонной книги Python версии, делать лучше структуру кода, улучшать читаемость кода, повышать отказоустойчивость, использовать аннотации типов, обработку ошибок и валидировать код. Открывайте свои IDE и приготовьтесь делать нашу электронную записную книгу лучше.

 

Улучшение кода в Python

 

Что такое аннотация типов данных?

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

Сама по себе аннотация типов является своего рода "соглашением разработчиков", что я имею ввиду? Если мы в функции указем параметр с типом данных str, но передадим int, то сам Python никак на это не отреагирует (если в коде не произойдёт исключения, конечно), зато нас о несовпадении типов предупредит IDE.
Однако, в Python есть и инструменты, использующие аннотацию типов по прямому назначению, подобно указанию типа данных в языке C/C++. Самым ярким примером будет использование @dataclass или Pydantic-моделей:```python

@dataclass
class Example:
name: str

 

В данном примере, если мы создадим экземпляр класса Example(name=123), произойдёт ошибка.

 

Аннотация в действии

Давайте начнём с функции add_contact, которая добавляет новый контакт в телефонную книгу:

def add_contact(firstname, lastname, number):
    """Функция добавит новый контакт в книгу"""

    contact = f'{firstname} {lastname}'
    phone_book[contact] = number

 

На первый взгляд это простая функция, в которой всё хорошо. Однако она имеет ряд проблем со структурой кода и нуждается в переработке. А именно:
1) Было бы неплохо назначить параметрам функции их типы;
2) Также наша функция ничего не возвращает, поэтому нужно показать что она вернёт None.

Давайте перепишем нашу функцию:

def add_contact(firstname: str, lastname: str, number: int) -> None:
    """Функция добавит новый контакт в книгу"""

    contact = f'{firstname} {lastname}'
    phone_book[contact] = number

 

Теперь мы точно видим, что имя и фамилия имеют тип данных str (строка), а телефонный номер имеет тип int (целое число). Также мы видим, что функция вернёт -> None так как у неё нет данных, которая она вернёт.

Давайте теперь посмотрим как будет выглядеть функция, которая обязательно должна что-то вернуть.

Следующая функция get_number должна получить телефонный номер по имени и фамилии контакта, для этого необходимо ее переписать следующим образом:

def get_number(firstname: str, lastname: str) -> str | None:
    """Функция получает номер по фио"""

    contact = f'{firstname} {lastname}'
    if contact in phone_book.keys():
        return phone_book[contact]
    print('Контакт не обнаружен!')

 

Теперь мы видим что у параметров firstname и lastname появились типы данных str, а возвращаем мы -> str | None номер в виде строки или None. Также я решил вместо возврата строки 'Контакт не обнаружен!' выводить её функцией print. Так мы будем понимать, что контакт не обнаружен в телефонной книге и при этом прилетит None, что полезно для логики программы.

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

 

Улучшение Python кода

Для чего нам нужно улучшать свой код? Хорошим и точным ответом будет: для его оптимизации, скорости, уменьшения потребления ресурсов ПК и т.д.

 

Функция get_number

Давайте возьмём для примера функцию get_number и посмотрим как её можно улучшить.

def get_number(firstname, lastname): 
    """Функция получает номер по фи""" 

    contact = f'{firstname} {lastname}' 
    if contact in phone_book.keys(): 
        return phone_book[contact] 
    return 'Контакт не обнаружен!'

 

Внутри функции мы используем f-строку для создания строки "фамилии имени". Это правильный, современный и быстрый способ создать строку. Любой из остальных способов создания строки на современной версии Python я бы заменил на f-strings.

Теперь давайте пойдём дальше вниз по коду. Мы проверяем в строке ключей телефонной книги if contact in phone_book.keys() есть ли "фамилия имя" нашего контакта. Тут я уже вижу ошибку потому что нам не нужны все ключи словаря чтобы проверить один ключ. Получение всех ключей телефонной книги, а их там может быть очень много, что будет очень накладной операцией в плане производительности. Это было бы приемлемо, если мы должны были пройти по всем ключам книги и сделать операцию с каждым ключом. Для получения значения по ключу в dict Python есть метод get(), который вернёт None в случае если ключа нет. Также мы зависим от глобальной переменной phone_book, которую лучше передать функции в качестве аргумента.

Давайте внесем соответствующие изменения в наш код:

def get_number(phone_book: dict[str: str], firstname: str, lastname: str) -> str:
    """Функция получает номер по фио"""

    contact = f'{firstname} {lastname}'
    get_contact = phone_book.get(contact)

    if get_contact:
        return get_contact

    print('Контакт не обнаружен!')

 

Этим улучшением мы добились двух вещей:

  1. Вместо двух операций мы сделали одну. 
    Переменная get_contact используется как для проверки в условии if get_contact: так и для возврата контакта return get_contact.
  2. Мы убрали лишнюю операцию, которая получает все ключи из телефонной книги, что улучшает производительность в случае, если данных очень много.

Мы видим, что телефонная книга передаётся в качестве аргумента, а её параметр имеет тип dict, в котором хранятся ключи str. Теперь функция не зависит от имени книги и становится универсальной и легко переносимой в разные проекты и модули. Ну и ещё я сделал отступы для блоков кода для более удобной читаемости.

 

Функция get_all_contacts

Следующая функция get_all_contacts() ничего не возвращает, а выводит на экран все контакты функцией print(). Тут есть одна маленькая ошибка - из её названия следует, что она получает все контакты, а не выводит их на экран. Тут возможны три варианта улучшения: переименовать функцию согласно её функциональности или сделать её генераторной с помощью yield или возвращать список контактов. Я решил использовать генераторную функцию с yield.

Давайте посмотрим как изменилась функция:

def get_all_contacts(phone_book: dict[str: str]) -> Generator[str, str, str]:
    """Генераторная функция для возврата всех контактов"""

    for contact, number in phone_book.items():
        yield f'{contact} - {number}'

 

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

 

Декоратор print_items

Для вывода значений на экран мы можем либо использовать цикл for при вызове функции генератора, что не очень практично и удобно, а можем написать декоратор, который будет печатать значения из функции генератора. Давайте напишем декоратор и посмотрим на новый код.

def print_items(func: Callable[..., Generator[Any, None, None]]
)-> Callable[..., Generator[Any, None, None]]:
    """Функция декоратор для печати значений из генератора на экран"""

    def wrapper(*args, **kwargs) -> None:
        for value in func(*args, **kwargs):
            print(value)
    return wrapper


@print_items
def get_all_contacts(phone_book: dict[str: str]) -> Generator[str, str, str]:
    """Генераторная функция для возврата всех контактов"""

    for contact, number in phone_book.items():
        yield f'{contact} - {number}'

 

Мы видим, что у нас есть декоратор print_items, у которого в качестве параметра выступает функция с генератором func: Callable[..., Generator[Any, None, None].

Внутри декоратора мы проходим по генератору и выводим значения из него. Декоратор вернёт None так как он только печатает значение на экран функцией print(). Также саму функцию get_all_contacts() мы оборачиваем в декоратор @print_items.

 

Функция del_all_contacts

Следующую функцию, которую я бы поменял - это del_all_contacts().

def get_all_contacts(): 
    """Функция получает список всех контактов""" 
    for contact, number in phone_book.items(): 
        print(f'{contact} - {number}')

 

Мы создаём две лишних операции:

  • Первая проблема - мы делаем полную копию телефонной книги copy_contacts = phone_book.copy(). Если там 2000 контактов, то это будет накладно по ресурсам и производительности.
  • Вторая проблема - вопрос, для чего мы это делаем? Правильно, для ещё одной бесполезной операции copy_contacts.keys(), которая нам нужна чтобы пройти циклом for по ключам из телефонной книги.
  • Третья проблема - в конце мы удаляем каждую запись телефонной книги по одной штуке del phone_book[contact].

Итого три операции, которые можно сделать за одну. Давайте перепишем нашу функцию и посмотрим на неё:

def del_all_contacts(phone_book: dict[str: str]) -> None:
    """Функция удалит все контакты из телефонной книги"""

    phone_book.clear()

 

Как мы видим, всё это вмещается в одну строку с использованием встроенного в Python метода .clear(), который просто очистит словарь. Если есть хорошие встроенные методы и функции всегда используйте их, потому что они очень эффективны. Также они будут экономить ваше время на разработку.

Во все остальные функции, где требовалось добавить phone_book: dict[str: str] в качестве аргумента, я уже добавил. Зачем показывать одинаковые изменения по блоку, если вы увидите их в итоговом коде ниже?

 

Обработка ошибок

Теперь, когда наш код выглядит причёсанным, более читаемым и оптимизированным, мы можем приступить к его проверке на прочность =) Давайте попробуем вместо типа dict скормить функции add_contact() тип данных list.

# список со словарями для телефонной книги
phone_book = [{'Николай Николаев': '+72423535354'}, {'Виталий Краснов': '84424434345'}]

# добавим новый контакт
add_contact(
    phone_book=phone_book,
    firstname='Сергей',
    lastname='Сергеев',
    number='83201345024'
)

# выведем количество контактов
print(f'Количество контактов: {len(phone_book)}')

 

Запустим наш код через IDE, нажав на Run Python File.

Traceback (most recent call last):
  File "/home/user_name/Документы/Сode/phone_book.py", line 65, in <module>
    nuber = get_number(
            ^^^^^^^^^^^
  File "/home/user_name/Документы/Сode/phone_book.py", line 16, in get_number
    get_contact = phone_book.get(contact)
                  ^^^^^^^^^^^^^^
AttributeError: 'list' object has no attribute 'get'

 

Мы видим ошибку AttributeError, так как у list нет метода .get(). Наша программа упала полностью, а вызовы оставшихся функций выполнены не будут. Чтобы этого не произошло, а программа не упала и выдала нам подсказку, нужно обрабатывать ошибки. Тут нам понадобится конструкция try ... except, которая будет помогать нашей программе не упасть, а разработчику и пользователю давать подсказки, где есть проблема в коде. Теперь давайте добавим в функцию add_contact() обработку ошибок и посмотрим на изменения в коде.

def add_contact(phone_book: dict[str: str], firstname: str, lastname: str,
number: int) -> None:
    """Функция добавит новый контакт в книгу"""

    try:
        if not isinstance(phone_book, dict):
            raise TypeError('Тип записной книги должен быть dict')
        elif not isinstance(firstname, str):
            raise TypeError('Имя должно иметь тип str')
        elif not isinstance(lastname, str):
            raise TypeError('Фамилия должна иметь тип str')

        contact = f'{firstname} {lastname}'
        phone_book[contact] = number
    except TypeError as err:
        message_err =f'{err.__class__.__name__}: {err}'
        print(message_err)

 

Как мы видим в if not isinstance(phone_book, dict), если тип данных book не dict мы вызываем ошибку TypeError, в которой прописываем сообщение, что указывает на проблему 'Тип записной книги должен быть dict'. Похожую обработку мы сделали для имени и фамилии, только там типом данных должен быть str. Строка except TypeError as err: отлавливает ошибку TypeError. Строка ниже message_err = f'{err.__class__.__name__}: {err}' формирует само сообщение об ошибке, которое состоит из названия ошибки err.__class__.__name__ и её сообщения в тексте err. Ну и в конце мы выводим само сообщение функцией print().

Посмотрим, как себя поведёт программа, запустив её в VSCode:

TypeError: Тип записной книги должен быть dict
Количество контактов: 2

 

Как мы видим, наша программа вывела понятную информацию об ошибке. Также наш код ниже продолжил свою работу и программа не упала полностью.

Сейчас я перепишу остальные функции под обработку ошибок, которые вы увидите в финальной версии кода. Я не буду заострять внимание на обработке ошибок в каждой функции, ибо это слишком долго расписывать. Вместо этого вы можете сами взглянуть на финальную версию кода и подробно ознакомиться с каждой ошибкой в документации Concrete exceptions. Я использую Python версии 3.12 поэтому скинул документацию на свою версию.

 

Переиспользование кода

Написав обработчики для нескольких функций, я заметил, что они одинаковые, а PEP 20 или "Дзен питона" учит нас не повторять код, а переиспользовать его.

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

Давайте напишем наш декоратор для отлавливания ошибок и посмотрим на его код:

def catching_exceptions(func: Callable) -> Callable:
    """Функция декоратор для отлова ошибок"""

    def wrapper(*args, **kwargs) -> None:
        try:
            return func(*args, **kwargs)
        except TypeError as err:
            message_err =f'{err.__class__.__name__}: {err}'
            print(message_err)
    return wrapper

 

Декоратор catching_exceptions принимает функцию в качестве аргумента и возвращает функцию. Об этом говорит объект Callable в аннотации и возврате декоратора. Мы видим, что с помощью декоратора выполняется отлов ошибки TypeError, которая нужна абсолютно для всех функций. Также мы формируем сообщение ошибки и выводим его на экран.

Мы экономим данные строки кода для всех функций. В самих функциях остались только raise ошибок и проверка на типы. Это нужно, потому что параметры в разных функциях разные и за их именами сложно следить в декораторах.

Давайте посмотрим, как изменилась функция add_contact():

@catching_exceptions
def add_contact(phone_book: dict[str: str], firstname: str, lastname: str,
number: int) -> None:
    """Функция добавит новый контакт в книгу"""

    if not isinstance(phone_book, dict):
        raise TypeError('Тип записной книги должен быть dict')
    elif not isinstance(firstname, str):
        raise TypeError('Имя должно иметь тип str')
    elif not isinstance(lastname, str):
        raise TypeError('Фамилия должна иметь тип str')

    contact = f'{firstname} {lastname}'
    phone_book[contact] = number

 

Функции-валидаторы

Мы видим, что функция стала более лаконична. Однако нужно понимать, что мы убрали только часть повторяющегося кода. Напрашивается написание двух валидаторов. Для типа записной книги dict и для фи lastname и firstname. Они должны идти отдельно, так как проверка на str имени и фамилии встречается не в каждой функции.

Давайте напишем два валидатора и посмотрим на наш новый код:

def type_phone_book_valid(phone_book: dict[str: str]) -> None:
    """Функция валидатор типа телефонной книги"""

    if not isinstance(phone_book, dict):
        raise TypeError('Тип записной книги должен быть dict')


def names_valid(firstname: str, lastname: str) -> None:
    """Функция валидатор фи"""

    if not isinstance(firstname, str):
        raise TypeError('Имя должно иметь тип str')

    if not isinstance(lastname, str):
        raise TypeError('Фамилия должна иметь тип str')


@catching_exceptions
def add_contact(phone_book: dict[str: str], firstname: str, lastname: str,
number: int) -> None:
    """Функция добавит новый контакт в книгу"""

    type_phone_book_valid(phone_book=phone_book)
    names_valid(firstname=firstname, lastname=lastname)

    contact = f'{firstname} {lastname}'
    phone_book[contact] = number

 

Теперь мы видим, что у нас появились две функции type_phone_book_valid() и names_valid() для удобной валидации типа записной книжки и типов имён. На самом деле можно было бы расширить валидацию, добавив ещё проверок. К примеру, можно было добавить проверку на пустую строку. Но моя задача в данном случае - показать и направить на процесс улучшения кода, а не предусмотреть все возможные проблемы кода. Я внедрю функции валидации во все функции телефонной книги и покажу всё в итоговом результате.

Было бы неплохо строки, подобные 'Тип записной книги должен быть dict', положить в errors.json или erros.yml и переиспользовать их. Но об этом как-нибудь в другой раз.

Посмотрев на код, я понимаю, что не хватает сообщений для оповещения об успешных операциях. Сейчас пользователь не понимает, успешно ли он создал контакт или нет. Программа при успехе просто хранит молчание. Чтобы это исправить, в конце функции add_contact() добавим строку print(f'Контакт {contact} с номером {number} успешно добавлен.'). В случае успешного запуска вы увидите строку вроде такой Контакт Сергей Сергеев с номером 83201345024 успешно добавлен. Я добавлю строки об успешной операции в каждую функцию где это необходимо. Затем я покажу вам весь код, что у меня получился и запущу его.

 

Итоговый результат

from typing import Any, Callable, Generator


def type_phone_book_valid(phone_book: dict[str: str]) -> None:
    """Функция валидатор типа телефонной книги"""

    if not isinstance(phone_book, dict):
        raise TypeError('Тип записной книги должен быть dict')


def names_valid(firstname: str, lastname: str) -> None:
    """Функция валидатор фи"""

    if not isinstance(firstname, str):
        raise TypeError('Имя должно иметь тип str')

    if not isinstance(lastname, str):
        raise TypeError('Фамилия должна иметь тип str')


def catching_exceptions(func: Callable) -> Callable:
    """Функция декоратор для отлова ошибок"""

    def wrapper(*args, **kwargs) -> None:
        try:
            return func(*args, **kwargs)
        except TypeError as err:
            message_err =f'{err.__class__.__name__}: {err}'
            print(message_err)

    return wrapper


@catching_exceptions
def add_contact(phone_book: dict[str: str], firstname: str, lastname: str,
number: int) -> None:
    """Функция добавит новый контакт в книгу"""

    type_phone_book_valid(phone_book=phone_book)
    names_valid(firstname=firstname, lastname=lastname)

    contact = f'{firstname} {lastname}'
    phone_book[contact] = number

    print(f'Контакт {contact} с номером {number} успешно добавлен.')


@catching_exceptions
def get_number(phone_book: dict[str: str], firstname: str, lastname: str) -> str:
    """Функция получает номер по фио"""

    type_phone_book_valid(phone_book=phone_book)
    names_valid(firstname=firstname, lastname=lastname)

    contact = f'{firstname} {lastname}'
    get_contact = phone_book.get(contact)

    if get_contact:
        return get_contact

    print('Контакт не обнаружен!')


def print_items(func: Callable[..., Generator[Any, None, None]]) -> None:
    """Функция декоратор для печати значений из генератора на экран"""

    def wrapper(*args, **kwargs) -> None:
        for value in func(*args, **kwargs):
            print(value)

    return wrapper


@catching_exceptions
@print_items
def get_all_contacts(phone_book: dict[str: str]) -> Generator[str, str, str]:
    """Генераторная функция для возврата всех контактов"""

    type_phone_book_valid(phone_book=phone_book)

    for contact, number in phone_book.items():
        yield f'{contact} - {number}'


@catching_exceptions
def del_contact(phone_book: dict[str: str], firstname: str, 
lastname: str) -> None:
    """Функция удалит контакт по фио"""

    type_phone_book_valid(phone_book=phone_book)
    names_valid(firstname=firstname, lastname=lastname)

    contact = f'{firstname} {lastname}'
    del phone_book[contact]

    print(f'Контакт {contact} успешно удалён.')


@catching_exceptions
def del_all_contacts(phone_book: dict[str: str]) -> None:
    """Функция удалит все контакты из телефонной книги"""

    type_phone_book_valid(phone_book=phone_book)
    phone_book.clear()

    print('Теелефонная книга успешно очищена от контактов.')


if __name__  '__main__':
    phone_book = {
        'Николай Николаев': '+72423535354',
        'Виталий Краснов': '84424434345'
    }

    # получим номер контакта
    nuber = get_number(
        phone_book=phone_book,
        firstname='Николай',
        lastname='Николаев'
    )
    print(nuber)

    print('--------------------------------')

    # добавим новый контакт
    add_contact(
        phone_book=phone_book,
        firstname='Сергей',
        lastname='Сергеев',
        number='83201345024'
    )

    print('--------------------------------')

    # выведем количество контактов
    print(f'Количество контактов: {len(phone_book)}')

    print('--------------------------------')

    # выведем список всех контактов
    get_all_contacts(phone_book=phone_book)

    print('--------------------------------')

    # удалим контакт
    del_contact(phone_book=phone_book, firstname='Виталий', lastname='Краснов')

    print('--------------------------------')

    # снова выведем список всех контактов
    get_all_contacts(phone_book=phone_book)

    print('--------------------------------')

    # удалим все контакты
    del_all_contacts(phone_book=phone_book)

    # убедимся, что телефонная книга пуста
    print(phone_book)
    print(type(phone_book.items()))

 

Итогововый результат работы кода в terminal:

+72423535354
--------------------------------
Контакт Сергей Сергеев с номером 83201345024 успешно добавлен.
--------------------------------
Количество контактов: 3
--------------------------------
Николай Николаев - +72423535354
Виталий Краснов - 84424434345
Сергеев - 83201345024
--------------------------------
Контакт Виталий Краснов успешно удалён.
--------------------------------
Николай Николаев - +72423535354
Сергеев - 83201345024
--------------------------------
Теелефонная книга успешно очищена от контактов.
{}
<class 'dict_items'>

 

Наш код отработал как надо. Дополнительно, теперь мы видим сообщения об успешном добавлении и удалении контакта, а также сообщение об успешном очищении телефонной книги. Это делает процесс использования телефонной книги более удобным и понятным. Ну и посмотрите на сам итоговый код проекта: он стал более защищённым от ошибок, понятным и удобным, плюс мы избавились от лишних строк. Основным недостатком данного кода является то, что весь код находится в одном файле. Поэтому в будущих статьях я буду разбивать проект на несколько .py файлов. Впереди эту телефонную книгу ждёт ещё много интересных преобразований =)

 

Анонс на следующие статьи

Следующая часть статьи выйдет 26.12.24, где я буду улучшать код телефонной книги в версии С++. В версию C++ я тоже добавлю валидацию, обработку ошибок и расскажу про то, как там можно аннотировать типы.

Дальше будет ещё интереснее :)

 

Заключение

  1. Поговорили об аннотации типов в Python;
  2. Узнали, как можно улучшить код;
  3. Внедрили обработку ошибок;
  4. Переиспользовали повторяющийся код;

Автор

    Нет комментариев

    Реклама