Поздравляем! Вы уже изучили все темы образовательного блока и сейчас подходите к заключительному заданию.
Это уже четвёртое итоговое задание, вы наверняка всё уже знаете, а если нет — коротко напомним, в чём тут суть.
Итак: эта задача похожа на те, что решают Python-разработчики на практике. Она поможет вам повторить пройденный материал и применить полученные знания.
Эта задача состоит из нескольких блоков:
Контекст. В реальной жизни задача не существует сама по себе, а возникает из определённого бизнес-контекста. Поэтому мы добавили небольшое описание трансформации задачи, от первого запроса до её финального вида.
Подсказки. Помощь в решении, если зайдёте в тупик. Комментарии к ключевым моментам решения задачи в формате FAQ.
Разбор. Подробный разбор задачи от автора, специалиста из Яндекса: от составления алгоритма решения до реализации и пояснения каждого блока кода. Открывается после первого успешно пройденного теста.
Важно: эта задача «со звёздочкой». Она посложнее предыдущих. Если не справитесь — ничего страшного: в программировании редко что-то получается с первого раза. Сделайте паузу, перечитайте теорию и возвращайтесь с новыми силами и хорошим настроением.
В этой задаче вы создадите симуляцию части функциональности банковского приложения. Это позволит пощупать взаимодействие нескольких классов и их методов. А ещё выстроите обработку исключений таким образом, чтобы приложение продолжало работать даже после ошибок.
Удачи!
О финальной задаче
Вы разработчик бэкенд-части приложения для нового банка. Вас попросили собрать базовую функциональность, которая позволит выполнять банковские операции, заводить карты разного типа и поддерживать обработку ошибок.
В основе бизнес-логики приложения выделены следующие пять классов, из которых будут заводиться реальные объекты:
Bank.
Account.
Card.
Transaction.
User.
Вот подробнее табличка о задаче каждого класса и функциях:
Название класса
За что отвечает
Сценарии пользователей/банка
Bank
Выпускает банковские продукты и хранит информацию обо всём, что было когда-либо выпущено
Выпустить карту
Посмотреть все транзакции в банке
Account
Хранит номер счёта, баланс, кто владелец
Сard
Отвечает за проведение операций по карте и хранит информацию о карте
Посмотреть информацию по карте
Посмотреть баланс
Пополнить счёт
Оплатить покупку
Перевести деньги на другую карту
Посмотреть историю транзакций
Transaction
Хранит информацию о транзакции
User
Данные пользователя, карты, счёта, ПИН-код приложения
Поменять ПИН-код
Задача кажется сложной, поэтому лучше всего работать последовательно, шаг за шагом добавляя новые функции:
Сначала наладить выпуск карт в целом.
Затем добавить базовые функции для использования карт (например, оплаты, переводы).
Под предпочтения пользователей выпустить несколько банковских продуктов в виде новых типов карт.
Организовать поддержку работы приложения, настроить обработку исключений.
В итоге у вас получится продуманная симуляция банковского приложения, реализованная с помощью принципов ООП. При желании можете добавить дополнительные функции и положить результат задачи к себе на GitHub!
Задачка объёмная — поэтому, чтобы вы сфокусировались на проработке принципов ООП, вам будут помогать заготовленные шаблоны. Например, ниже шаблон для метода get_card_info из первой задачи. Вся текстовая заготовка уже есть, вам нужно проставить наполнение.
1defget_card_info(self, fields: list = None):
2#ToDo: добавьте необходимую информацию для реализации метода3 user = self.account.owner
4 data = {
5"bank_name": f"Банк: {}",
6"bank_bic": f"БИК банка: {}",
7"card_id": f"Карта #{}",
8"user_id": f"Пользователь: {} — {}{}",
9"phone": f"Телефон: {}",
10"pan": f"PAN: {}",
11"acc_id": f"Счёт: {}",
12"payment_system": f"Плат. система: {}",
13"currency": f"Валюта: {}",
14"status": f"Статус: {}",
15"issue_date": f"Выпуск: {}",
16"expiry_date": f"Срок: {}",
17"user_cards": f"Карты пользователя: {}",
18"cashback_balance": f"Кешбэк: {:.2f}₽",
19"balance": f"Баланс: {:.2f}₽",
20 }
21if fields isNone:
22 fields = DEFAULT_CARD_INFO_FIELDS
23return (
24"\n".join([data[field] for field in fields if field in data])
25 + "\n"26 + "-" * 5027 )
Таким образом вы будете собирать весь код, используя и изучая заготовки. Более конкретные детали прописаны в условиях задач.
💡
Задачи последовательные и связаны между собой. Поэтому, если почувствуете, что где-то что-то не понимаете и нужна помощь, — обращайтесь к подсказкам или берите необходимые кусочки решения из разбора.
Также в рамках задачи вы будете использовать вспомогательные конструкции для работы с классами:
Класс Enum позволит унифицировать повторяющиеся элементы: статусы, типы операций.
Dataclass упростит работу с классами, которые предназначены для хранения данных.
Иерархическая организация класса ошибок настроит проверку и ловлю ошибок.
Всё это плюс нюансы разработки ПО для банковской сферы мы разобрали ниже. Советуем ознакомиться!
Нюансы
Как уже знаете, вы разрабатываете банковское приложение. В этой части хотим раскрыть несколько моментов, связанных со спецификой разработки в этой отрасли.
С одной стороны, вы создаёте ценность для пользователей:
Помогаете им завести карты.
Проводите их денежные операции.
Помогаете понять, что пошло не так, при возникновении ошибок.
С другой стороны, вы поддерживаете работу банковского приложения:
Храните всю информацию о пользователях и их операциях.
Выпускаете новые банковские продукты.
Поддерживаете работу приложения в рамках законодательства.
С третьей стороны, как разработчик, вы думаете о том, как лучше всё это сделать:
Выстраиваете удобную архитектуру для реализации логики приложения:
Проверяете данные.
Придерживаетесь бизнес-правил.
Обрабатываете исключения.
Проводите каждую операцию пользователя как транзакцию.
Тестируете работу этой системы в рамках пользовательских сценариев.
При этом в реальности требуется учесть несколько моментов, которые характерны для банковской сферы:
Номер счёта собирается по ГОСТу от Центробанка.
Номер банковской карты собирается по определённому алгоритму.
При работе с числами используется тип данных decimal.
Рассмотрим их подробнее: это важно для контекста. Но не переживайте, у нас уже готовы вспомогательные функции, реализовывать их не потребуется.
Сбор номера счёта
Номер счёта состоит из 20 цифр, которые разбиты на пять групп:
Тип счёта
Валюта
Проверочный код
Номер филиала
Серийный номер
40817
810
4
0000
0000001
Тип счёта. В данном случае — операции для физических лиц (40817). Если бы это был расчётный счёт организации, число было бы другим.
Валюта. В нашем случае — рубли (810).
Номер филиала. У нашего банка филиалов нет, поэтому стоят нули.
Серийный номер. Порядковый номер счёта, заведённого в банке (начинаем с 0000001).
Проверочный код. Рассчитывается по определённому алгоритму. О нём можно почитать в инструкции от Банка России.
Сбор номера карты
Он тоже собирается по определённому алгоритму с проверкой и состоит из 16 цифр, разбитых на три группы:
BIN (идентификатор платёжной системы)
Серийный номер
Контрольная цифра
220400
000000001
5
400000
000000002
8
510000
000000003
2
BIN. Зафиксирован для каждой платёжной системы. Для MIR это 220400, для VISA — 400000, а для MASTERCARD — 510000.
Серийный номер. Это порядковый номер заведённой банковской карты.
Контрольная цифра. Рассчитывается по алгоритму Луна. Подробнее про алгоритм можно почитать в статье на Википедии.
Таким образом, если на вход подаётся номер счёта или номер карты, то можно быстро проверить его валидность по структуре и проверочному коду или контрольной цифре.
Тип данных decimal
decimal — модуль в Python, реализующий десятичную арифметику с заданной точностью:
Он используется вместо float. Причина проста: у float есть погрешность при расчётах десятичных дробей, которая неприемлема, когда речь идёт о денежных суммах. Например, 0.1 + 0.2 с типом float даст не 0.3, а 0.30000000000000004.
1print(0.1 + 0.2) # 0.30000000000000004
Чтобы вам было проще, в задаче мы будем работать с float. Но если бы вы делали реальный проект для финансовой сферы, то использовали бы везде decimal.
Остальные детали касаются непосредственно определённых классов, методов и подхода к работе с исключениями. Погружаемся дальше!
Класс Enum для работы с ограниченными множествами
Часто в коде встречаются элементы, которые принимают значения из ограниченного множества или вариантов. Чтобы каждый раз не перепроверять опечатки и не вносить изменения в каждый блок кода при правках, рекомендуем использовать класс Enum.
Еnum (англ. enumeration, перечисление) — это специальный класс для создания наборов уникальных, ограниченных значений с понятными именами.
Например, вы хотите поддерживать статус карты (активна, закрыта, заблокирована). Можно реализовать это без Еnum, как в примере ниже.
1#Код без Enum2classCard:
3def__init__(self, status):
4# status может быть любой строкой!5 self.status = status
67card = Card("Active")
89if card.status == "Active":
10print("Карта активна")
11elif card.status == "Blocked":
12print("Карта заблокирована")
13else:
14print("Неизвестный статус")
Здесь статус задаётся и прописывается вручную. Таких кусочков кода в программе могут быть десятки. Основная сложность заключается в том, что где-то может быть написано в статусе «active» или «Activ». При этом ваш редактор кода (IDE) не подсветит, где введено неправильно. А если нужно переписать название статуса — то придётся править во всех местах тоже вручную.
Для осмысленной работы со статусами используют как раз класс Enum. Вот как выглядит тот же код с классом Enum ниже.
Теперь IDE знает, какие бывают статусы, и помогает автодополнением.
Если где-то будет опечатка, например card = Card(CardStatus.Active), то программа вызовет ошибку.
В целом CardStatus.ACTIVE выглядит как осмысленная фраза для читаемости кода.
При этом можно по-разному обращаться к элементу Enum, а также доставать имя и значение.
1# Один и тот же вызов элемента Enum разными способами2CardStatus.ACTIVE
3CardStatus["ACTIVE"] # По имени, строкой4CardStatus("Active") # По значению56# Получение имени и значения7status = CardStatus.ACTIVE
89status.name # 'ACTIVE' (имя элемента в перечислении)10status.value # 'Active' (значение, которое вы задали)
В рамках задачи Enum вы будете использовать для определения статуса карты и типа транзакции.
Подробнее об использовании Enum можно прочитать в документации.
Декоратор dataclass для классов-хранилищ
dataclass в Python — это специальный декоратор (@dataclass) из стандартного модуля dataclasses, который позволяет очень быстро и удобно создавать классы для хранения данных.
Ниже приведены два примера — с декоратором и без него.
Пример без dataclass
12classUser:
3def__init__(self, name, age):
4 self.name = name
5 self.age = age
67def__repr__(self):
8returnf"User(name={self.name!r}, age={self.age!r})"910user = User("Ivan", 23)
11print(user)
Декоратор создаёт такой же по функциональности элемент, но с меньшим количеством кода. Такой подход удобно использовать, когда вы знаете, что класс будет содержать конкретные поля данных, ему не нужен сложный __init__ и он не заточен чисто на методы (поведение).
В рамках задачи все классы, кроме Card, будут обёрнуты в dataclass, так как Card заточен больше на методы и ему важен __init__ с определённой логикой.
Ещё вы будете использовать функцию default_factory(). С её помощью модуль dataclasses умеет работать с изменяемыми объектами.
В рамках задачи в классе Bank и User вы будете заводить списки карт, транзакций, счетов и т. д. И с этим могут быть проблемы.
Например, вы хотите в рамках класса напрямую создать список.
1#Вот это создаст общий список на всех2from dataclasses import dataclass
34@dataclass5classUser:
6 tags: list = []
Вспоминаем, что список — это изменяемый тип данных. И если в одном месте вы что-то добавите в него, то он обновится везде. Подробнее об изменяемых и неизменяемых типах данных можно прочитать в статье на Хабре. Ниже код с демонстрацией проблемы.
Таким образом, с помощью декоратора dataclass вы упростите код у классов-хранилищ, а с помощью default_factory() создадите уникальные списки для каждого объекта.
Организация класса ошибок
В задачах 5.4 и 5.5 вы будете обрабатывать исключения у методов. Чтобы вам было проще поднимать конкретные ошибки, в шаблоне будет представлен каталог сообщений и организация классов ошибок. Давайте разберём его устройство и то, чем оно может вам помочь.
1# ======================= КАТАЛОГ СООБЩЕНИЙ ОБ ОШИБКАХ =======================2classBankError(Exception):
3"""Базовый класс для всех ошибок банковского приложения."""456classValidationError(BankError):
7"""Ошибка валидации пользовательских данных (форматы, обязательные поля, допустимые значения)."""89 PIN_MISMATCH = "Введенный ПИН-код не соответствует текущему"10 PIN_INVALID = "ПИН-код должен быть строкой из 4 символов"11 PIN_FORMAT_INVALID = "ПИН-код должен состоять только из цифр"12 NAME_INVALID = "Имя и фамилия должны быть на русском, без использования специальных символов"13 AMOUNT_NEGATIVE = "Сумма должна быть положительной"14 DEPOSIT_AMOUNT_NEGATIVE = "Сумма пополнений должна быть положительной"15 CASHBACK_NEGATIVE = "Процент кешбэка на покупки должен быть положительным"16 INTEREST_NEGATIVE = "Ставка по счету должна быть положительной"17 PAY_AMOUNT_NEGATIVE = "Сумма покупки должна быть положительной"18 INVALID_MCC = "Неверный код категории продавца (MCC)"19 PAYMENT_SYSTEM_NOT_SUPPORTED = "Платежная система {payment_system} не поддерживается банком"202122classNotFoundError(BankError):
23"""Ошибка отсутствия объекта: карта, счёт, пользователь не найдены."""2425 RECIPIENT_NOT_FOUND = "Ошибка номером карты. Такой карты не существует."262728classAccessError(BankError):
29"""Ошибка доступа к объекту или операции: карта/счёт не активны, недоступны или не привязаны."""3031 CARD_CLOSED = "Карта закрыта или заблокирована. Невозможно провести операцию."32 ACCOUNT_NOT_LINKED = "Карта не привязана к счёту"33 BANK_NOT_LINKED = "Карта не привязана к банку"34 RECIPIENT_ACCOUNT_NOT_LINKED = "Карта получателя не привязана к счёту"35 RECIPIENT_CARD_CLOSED = "Карта получателя закрыта или заблокирована. Невозможно провести операцию."363738classBusinessRuleError(BankError):
39"""Ошибка бизнес-логики: нарушено ограничение по правилам банка
40 (лимиты, количество, уникальность, запрещённые операции)."""4142 DEPOSIT_LIMIT_EXCEEDED = "Превышен лимит депозита. Карта заблокирована до выяснения причин."43 TRANSFER_LIMIT_EXCEEDED = "Подозрение на мошенническую операцию. Карта заблокирована до выяснения причин."44 CASHBACK_LIMIT = "Процент кешбэка завышен, возможна техническая ошибка. Карта заблокирована до выяснения причин."45 TRANSFER_TO_SELF = "Нельзя пересылать деньги самому себе"46 USER_CONFLICT = "Пользователь с таким телефоном уже зарегистрирован"47 TOO_MANY_DEBIT_CARDS = "У пользователя уже есть пять дебетовых карт"48 MCC_FORBIDDEN = "Оплата отклонена. Покупки по {mcc} запрещены банком"49 PURCHASE_LIMIT_EXCEEDED = "Сумма оплаты превышает лимит. Операция заблокирована до выяснения причин."50 PAYMENT_NOT_ALLOWED_FOR_SAVING = "С накопительного счёта нельзя списывать покупки"51 SAVING_RATE_TOO_HIGH = (
52"Ставка накопления завышена, возможна техническая ошибка. "53"Карта заблокирована до выяснения причины."54 )
555657classInsufficientFundsError(BankError):
58"""Ошибка недостатка денег"""5960 INSUFFICIENT_FUNDS_FOR_PAYMENT = "Недостаточно денег для оплаты."61 INSUFFICIENT_FUNDS_FOR_TRANSFER = "Недостаточно денег для осуществления перевода."
Все классы ошибок наследуются от одного класса BankError:
Последовательность классов определяет, в каком порядке обрабатываем ошибки. Чтобы самые широкие и критичные ловились в начале, а более локальные в конце.
Поэтому, когда в задаче описывается, какие исключения нужно обработать, последовательность исключений придерживается этой структуры. Сначала обрабатываем ValidationError — и т. д.
💡
Ведь если есть ошибка на более высоком уровне, то нет смысла лезть ниже.
Теперь вы знаете больше об использовании и организации конкретных классов и конструкций. Время закрепить знания на практике!
Чтобы добавить в заметки выделенный текст, нажмите Ctrl + E
Оцените ясность обучающего материала
Будем признательны, если вы поделитесь своим отзывом по финальной задаче и по блоку «5. Объектно-ориентированное программирование»