Лекция 7 · Часть 2

ООП в Python

Полиморфизм, инкапсуляция и проектирование отказоустойчивых банковских систем

Сегодня мы разберём, как проектировать гибкие и защищённые архитектуры. На реальных примерах из сферы финтеха - как управлять сложными потоками данных, защищать пароли от попадания в логи и организовывать работу десятков платёжных шлюзов через единый интерфейс.

Повторение Лекции 6

Базовые концепции

На прошлом занятии мы заложили фундамент: научились упаковывать состояние и поведение в классы, а также познакомились с наследованием.

Прежде чем перейти к более сложным концепциям - динамическому полиморфизму и инкапсуляции - необходимо закрепить терминологию на базовых примерах. Без этой базы в механизмах MRO и Name Mangling легко запутаться.

Повторение

Назначение классов

Сравним два подхода к управлению данными на примере ведения банковских счетов.

Процедурный подход
# данные и логика независимы; # при масштабировании - сложно acc1_number = "44001122" acc1_balance = 50000.0 acc2_number = "55003344" acc2_balance = 1200.0
ООП-подход
class BankAccount: def __init__(self, number: str, balance: float): self.number = number self.balance = balance acc1 = BankAccount("44001122", 50000.0)

Класс - пользовательский тип данных, связывающий финансовое состояние и логику работы с ним в единую структуру.

Повторение

Класс и экземпляр

Класс

Blueprint

Программное описание структуры. Сам по себе не хранит данные конкретного клиента и не занимает места в памяти под данные.

Экземпляр

Instance

Конкретный объект в памяти - например, счёт клиента с реальным балансом. По одному классу создаются миллионы изолированных объектов.

Повторение

Атрибуты и методы

1

Атрибуты

Переменные внутри объекта, отражающие его текущее состояние: account_number, currency, is_blocked.

2

Методы

Функции внутри класса с доступом к состоянию объекта: withdraw(), block_account().

💡 Хорошо спроектированный класс самостоятельно управляет своими атрибутами, а внешний код взаимодействует с ним только через вызовы методов.

Повторение

Метод __init__ и аргумент self

class BankAccount: def __init__(self, account_number: str, balance: float): print(f"Выделена память. self ссылается на адрес: {id(self)}") self.account_number = account_number self.balance = balance my_acc = BankAccount("44001122", 1000.0)
Выделена память. self ссылается на адрес: 140237482390224

__init__ - конструктор объекта, вызывается автоматически после выделения памяти. self - явная ссылка на текущий экземпляр.

Контекст

Python self vs Java this

Python

Явное объявление

def deposit(self, amount: float): self.balance += amount

self пишется первым аргументом каждого метода. «Явное лучше неявного».

Java

Неявная передача

public void deposit(double amount) { this.balance += amount; }

JVM передаёт ссылку автоматически, внутри доступна через this.

Повторение

Изоляция пространств имён объектов

class BankAccount: def __init__(self, owner: str, balance: float): self.owner = owner self.balance = balance alice_acc = BankAccount("Alice", 500.0) bob_acc = BankAccount("Bob", 10000.0) alice_acc.balance -= 50.0 print(f"Баланс Элис: {alice_acc.balance}") print(f"Баланс Боба: {bob_acc.balance}")
Баланс Элис: 450.0 Баланс Боба: 10000.0

Повторение

Метод __str__

class BankAccount: def __init__(self, owner: str, balance: float): self.owner = owner self.balance = balance def __str__(self) -> str: return f"Счёт клиента: {self.owner} | Баланс: {self.balance} USD" acc = BankAccount("Олег", 7500.0) print(acc)
Счёт клиента: Олег | Баланс: 7500.0 USD

Повторение

Сигнатуры методов

class BankAccount: def __init__(self, balance: float): self.balance = balance def transfer(self, target_acc: 'BankAccount', amount: float) -> bool: if self.balance >= amount: self.balance -= amount target_acc.balance += amount return True return False acc_a = BankAccount(100.0) acc_b = BankAccount(20.0) success = acc_a.transfer(acc_b, 50.0) print(f"Статус транзакции: {success} | Остаток: {acc_a.balance}")
Статус транзакции: True | Остаток: 50.0

Повторение

Наследование и super()

