Лекция 7 · День 2
Свойства, инструменты уровня класса и магические методы
Повторение
ООП держится на нескольких базовых концепциях. Три из них мы уже разобрали на прошлых занятиях.
Класс-наследник переиспользует код родителя: class Child(Parent) и super().
Один вызов работает для разных классов: переопределение, Duck Typing, MRO.
Внутренние данные скрыты от прямого доступа: соглашение _ и приватность __.
Повторение · Полиморфизм
Наследник задаёт свою версию метода. Один вызов gateway.process() даёт разное поведение.
Наследование не требуется, достаточно нужного метода. «Крякает как утка, значит утка».
При множественном наследовании Python ищет метод по __mro__ слева направо.
Полиморфизм даёт единый интерфейс для разных реализаций. Вызывающий код не зависит от конкретного класса.
Повторение · Инкапсуляция
| Инструмент | Смысл |
|---|---|
_имя | Соглашение: «поле внутреннее, снаружи не трогать» |
__имя | Name Mangling: Python переименовывает поле в _Класс__имя |
Оба инструмента работают по принципу «всё или ничего»: поле либо просто просят не трогать, либо прячут полностью. Промежуточного варианта пока нет: открыть поле для чтения и записи, но под контролем.
План
@property, сеттеры, валидация, вычисляемые и read-only свойства
Атрибуты класса, @classmethod, @staticmethod
Магические (дандер) методы
Между темами два практических блока в Jupyter. В конце итоговый проект «Платёжная карта».
Свойства
Обычный публичный атрибут Python отдаёт «как есть». При чтении просто возвращается значение из памяти. Вставить проверку, запись в журнал или пересчёт некуда.
Хотелось бы при чтении card.pan показывать только часть номера. Но обычный атрибут так не умеет: он не выполняет код.
Свойства
@property@property превращает метод в «виртуальное поле». Снаружи объект читают как атрибут, без скобок. Но Python при этом вызывает метод, а внутри метода можно выполнять любой код.
Аналогия: вместо ключа от хранилища (прямой доступ к полю) мы ставим окно выдачи (метод). Снаружи так же удобно, внутри всё под контролем.
Свойства
Геттер, который просто возвращает значение, сам по себе бесполезен. Свойство нужно, когда при обращении к полю должна выполняться работа. Три типичные причины:
При каждом обращении к полю выполняется код: маскировка, запись в журнал, проверка.
Значение не хранится, а считается на лету по актуальным данным объекта.
Публичный атрибут превращается в свойство без изменения внешнего кода.
Свойства · Причина 1
Добавим в свойство код. При каждом чтении оно маскирует номер карты и записывает обращение в журнал безопасности.
Свойства · Причина 2
Свойство не хранит значение, а пересчитывает его при каждом обращении по актуальным данным объекта. Меняются данные, меняется и результат.
Свойства · Причина 3
Каждое поле сразу оборачивают в getBalance() и setBalance(), даже когда логики внутри ещё нет.
Начинаем с обычного атрибута. Когда понадобится логика, превращаем его в @property. Внешний код продолжает работать без изменений.
Поэтому в Python не пишут геттеры заранее. Обычный атрибут становится свойством только тогда, когда появляется логика.
Свойства · Разбор вопроса
В Python нет жёсткой приватности: до поля _pan можно дотянуться напрямую и даже изменить его. Подчёркивание _ ничего не запрещает технически.
Подчёркивание _ работает как соглашение, а не замок. Оно говорит: «поле внутреннее, снаружи его трогать не нужно». Но физически доступ открыт.
Свойства · Разбор вопроса
Смысл геттера не в том, чтобы сделать доступ невозможным. Смысл в том, чтобы правильный путь был самым удобным. Тогда обычный код идёт через него сам собой.
Через него ходит весь обычный код. Маскировка, запись в журнал и проверки включаются автоматически.
Технически открыта. Но обращаться к ней снаружи значит осознанно нарушать договорённость, и на ревью кода это заметно.
Свойства · Разбор вопроса
Представьте: через полгода правила изменились. Номер карты больше нельзя хранить открыто, теперь он шифруется. Меняем внутреннее устройство класса:
Код, который читал card.masked_pan, продолжит работать без единой правки. Код, который брал card._pan напрямую, сломался бы везде: поля больше нет.
Свойства
Пока @property управляет только чтением. Чтобы перехватить запись, то есть присвоение через =, к свойству добавляют сеттер: метод с декоратором @имя.setter.
Геттер и сеттер носят одно имя balance. Теперь поле можно и читать, и присваивать, но запись пройдёт через метод.
Свойства
Главная польза сеттера: проверить значение перед сохранением. Если оно недопустимо, сеттер поднимает ошибку, и поле остаётся прежним.
Под капотом
Геттер и сеттер вместе образуют одно свойство. Python сам выбирает нужный метод: читаем мы поле или присваиваем ему значение.
Имена геттера и сеттера обязаны совпадать. Поэтому декоратор пишется как @limit.setter: он привязывается к уже созданному свойству limit.
Свойства
Геттер не отдаёт реальный ПИН наружу, а сеттер проверяет формат при каждой попытке записать новое значение.
Свойства
Если у свойства нет сеттера, присвоить ему значение нельзя: Python поднимет ошибку. Так защищают поля, которые не должны меняться после создания.
Свойства
Соберём всё вместе: защищённый баланс, чтение через свойство и пополнение с проверкой.
Баланс закрыт от прямой записи: читается через @property, меняется только через методы.
Свойства
Тот же класс. Метод списания с двумя проверками: сумма и остаток.
Метод сам проверяет сумму и остаток. Некорректная операция просто не пройдёт.
Инкапсуляция
| Инструмент | Что делает | Когда использовать |
|---|---|---|
| _name | Сигнал: поле внутреннее | Поля, не предназначенные для внешнего кода |
| __name | Name Mangling: переименовывает поле | Критичные данные: пароли, токены, ПИН-коды |
| @property | Контролируемое чтение | Логика при чтении, маскировка, вычисляемые значения |
| @name.setter | Проверка перед записью | Валидация данных перед сохранением |
| @property без сеттера | Только для чтения | Неизменяемые поля: номер контракта, ID счёта |
Уровень класса
Атрибут экземпляра создаётся через self.поле отдельно для каждого объекта. Атрибут класса объявляется в теле класса - одна копия для всех экземпляров.
Уровень класса
При изменении через экземпляр (contract1.base_interest_rate = 0.25) Python создаст новый атрибут в этом экземпляре - атрибут класса останется прежним.
Практика · Jupyter
Откройте блокнот oop_practicum.ipynb и выполните задания на разобранные темы. Проверяйте вывод по комментарию # Ожидаемый вывод.
_ и __, name mangling
Геттер и сеттер с валидацией
Вычисляемые свойства
Атрибут класса vs экземпляра
Уровень класса
@classmethodУ класса один конструктор, метод __init__. Он ждёт готовые поля, разложенные по местам. Но данные часто приходят в другом виде: JSON из API, строка из CSV-файла, просто текущее время.
Если объект так создают в десяти местах проекта, разбор JSON повторяется десять раз. Логика создания «размазана» по коду.
Уровень класса
@classmethodМетод с декоратором @classmethod работает как второй, именованный конструктор. Первым аргументом он получает не объект self, а сам класс cls.
selfВ метод приходит конкретный объект. Метод работает с его данными, например self.amount.
clsВ метод приходит сам класс. Вызов cls(...) создаёт новый объект, так же как Transaction(...).
Логику разбора данных прячут внутрь метода класса. Снаружи остаётся короткий вызов: Transaction.from_json(raw).
Уровень класса
Соберём решение в коде. Метод from_json разбирает строку внутри себя и возвращает готовый объект через cls(...).
@classmethod получает cls и создаёт объект через cls(...) - это второй способ собрать Transaction.
Уровень класса
Ещё два фабричных метода того же класса Transaction:
Один класс - несколько точек входа: из JSON, из CSV, из текущего времени.
Уровень класса
cls vs жёсткое имя класса при наследованииВнутри фабричного метода объект создают через cls(...), а не через имя класса напрямую. Тогда метод правильно работает и в подклассах.
С BaseCard(...) вместо cls(...) объект gold оказался бы экземпляром BaseCard, а не GoldCard.
Уровень класса
@staticmethodБывает функция, которая логически принадлежит классу, но не работает ни с конкретным объектом, ни с самим классом. Ей не нужны ни self, ни cls.
Пример: проверка формата номера телефона рядом с классом PhoneValidator. Это чистая утилита: получает строку, возвращает строку. Её можно вынести в обычную функцию, но тогда теряется связь с классом, к которому она относится.
Декоратор @staticmethod позволяет положить такую утилиту внутрь класса. Она группируется рядом с родственным кодом, но не получает ни объект, ни класс.
Уровень класса
@staticmethodМетод с @staticmethod объявляют без self и cls. Вызывают его через класс. Здесь это утилита, приводящая номер телефона к единому формату.
Уровень класса
@staticmethod группирует утилиты внутри класса, экспортирует их вместе с ним и допускает переопределение в подклассах.
Уровень класса
Обычный метод принимает self и работает с данными конкретного объекта:
Уровень класса
Тот же класс - метод класса (получает cls) и статический метод (без контекста):
Уровень класса
| Декоратор | Первый аргумент | Доступный контекст | Назначение |
|---|---|---|---|
| Обычный метод | self | Атрибуты экземпляра и класса | Изменение состояния объекта |
| @classmethod | cls | Только атрибуты и методы класса | Альтернативные конструкторы |
| @staticmethod | отсутствует | Нет доступа к контексту | Утилиты и валидаторы |
Уровень класса
Практика · Jupyter
Продолжаем в oop_practicum.ipynb: задания на инструменты уровня класса и магические методы.
Фабричный конструктор from_application
Валидатор validate_pan
__len__ для истории транзакций
Протоколы
Дандер-методы (от Double Underscore) позволяют интегрировать пользовательские объекты со стандартными операторами и функциями Python.
Переопределяя __add__, __eq__, __len__ и другие, вы определяете, как объекты складываются, сравниваются и ведут себя в стандартных конструкциях языка.
Протоколы
| Метод | Что переопределяет | Пример применения |
|---|---|---|
| __init__ | Конструктор | Инициализация полей счёта |
| __str__ | print(obj), str(obj) | Форматированный вывод в логах |
| __repr__ | Отладочное представление | repr(obj) для разработчика |
| __eq__ | Оператор == | Сравнение счетов по балансу |
| __lt__ | Оператор < | Сортировка счетов |
| __add__ | Оператор + | Объединение портфелей |
| __len__ | Функция len(obj) | Количество транзакций в истории |
| __contains__ | Оператор in | Проверка наличия транзакции |
Итоги
| Концепция | Статус | Какую проблему решает | Ключевые инструменты |
|---|---|---|---|
| Наследование | 🟢 | Дублирование кода в похожих классах | class Child(Parent), super() |
| Полиморфизм | 🟢 | Зависимость кода от конкретных реализаций | Переопределение, Duck Typing, MRO |
| Инкапсуляция | 🟢 | Прямой доступ к данным ломает логику | _, __, @property, setter |
| Абстракция | ⚪ | Высокоуровневый код смешан с деталями | ABC, @abstractmethod |
Итоги
_, __, @property и сеттеры защищают данные и контролируют доступ@classmethod - несколько точек входа в класс, корректно работает при наследовании через cls@staticmethod - изолированные утилиты, сгруппированные внутри классаИтоговый проект
Итоговое задание практикума объединяет все темы лекции 7 в одной системе процессинга карт.
BankCard с приватным ПИН и @property-валидацией (ровно 4 цифры)@staticmethod validate_pan и @classmethod from_applicationDebitCard и CreditCard: переопределение process_paymentCardBlockedError: блокировка после 3 неверных ПИН__str__, __eq__Часть A и часть B в oop_practicum.ipynb, каждая с ячейкой-проверкой.
Материалы
Все задания по ООП лекций 6-7 в одном блокноте Jupyter.
14 заданий по темам + мини-проект «Платёжная карта». Откройте в Jupyter Notebook, JupyterLab, VS Code или Google Colab.