Лекция 7 · Часть 2
Полиморфизм, инкапсуляция и проектирование отказоустойчивых банковских систем
Сегодня мы разберём, как проектировать гибкие и защищённые архитектуры. На реальных примерах из сферы финтеха - как управлять сложными потоками данных, защищать пароли от попадания в логи и организовывать работу десятков платёжных шлюзов через единый интерфейс.
Повторение Лекции 6
На прошлом занятии мы заложили фундамент: научились упаковывать состояние и поведение в классы, а также познакомились с наследованием.
Прежде чем перейти к более сложным концепциям - динамическому полиморфизму и инкапсуляции - необходимо закрепить терминологию на базовых примерах. Без этой базы в механизмах MRO и Name Mangling легко запутаться.
Повторение
Сравним два подхода к управлению данными на примере ведения банковских счетов.
Класс - пользовательский тип данных, связывающий финансовое состояние и логику работы с ним в единую структуру.
Повторение
Программное описание структуры. Сам по себе не хранит данные конкретного клиента и не занимает места в памяти под данные.
Конкретный объект в памяти - например, счёт клиента с реальным балансом. По одному классу создаются миллионы изолированных объектов.
Повторение
Переменные внутри объекта, отражающие его текущее состояние: account_number, currency, is_blocked.
Функции внутри класса с доступом к состоянию объекта: withdraw(), block_account().
💡 Хорошо спроектированный класс самостоятельно управляет своими атрибутами, а внешний код взаимодействует с ним только через вызовы методов.
Повторение
__init__ и аргумент self__init__ - конструктор объекта, вызывается автоматически после выделения памяти. self - явная ссылка на текущий экземпляр.
Контекст
self vs Java thisself пишется первым аргументом каждого метода. «Явное лучше неявного».
JVM передаёт ссылку автоматически, внутри доступна через this.
Повторение
Повторение
__str__Повторение
Повторение
super()Повторение
Работая с Python, вы используете ООП ежеминутно - почти каждый вызов через точку - это метод объекта.
Повторение
| Понятие | Коротко | Смысл в архитектуре |
|---|---|---|
| Класс / Экземпляр | Описание структуры vs объект в RAM | Шаблон продукта и реальный счёт клиента |
| Атрибут / Метод | Переменная состояния vs функция | Баланс (данные) и метод перевода (поведение) |
| __init__ / self | Конструктор и ссылка на контекст | Точка входа при создании и ссылка на экземпляр |
| Наследование / super() | Расширение без дублирования | CreditAccount на базе BasicAccount |
Карта курса
| Концепция | Статус | Этап |
|---|---|---|
| Наследование | 🟢 ПРОЙДЕНО | Синтаксис расширения классов и super(). |
| Полиморфизм | 🟡 ТЕКУЩАЯ ТЕМА | Единый интерфейс для разных платёжных шлюзов. |
| Инкапсуляция | ⚪ ПРЕДСТОИТ | Сокрытие критических данных и валидация. |
| Абстракция | ⚪ ПРЕДСТОИТ | Отделение бизнес-логики от деталей реализации. |
Сегодня
_ и __, @property, сеттеры@staticmethod, @classmethod+, ==, len() для пользовательских объектовПолиморфизм
Представьте платёжный терминал на кассе. Ему не важно, что именно клиент прикладывает: пластиковую карту UzCard, смартфон с Humo Pay или QR-код от Milly Bank. Для терминала существует один интерфейс - «принять оплату». А то, как именно происходит списание, зависит от конкретного платёжного средства.
Полиморфизм - способность программы взаимодействовать с разными объектами через единый интерфейс, опираясь только на имя метода, но не на внутреннее устройство объекта.
Полиморфизм
Через наследование и переопределение. Создаётся родительский класс; потомки реализуют метод с одним именем, но собственной логикой.
Утиная типизация. Наследование не требуется: если у независимых классов есть метод с одним именем, его можно вызвать полиморфно.
Полиморфизм
Классический полиморфизм
Переопределение - это когда класс-потомок объявляет метод с тем же именем, что и у родителя, но реализует его по-своему.
При вызове obj.greet() Python ищет метод начиная с класса самого объекта. Версия из потомка «перекрывает» родительскую. А super().greet() позволяет дополнительно выполнить и версию родителя.
Классический полиморфизм
Базовый класс хранит общую логику. Класс-наследник переопределяет метод:
super() вызывает версию родителя. UzCardGateway добавляет своё поведение, не теряя общую логику ядра.
Классический полиморфизм
Humo и Milly Bank переопределяют метод так же. run_checkout работает с любым шлюзом:
Переопределение методов
Каждый шлюз по-своему переопределяет process_payment. super() сначала выполняет общую логику ядра (запись в лог), затем - код конкретного шлюза. Функция run_checkout вызывает единый метод и не знает, какой класс ей передан.
Закрепляем переопределение
Три тарифа переопределяют calculate_fee со своей формулой. Цикл вызывает один и тот же метод - Python сам подставляет нужную реализацию по типу объекта.
В других языках
| Критерий | Python | Java |
|---|---|---|
| Режим по умолчанию | Все методы виртуальные. Совпадение имени в потомке переопределяет родительский метод. | Требует совпадения сигнатур. Рекомендуется аннотация @Override. |
| Контроль типов | Отсутствует на уровне рантайма. | Контролируется компилятором. |
| Принцип поиска | Динамический поиск по MRO в момент выполнения. | По таблице виртуальных методов (VMT) на этапе компиляции. |
Риск переопределения
Причина: format_inn вернул None, а вызов None.upper() завершился ошибкой - у типа NoneType нет этого метода.
Риск переопределения
В компилируемых языках такая опечатка вызвала бы ошибку сборки. Python молча создаёт новый метод и вызывает родительский.
Иерархия типов
objectobject - это встроенный класс, который Python создал сам как фундамент всей системы типов. Он существует ещё до вашего кода: его не нужно ни определять, ни импортировать.
Любой класс в Python - в конечном счёте потомок object. Именно от него все объекты получают базовые возможности: __init__, __str__, __eq__, __hash__ и другие.
Иерархия типов
object автоматическиКогда вы пишете класс без указания родителя, Python молча добавляет object базовым классом. Две записи ниже полностью идентичны:
Скобки с object можно не писать - Python подставит его сам. Поэтому мы «не видим» родителя в коде, но он всегда там.
Иерархия типов
objectЕсли родительский класс не указан явно, Python наследует от object - вершины всей иерархии.
В других языках
object vs Java java.lang.Object| Параметр | Python | Java |
|---|---|---|
| Автосвязывание | Замыкает любую пользовательскую иерархию. | Абсолютный корень объектной модели. |
| Базовый набор | __new__, __init__, __repr__, __eq__. | toString(), equals(), hashCode(). |
| Множественное наследование | Финальная точка MRO. | Не применимо - Java поддерживает только одиночное наследование. |
Конфликт иерархий
Глубокий разбор
Python использует C3-линеаризацию: при вызове метода классы просматриваются по порядку MRO, выполняется первый найденный.
Кооперативное наследование
super() и MROsuper() вызывает метод не у «родителя из объявления», а у следующего класса в цепочке MRO текущего объекта.
super() внутри Left передал управление в Right, хотя они не связаны напрямую - это кооперативное множественное наследование.
Конфликт иерархий
Полиморфизм
Полиморфизм
Единый интерфейс позволяет обрабатывать наборы разнородных объектов в обычных циклах - без проверок типов через if isinstance().
Архитектура
С архитектурной точки зрения полиморфизм изолирует бизнес-логику от технических деталей конкретных вендоров.
При подключении нового шлюза (например, Click или Payme) не требуется менять код ядра. Достаточно создать класс - наследник PaymentGateway и реализовать process_payment. Остальное заработает автоматически.
Итоги
| Правило | Почему важно |
|---|---|
| Сохраняйте имя метода точно | Опечатка создаст новый метод, старый продолжит работать молча |
| Сохраняйте возвращаемый тип | Родительский код рассчитывает получить конкретный тип данных |
| Вызывайте super() при необходимости | Если базовая логика (аудит-лог) должна выполняться |
| Сохраняйте сигнатуру аргументов | Иначе полиморфный вызов получит неожиданные данные |
Динамический полиморфизм
«Если объект ведёт себя как утка - он и есть утка». Общие предки не обязательны: есть метод с нужным именем - его можно вызвать.
Внимание
ООП в реальных проектах
Scikit-learn целиком построен на утиной типизации: любая модель с методами .fit() и .predict() работает в общем пайплайне.
Итоги
| Характеристика | Переопределение | Утиная типизация |
|---|---|---|
| Требует общего предка | Да | Нет |
| Контроль контракта | Через иерархию | Только по имени метода |
| Когда заметны ошибки | На этапе проектирования | Только в рантайме |
| Где применяется | Шлюзы одной системы | Независимые библиотеки |
Карта курса
| Концепция | Статус | Описание |
|---|---|---|
| Наследование | 🟢 ПРОЙДЕНО | Синтаксис расширения классов и super(). |
| Полиморфизм | 🟢 ПРОЙДЕНО | Переопределение, MRO и утиная типизация. |
| Инкапсуляция | 🟡 ТЕКУЩАЯ ТЕМА | Сокрытие критических полей от внешнего доступа. |
| Абстракция | ⚪ ПРЕДСТОИТ | Отделение бизнес-логики от деталей реализации. |
Инкапсуляция
Пароль в открытом виде, доступный кому угодно на чтение и запись - прямая дорога к утечке.
Инкапсуляция
Инкапсуляция решает две задачи:
🔐 Изменить баланс или пароль напрямую нельзя. Для этого предоставляются методы change_password() (с проверкой старого пароля и хешированием) и deposit() (с валидацией суммы).
В других языках
Модификатор private блокирует доступ на уровне компиляции. Обращение к user.password извне - ошибка компилятора.
Физических ограничений нет. Защита строится на именовании атрибутов (_ и __). Разработчики придерживаются их по договорённости.
Инкапсуляция
_Одно подчёркивание - сигнал: «поле внутреннее, не предназначено для использования снаружи класса».
from module import *Инкапсуляция
__ и Name ManglingДвойное подчёркивание включает Name Mangling: __password_hash становится _ИмяКласса__password_hash. Это защищает поле от случайного переопределения при наследовании.
Инкапсуляция
@property - геттерыЕсли нужно читать данные, но контролировать процесс или маскировать вывод - используется @property. Он превращает метод в «виртуальный атрибут» - обращение без круглых скобок.
Зачем @property
Зачем @property
Зачем @property
Если атрибут был публичным, а потом потребовалась логика при чтении - @property позволяет добавить её без изменения синтаксиса вызова. Весь внешний код продолжает работать.
Поэтому в Python начинают с обычного атрибута и превращают его в @property только при появлении логики.
В других языках
Каждое поле с первого дня оборачивается в getBalance() / setBalance(), даже без логики внутри. Большой объём шаблонного кода.
Начинаем с обычного атрибута. При необходимости превращаем в @property - и весь внешний код продолжает работать без изменений.
Инкапсуляция
@property.setter - валидация при записиСеттер перехватывает присвоение через = и проверяет данные перед сохранением.
Под капотом
@property + setterИмена геттера и сеттера обязаны совпадать - поэтому декоратор записывается как @limit.setter, а не просто @setter.
Инкапсуляция
Инкапсуляция
Конструктор, защищённый баланс и пополнение с проверкой:
Баланс закрыт от прямой записи: читается через @property, меняется только через методы.
Инкапсуляция
Тот же класс - метод списания с двойной проверкой:
Метод сам проверяет сумму и остаток - некорректная операция просто не пройдёт.
Инкапсуляция
Инкапсуляция
| Инструмент | Что делает | Когда использовать |
|---|---|---|
| _name | Сигнал о внутренней принадлежности | Поля, не предназначенные для внешнего использования |
| __name | Name Mangling - переименовывает поле в памяти | Критичные данные: пароли, токены, ПИН-коды |
| @property | Контролируемое чтение | Логика при чтении, маскировка, вычисляемые значения |
| @name.setter | Валидация перед записью | Проверка данных перед сохранением |
| @property без setter | Только для чтения | Неизменяемые идентификаторы: ID контракта, номер счёта |
Итоги
| Концепция | Статус | Какую проблему решает | Ключевые инструменты |
|---|---|---|---|
| Наследование | 🟢 | Дублирование кода в похожих классах | class Child(Parent), super() |
| Полиморфизм | 🟢 | Зависимость кода от конкретных реализаций | Переопределение, Duck Typing, MRO |
| Инкапсуляция | 🟢 | Прямой доступ к данным ломает логику | _, __, @property, setter |
| Абстракция | ⚪ | Высокоуровневый код смешан с деталями | ABC, @abstractmethod |
Уровень класса
Атрибут экземпляра создаётся через self.поле отдельно для каждого объекта. Атрибут класса объявляется в теле класса - одна копия для всех экземпляров.
Уровень класса
При изменении через экземпляр (contract1.base_interest_rate = 0.25) Python создаст новый атрибут в этом экземпляре - атрибут класса останется прежним.
Практика
Два блокнота Jupyter к первой части лекции.
Откройте в Jupyter Notebook, JupyterLab или VS Code.