class BasicCard: def __init__(self, holder: str, number: str): self.holder = holder self.number = number class PremiumCard(BasicCard): def __init__(self, holder: str, number: str, cashback_rate: float): super().__init__(holder, number) self.cashback_rate = cashback_rate vip = PremiumCard("Asel", "4400...", 0.05) print(f"Карта: {vip.holder}, Кэшбэк: {vip.cashback_rate * 100}%")
Карта: Asel, Кэшбэк: 5.0%

Повторение

ООП повсюду в Python

"card_verified".upper() # метод экземпляра класса str balances.append(500.0) # изменение состояния экземпляра list df.dropna() # метод объекта DataFrame из pandas api.send_payment(payload) # клиент HTTP-сессии через requests / httpx

Работая с Python, вы используете ООП ежеминутно - почти каждый вызов через точку - это метод объекта.

Повторение

Сводная таблица

ПонятиеКороткоСмысл в архитектуре
Класс / ЭкземплярОписание структуры vs объект в RAMШаблон продукта и реальный счёт клиента
Атрибут / МетодПеременная состояния vs функцияБаланс (данные) и метод перевода (поведение)
__init__ / selfКонструктор и ссылка на контекстТочка входа при создании и ссылка на экземпляр
Наследование / super()Расширение без дублированияCreditAccount на базе BasicAccount

Карта курса

Текущий статус освоения ООП

КонцепцияСтатусЭтап
Наследование🟢 ПРОЙДЕНОСинтаксис расширения классов и super().
Полиморфизм🟡 ТЕКУЩАЯ ТЕМАЕдиный интерфейс для разных платёжных шлюзов.
Инкапсуляция⚪ ПРЕДСТОИТСокрытие критических данных и валидация.
Абстракция⚪ ПРЕДСТОИТОтделение бизнес-логики от деталей реализации.

Сегодня

План лекции №7

  • Полиморфизм - переопределение методов, утиная типизация, MRO и ромбовидное наследование
  • Инкапсуляция - модификаторы _ и __, @property, сеттеры
  • Инструменты уровня класса - @staticmethod, @classmethod
  • Магические методы - операторы +, ==, len() для пользовательских объектов
  • Исключения - пользовательские классы ошибок

Полиморфизм

Что такое полиморфизм?

Представьте платёжный терминал на кассе. Ему не важно, что именно клиент прикладывает: пластиковую карту UzCard, смартфон с Humo Pay или QR-код от Milly Bank. Для терминала существует один интерфейс - «принять оплату». А то, как именно происходит списание, зависит от конкретного платёжного средства.

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

Полиморфизм

Два пути реализации полиморфизма

1

Классический

Через наследование и переопределение. Создаётся родительский класс; потомки реализуют метод с одним именем, но собственной логикой.

2

Динамический

Утиная типизация. Наследование не требуется: если у независимых классов есть метод с одним именем, его можно вызвать полиморфно.

Полиморфизм

Механизмы полиморфизма в Python

┌──────────────────────────┐ │ ПОЛИМОРФИЗМ (концепция) │ └────────────┬─────────────┘ │ ┌──────────────────┴──────────────────┐ ▼ ▼ ┌──────────────────────────────┐ ┌──────────────────────────────┐ │ Классический (иерархический) │ │ Динамический (утиный) │ ├──────────────────────────────┤ ├──────────────────────────────┤ │ Требует общего родителя │ │ Наследование не требуется │ │ Переопределяет методы предка │ │ Важно только наличие метода │ │ Классы связаны в иерархии │ │ Классы полностью независимы │ └──────────────────────────────┘ └──────────────────────────────┘

Классический полиморфизм

Как работает переопределение

Переопределение - это когда класс-потомок объявляет метод с тем же именем, что и у родителя, но реализует его по-своему.

class Parent: def greet(self): print("Привет от родителя") class Child(Parent): def greet(self): # то же имя - своя реализация print("Привет от потомка") Child().greet()
Привет от потомка

При вызове obj.greet() Python ищет метод начиная с класса самого объекта. Версия из потомка «перекрывает» родительскую. А super().greet() позволяет дополнительно выполнить и версию родителя.

Классический полиморфизм

Переопределение методов

Базовый класс хранит общую логику. Класс-наследник переопределяет метод:

class PaymentGateway: def process_payment(self, amount: float) -> None: print(f"[Core-Banking] Фиксация транзакции на {amount} сум") class UzCardGateway(PaymentGateway): def process_payment(self, amount: float) -> None: super().process_payment(amount) # сначала логика ядра print(f"[UzCard API] Генерация QR-кода. Списание {amount} сум.") # затем своё

super() вызывает версию родителя. UzCardGateway добавляет своё поведение, не теряя общую логику ядра.

Классический полиморфизм

Тот же шаблон - другие шлюзы

Humo и Milly Bank переопределяют метод так же. run_checkout работает с любым шлюзом:

class HumoGateway(PaymentGateway): def process_payment(self, amount: float) -> None: super().process_payment(amount) print(f"[Humo API] Запрос к эквайрингу. Списание {amount} сум.") class MillyBankGateway(PaymentGateway): def process_payment(self, amount: float) -> None: super().process_payment(amount) print(f"[Milly Bank API] Онлайн-платёж. Сумма: {amount} сум.") def run_checkout(gateway: PaymentGateway, total: float): gateway.process_payment(total) # функция не знает тип шлюза run_checkout(UzCardGateway(), 150000.0) run_checkout(HumoGateway(), 320000.0)

Переопределение методов

Результат вызова

[Core-Banking] Фиксация транзакции на 150000.0 сум [UzCard API] Генерация QR-кода. Списание 150000.0 сум. [Core-Banking] Фиксация транзакции на 320000.0 сум [Humo API] Запрос к эквайрингу. Списание 320000.0 сум.

Каждый шлюз по-своему переопределяет process_payment. super() сначала выполняет общую логику ядра (запись в лог), затем - код конкретного шлюза. Функция run_checkout вызывает единый метод и не знает, какой класс ей передан.

Закрепляем переопределение

Расчёт комиссии за перевод

class BasicCardTariff: def calculate_fee(self, amount: float) -> float: return amount * 0.015 # 1.5% - стандартная комиссия class PremiumCardTariff(BasicCardTariff): def calculate_fee(self, amount: float) -> float: return amount * 0.005 # 0.5% - для премиум-клиентов class CorporateCardTariff(BasicCardTariff): def calculate_fee(self, amount: float) -> float: return 50000.0 # фикс для юридических лиц tariffs_pool = [BasicCardTariff(), PremiumCardTariff(), CorporateCardTariff()] for tariff in tariffs_pool: fee = tariff.calculate_fee(10_000_000.0) print(f"Тариф: {tariff.__class__.__name__} | Комиссия: {fee} сум")
Тариф: BasicCardTariff | Комиссия: 150000.0 сум Тариф: PremiumCardTariff | Комиссия: 50000.0 сум Тариф: CorporateCardTariff | Комиссия: 50000.0 сум

Три тарифа переопределяют calculate_fee со своей формулой. Цикл вызывает один и тот же метод - Python сам подставляет нужную реализацию по типу объекта.

В других языках

Сравнение переопределения: Python vs Java

КритерийPythonJava
Режим по умолчаниюВсе методы виртуальные. Совпадение имени в потомке переопределяет родительский метод.Требует совпадения сигнатур. Рекомендуется аннотация @Override.
Контроль типовОтсутствует на уровне рантайма.Контролируется компилятором.
Принцип поискаДинамический поиск по MRO в момент выполнения.По таблице виртуальных методов (VMT) на этапе компиляции.

Риск переопределения

Хрупкий базовый класс

class DocumentProcessor: def format_inn(self, raw_inn: str) -> str: return raw_inn.strip() # гарантирует возврат str def register_legal_entity(self, inn: str) -> str: clean_inn = self.format_inn(inn) return f"Регистрация компании с ИНН: {clean_inn.upper()}" class BadCustomProcessor(DocumentProcessor): def format_inn(self, raw_inn: str) -> None: # super() не вызван; нет return -> метод возвращает None print(f"[Лог] Запрос на проверку ИНН: {raw_inn}") processor = BadCustomProcessor() try: processor.register_legal_entity(" 123456789012 ") except AttributeError as error: print(f"ОШИБКА В РАНТАЙМЕ: {error}")
[Лог] Запрос на проверку ИНН: 123456789012 ОШИБКА В РАНТАЙМЕ: 'NoneType' object has no attribute 'upper'

Причина: format_inn вернул None, а вызов None.upper() завершился ошибкой - у типа NoneType нет этого метода.

Риск переопределения

Опечатка в имени метода

class NotificationService: def send_critical_alert(self, msg: str): print(f"[Fallback SMS] Критическая ошибка: {msg}") class TelegramService(NotificationService): def send_criticall_alert(self, msg: str): # опечатка: двойная 'l' print(f"[Telegram Bot] Ошибка отправлена в чат: {msg}") notifier = TelegramService() notifier.send_critical_alert("Упал сервер авторизации!")
[Fallback SMS] Критическая ошибка: Упал сервер авторизации!

В компилируемых языках такая опечатка вызвала бы ошибку сборки. Python молча создаёт новый метод и вызывает родительский.

Иерархия типов

Что такое object

object - это встроенный класс, который Python создал сам как фундамент всей системы типов. Он существует ещё до вашего кода: его не нужно ни определять, ни импортировать.

print(object) # сам корневой класс print(type(object)) # его тип print(int.__bases__) # даже int - потомок object
<class 'object'> <class 'type'> (<class 'object'>,)

Любой класс в Python - в конечном счёте потомок object. Именно от него все объекты получают базовые возможности: __init__, __str__, __eq__, __hash__ и другие.

Иерархия типов

Python подставляет object автоматически

Когда вы пишете класс без указания родителя, Python молча добавляет object базовым классом. Две записи ниже полностью идентичны:

class Account: # так пишем мы pass class Account(object): # так это видит Python pass print(Account.__bases__) # родитель есть, хотя мы его не писали print(issubclass(Account, object))
(<class 'object'>,) True

Скобки с object можно не писать - Python подставит его сам. Поэтому мы «не видим» родителя в коде, но он всегда там.

Иерархия типов

Что класс получает от object

class SimpleEntity: pass entity = SimpleEntity() print(f"Базовые предки класса: {SimpleEntity.__bases__}") print(f"__str__: {entity.__str__()}") print(f"__eq__: {entity.__eq__(entity)}") print(f"__dir__: {len(entity.__dir__())} элементов")
Базовые предки класса: (<class 'object'>,) __str__: <__main__.SimpleEntity object at 0x104b2b9a0> __eq__: True __dir__: 26 элементов

Если родительский класс не указан явно, Python наследует от object - вершины всей иерархии.

В других языках

Python object vs Java java.lang.Object

ПараметрPythonJava
АвтосвязываниеЗамыкает любую пользовательскую иерархию.Абсолютный корень объектной модели.
Базовый набор__new__, __init__, __repr__, __eq__.toString(), equals(), hashCode().
Множественное наследованиеФинальная точка MRO.Не применимо - Java поддерживает только одиночное наследование.

Конфликт иерархий

Ромбовидное наследование (Diamond Problem)

┌────────────────┐ │ BaseValidator │ объявляет validate() └───────┬────────┘ ┌─────────┴─────────┐ ▼ ▼ ┌───────────────┐ ┌───────────────┐ │ CardCheck │ │ LimitCheck │ оба переопределили └───────┬───────┘ └───────┬───────┘ └─────────┬─────────┘ ▼ ┌───────────────┐ │ FullCheck │ чья реализация validate()? └───────────────┘

Глубокий разбор

MRO - порядок разрешения методов

Python использует C3-линеаризацию: при вызове метода классы просматриваются по порядку MRO, выполняется первый найденный.

class BaseValidator: def validate(self): print("Реализация: BaseValidator") class CardCheck(BaseValidator): def validate(self): print("Реализация: CardCheck") class LimitCheck(BaseValidator): def validate(self): print("Реализация: LimitCheck") class FullCheck(CardCheck, LimitCheck): pass checker = FullCheck() checker.validate() for i, cls in enumerate(FullCheck.__mro__, start=1): print(f"{i}. {cls.__name__}")
Реализация: CardCheck 1. FullCheck 2. CardCheck 3. LimitCheck 4. BaseValidator 5. object

Кооперативное наследование

super() и MRO

super() вызывает метод не у «родителя из объявления», а у следующего класса в цепочке MRO текущего объекта.

class Base: def __init__(self): print("Инициализация Base") class Left(Base): def __init__(self): print("Старт Left"); super().__init__(); print("Конец Left") class Right(Base): def __init__(self): print("Старт Right"); super().__init__(); print("Конец Right") class Child(Left, Right): def __init__(self): print("Старт Child"); super().__init__(); print("Конец Child") instance = Child() # MRO: Child -> Left -> Right -> Base
Старт Child -> Старт Left -> Старт Right -> Инициализация Base -> Конец Right -> Конец Left -> Конец Child

super() внутри Left передал управление в Right, хотя они не связаны напрямую - это кооперативное множественное наследование.

Конфликт иерархий

Ошибки линеаризации

class X: pass class Y(X): pass try: class InvalidSystem(X, Y): # X не может стоять раньше потомка Y pass except TypeError as error: print(f"Ошибка построения иерархии:\n{error}")
Ошибка построения иерархии: Cannot create a consistent method resolution order (MRO) for bases X, Y

Полиморфизм

Разрешение вызовов в Runtime

gateways = [UzCardGateway(), HumoGateway(), MillyBankGateway()] def run_billing(gateway_instance, sum_to_charge): # Python не проверяет тип заранее. # В момент вызова он просматривает MRO переданного экземпляра. gateway_instance.process_payment(sum_to_charge) for gw in gateways: run_billing(gw, 100000.0)

Полиморфизм

Полиморфная обработка коллекций

Единый интерфейс позволяет обрабатывать наборы разнородных объектов в обычных циклах - без проверок типов через if isinstance().

processing_pool = [UzCardGateway(), HumoGateway(), MillyBankGateway(), UzCardGateway()] for gateway in processing_pool: gateway.process_payment(50000.0) print("--- Лог транзакции сохранён ---")

Архитектура

Программный интерфейс как контракт

С архитектурной точки зрения полиморфизм изолирует бизнес-логику от технических деталей конкретных вендоров.

При подключении нового шлюза (например, Click или Payme) не требуется менять код ядра. Достаточно создать класс - наследник PaymentGateway и реализовать process_payment. Остальное заработает автоматически.

Итоги

Переопределение методов - резюме

ПравилоПочему важно
Сохраняйте имя метода точноОпечатка создаст новый метод, старый продолжит работать молча
Сохраняйте возвращаемый типРодительский код рассчитывает получить конкретный тип данных
Вызывайте super() при необходимостиЕсли базовая логика (аудит-лог) должна выполняться
Сохраняйте сигнатуру аргументовИначе полиморфный вызов получит неожиданные данные
PaymentGateway ← общий контракт ├── UzCardGateway ← своя реализация ├── HumoGateway ← своя реализация └── MillyBankGateway ← своя реализация

Динамический полиморфизм

Утиная типизация (Duck Typing)

«Если объект ведёт себя как утка - он и есть утка». Общие предки не обязательны: есть метод с нужным именем - его можно вызвать.

class ApplePayService: def charge_money(self, total: float): print(f"[Apple Pay] Списание токена карты: {total} сум") class GooglePayService: # не связан с ApplePayService иерархией, но имя метода совпадает def charge_money(self, total: float): print(f"[Google Pay] Запрос к Google Wallet API: {total} сум") def process_mobile_wallet(payment_service, amount: float): payment_service.charge_money(amount) # важно лишь наличие метода process_mobile_wallet(ApplePayService(), 120000.0) process_mobile_wallet(GooglePayService(), 120000.0)

Внимание

Риски утиной типизации в продакшене

class TaxService: def charge_money(self, total: float): # имя совпадает с платёжными шлюзами, но логика другая print(f"[Налоговая] Списание штрафов: {total} сум") class FakePayGateway: def charge_monney(self, total: float): # опечатка в имени print(total) # Риск 1: AttributeError из-за опечатки try: process_mobile_wallet(FakePayGateway(), 100.0) except AttributeError as err: print(f"Ошибка выполнения: {err}") # Риск 2: семантическая ошибка - код выполнится, логика нарушена process_mobile_wallet(TaxService(), 500000.0)

ООП в реальных проектах

Duck Typing в аналитических библиотеках

from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier # объекты разных классов дают единый интерфейс: .fit() и .predict() scoring_models = [LogisticRegression(), RandomForestClassifier()] for model in scoring_models: model.fit(X_train, y_train) predictions = model.predict(X_test)

Scikit-learn целиком построен на утиной типизации: любая модель с методами .fit() и .predict() работает в общем пайплайне.

Итоги

Полиморфизм - полное резюме

ХарактеристикаПереопределениеУтиная типизация
Требует общего предкаДаНет
Контроль контрактаЧерез иерархиюТолько по имени метода
Когда заметны ошибкиНа этапе проектированияТолько в рантайме
Где применяетсяШлюзы одной системыНезависимые библиотеки
gateway.process_payment(amount) ↓ Python смотрит в MRO объекта ↓ → вызывает нужную реализацию автоматически

Карта курса

Статус освоения концепций ООП

КонцепцияСтатусОписание
Наследование🟢 ПРОЙДЕНОСинтаксис расширения классов и super().
Полиморфизм🟢 ПРОЙДЕНОПереопределение, MRO и утиная типизация.
Инкапсуляция🟡 ТЕКУЩАЯ ТЕМАСокрытие критических полей от внешнего доступа.
Абстракция⚪ ПРЕДСТОИТОтделение бизнес-логики от деталей реализации.

Инкапсуляция

Проблема - свободная модификация состояния

class BankUser: def __init__(self, login: str, raw_password: str): self.login = login self.password = raw_password # пароль хранится в открытом виде user = BankUser("manager_olga", "123456") # любой разработчик может изменить пароль напрямую: user.password = "HACKED_OR_CLEARED" # или прочитать его без какого-либо хеширования: print(f"Пароль в логах: {user.password}")

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

Инкапсуляция

Задачи инкапсуляции

Инкапсуляция решает две задачи:

  • Объединить данные и методы работы с ними в одну структуру
  • Скрыть внутренние детали, предоставив наружу только безопасный публичный интерфейс с явной валидацией

🔐 Изменить баланс или пароль напрямую нельзя. Для этого предоставляются методы change_password() (с проверкой старого пароля и хешированием) и deposit() (с валидацией суммы).

В других языках

Философия управления доступом

Java

Жёсткий запрет

Модификатор private блокирует доступ на уровне компиляции. Обращение к user.password извне - ошибка компилятора.

Python

Соглашение

Физических ограничений нет. Защита строится на именовании атрибутов (_ и __). Разработчики придерживаются их по договорённости.

Инкапсуляция

Защищённые атрибуты: _

Одно подчёркивание - сигнал: «поле внутреннее, не предназначено для использования снаружи класса».

  • Технически доступ открыт
  • IDE подсвечивает такое обращение как нарушение стиля
  • Не экспортируется при from module import *
class InternalBankConfig: def __init__(self): self._swift_code = "UZUZBGBU" # защищённый атрибут по соглашению config = InternalBankConfig() print(config._swift_code) # работает, но нарушает архитектурный контракт

Инкапсуляция

Приватные атрибуты: __ и Name Mangling

Двойное подчёркивание включает Name Mangling: __password_hash становится _ИмяКласса__password_hash. Это защищает поле от случайного переопределения при наследовании.

import hashlib class SecureUser: def __init__(self, login: str, raw_password: str): self.login = login self.__password_hash = hashlib.sha256(raw_password.encode()).hexdigest() user = SecureUser("admin_damir", "qwerty2026") try: print(user.__password_hash) # атрибута с таким именем не существует except AttributeError as error: print(f"Система безопасности: {error}") print(f"Обходной путь: {user._SecureUser__password_hash}")
Система безопасности: 'SecureUser' object has no attribute '__password_hash' Обходной путь: 4bc82b9a76d...

Инкапсуляция

Декоратор @property - геттеры

Если нужно читать данные, но контролировать процесс или маскировать вывод - используется @property. Он превращает метод в «виртуальный атрибут» - обращение без круглых скобок.

class CreditCard: def __init__(self, client_name: str, raw_pan: str): self.client_name = client_name self._pan = raw_pan @property def masked_pan(self) -> str: print("[Security Audit] Запрос на чтение номера карты.") return f"{self._pan[:4]} **** **** {self._pan[-4:]}" card = CreditCard("Arman", "8600112233445566") print(f"Номер карты: {card.masked_pan}") # без скобок - как атрибут
[Security Audit] Запрос на чтение номера карты. Номер карты: 8600 **** **** 5566

Зачем @property

Причина 1 - логика при чтении

class UserAccount: def __init__(self, email: str): self._email = email self._read_count = 0 @property def email(self) -> str: self._read_count += 1 return self._email acc = UserAccount("user@millybank.uz") print(acc.email) print(acc.email) print(f"Email запрашивали {acc._read_count} раза")
user@millybank.uz user@millybank.uz Email запрашивали 2 раза

Зачем @property

Причина 2 - вычисляемые значения

class LoanPortfolio: def __init__(self, principal: float, interest_rate: float): self.principal = principal self.interest_rate = interest_rate @property def total_debt(self) -> float: return self.principal * (1 + self.interest_rate) portfolio = LoanPortfolio(10_000_000, 0.20) print(f"Итого к возврату: {portfolio.total_debt} сум") portfolio.principal -= 2_000_000 print(f"После частичного погашения: {portfolio.total_debt} сум")
Итого к возврату: 12000000.0 сум После частичного погашения: 9600000.0 сум

Зачем @property

Причина 3 - совместимость интерфейса

Если атрибут был публичным, а потом потребовалась логика при чтении - @property позволяет добавить её без изменения синтаксиса вызова. Весь внешний код продолжает работать.

# Было: acc.balance → просто читает значение # Стало: acc.balance → читает значение И пишет в аудит-лог # Синтаксис вызова во всём проекте не изменился

Поэтому в Python начинают с обычного атрибута и превращают его в @property только при появлении логики.

В других языках

Подходы к геттерам и сеттерам

Java

Boilerplate с первого дня

Каждое поле с первого дня оборачивается в getBalance() / setBalance(), даже без логики внутри. Большой объём шаблонного кода.

Python

По необходимости

Начинаем с обычного атрибута. При необходимости превращаем в @property - и весь внешний код продолжает работать без изменений.

Инкапсуляция

@property.setter - валидация при записи

Сеттер перехватывает присвоение через = и проверяет данные перед сохранением.

class CreditLimitManager: def __init__(self, current_limit: float): self._limit = current_limit @property def limit(self) -> float: return self._limit @limit.setter def limit(self, new_value: float): print(f"Попытка изменить кредитный лимит на: {new_value} сум") if new_value < 0: raise ValueError("Кредитный лимит не может быть отрицательным!") if new_value > 500_000_000: raise ValueError("Превышен максимально допустимый лимит филиала!") self._limit = new_value manager = CreditLimitManager(50_000_000) try: manager.limit = 700_000_000 except ValueError as error: print(f"Операция отклонена: {error}")
Попытка изменить кредитный лимит на: 700000000 сум Операция отклонена: Превышен максимально допустимый лимит филиала!

Под капотом

Как работает связка @property + setter

@property → Python создаёт дескриптор 'limit' с логикой чтения @limit.setter → к тому же дескриптору добавляется логика записи manager.limit → вызывает limit.fget(manager) manager.limit = 700000 → вызывает limit.fset(manager, 700000)

Имена геттера и сеттера обязаны совпадать - поэтому декоратор записывается как @limit.setter, а не просто @setter.

class PinValidator: def __init__(self): self._pin = None @property def pin(self) -> str: return "****" # реальное значение не возвращается @pin.setter def pin(self, value: str): if not value.isdigit() or len(value) != 4: raise ValueError("ПИН-код должен состоять ровно из 4 цифр!") self._pin = value print("ПИН-код успешно обновлён.")

Инкапсуляция

Свойство без сеттера - только для чтения

class Contract: def __init__(self, contract_id: str, client: str): self._contract_id = contract_id self.client = client @property def contract_id(self) -> str: """Идентификатор контракта - неизменяем после создания""" return self._contract_id contract = Contract("UZ-2024-001", "Тимур") print(contract.contract_id) try: contract.contract_id = "UZ-2024-999" except AttributeError as e: print(f"Защита сработала: {e}")
UZ-2024-001 Защита сработала: can't set attribute

Инкапсуляция

Эталонный класс BankAccount

Конструктор, защищённый баланс и пополнение с проверкой:

class BankAccount: def __init__(self, owner: str, initial_balance: float = 0.0): self.owner = owner self._balance = initial_balance # защищённый баланс @property def balance(self) -> float: return self._balance def deposit(self, amount: float) -> None: if amount <= 0: print("Ошибка: сумма пополнения должна быть больше нуля.") return self._balance += amount

Баланс закрыт от прямой записи: читается через @property, меняется только через методы.

Инкапсуляция

BankAccount: списание средств

Тот же класс - метод списания с двойной проверкой:

def withdraw(self, amount: float) -> None: if amount <= 0: print("Ошибка: сумма списания должна быть положительной.") return if amount > self._balance: print("Транзакция заблокирована: недостаточно средств.") return self._balance -= amount acc = BankAccount("Дмитрий", 1_000_000.0) acc.withdraw(1_500_000.0)
Транзакция заблокирована: недостаточно средств.

Метод сам проверяет сумму и остаток - некорректная операция просто не пройдёт.

Инкапсуляция

Динамически вычисляемые свойства

class LoanPortfolio: def __init__(self, principal: float, interest_rate: float): self.principal = principal self.interest_rate = interest_rate @property def total_debt(self) -> float: """Вычисляется при каждом обращении по актуальным данным""" return self.principal * (1 + self.interest_rate) portfolio = LoanPortfolio(10_000_000, 0.20) print(f"Итого к возврату: {portfolio.total_debt} сум") portfolio.principal -= 2_000_000 print(f"После частичного погашения: {portfolio.total_debt} сум")
Итого к возврату: 12000000.0 сум После частичного погашения: 9600000.0 сум

Инкапсуляция

Сводная таблица инструментов инкапсуляции

ИнструментЧто делаетКогда использовать
_nameСигнал о внутренней принадлежностиПоля, не предназначенные для внешнего использования
__nameName Mangling - переименовывает поле в памятиКритичные данные: пароли, токены, ПИН-коды
@propertyКонтролируемое чтениеЛогика при чтении, маскировка, вычисляемые значения
@name.setterВалидация перед записьюПроверка данных перед сохранением
@property без setterТолько для чтенияНеизменяемые идентификаторы: ID контракта, номер счёта

Итоги

Три концепции ООП - сводная матрица

КонцепцияСтатусКакую проблему решаетКлючевые инструменты
Наследование🟢Дублирование кода в похожих классахclass Child(Parent), super()
Полиморфизм🟢Зависимость кода от конкретных реализацийПереопределение, Duck Typing, MRO
Инкапсуляция🟢Прямой доступ к данным ломает логику_, __, @property, setter
АбстракцияВысокоуровневый код смешан с деталямиABC, @abstractmethod
Наследование → создаём иерархию: UzCardGateway(PaymentGateway) Полиморфизм → единый интерфейс: gateway.process_payment() Инкапсуляция → защищаем данные: self.__pin_hash, @property

Уровень класса

Атрибут экземпляра vs атрибут класса

Атрибут экземпляра создаётся через self.поле отдельно для каждого объекта. Атрибут класса объявляется в теле класса - одна копия для всех экземпляров.

class NationalBankCreditProduct: base_interest_rate = 0.20 # атрибут класса - общий для всех def __init__(self, client_name: str, loan_amount: float): self.client_name = client_name # атрибут экземпляра self.loan_amount = loan_amount contract1 = NationalBankCreditProduct("Тимур", 50_000_000) contract2 = NationalBankCreditProduct("Елена", 90_000_000) print(f"Ставка для Тимура: {contract1.base_interest_rate * 100}%") print(f"Ставка для Елены: {contract2.base_interest_rate * 100}%")
Ставка для Тимура: 20.0% Ставка для Елены: 20.0%

Уровень класса

Изменение атрибута класса затрагивает всех

print("--- До изменения ставки ---") print(f"Тимур: {contract1.base_interest_rate * 100}%") print(f"Елена: {contract2.base_interest_rate * 100}%") NationalBankCreditProduct.base_interest_rate = 0.22 print("--- После изменения ставки ---") print(f"Тимур: {contract1.base_interest_rate * 100}%") print(f"Елена: {contract2.base_interest_rate * 100}%")
--- До изменения ставки --- Тимур: 20.0% Елена: 20.0% --- После изменения ставки --- Тимур: 22.0% Елена: 22.0%

При изменении через экземпляр (contract1.base_interest_rate = 0.25) Python создаст новый атрибут в этом экземпляре - атрибут класса останется прежним.

Практика

Материалы для практики

Два блокнота Jupyter к первой части лекции.

↓ Задания и примеры ↓ Сниппеты для практики

Откройте в Jupyter Notebook, JupyterLab или VS Code.

RU UZ EN
Python · Лекция 7 · ч.1
1 / 64