На предыдущем занятии мы подробно разобрали механизм инкапсуляции: как скрывать данные внутри объекта и работать с ними через сеттеры и геттеры.
Этот подход решает главную задачу — контроль над состоянием объекта.
Однако у него есть практическая проблема: он неудобен в использовании.
Рассмотрим типичную ситуацию.
class Person:
def __init__(self, name, old):
self.__name = name
self.__old = old
def get_old(self):
return self.__old
def set_old(self, old):
self.__old = oldИспользование:
p = Person("Igor", 20)
p.set_old(35)
print(p.get_old())С точки зрения архитектуры — все корректно. Но с точки зрения удобства — нет:
- нужно помнить имена методов
- синтаксис отличается от обычной работы с атрибутами
- код становится перегруженным
Возникает естественный вопрос:
Можно ли сохранить контроль над данными, но работать с ними как с обычными атрибутами?
Ответ: да, с помощью свойства (property).
Вернемся к классу Person и немного его изменим.
class Person:
def __init__(self, name, old):
self.__name = name
self.__old = old
def get_old(self):
return self.__old
def set_old(self, old):
self.__old = old
old = property(get_old, set_old)Теперь можно работать через свойство так:
p = Person("Сергей", 20)
p.old = 35
print(p.old)Атрибут old — это уже не просто переменная. Это объект property, который:
- при чтении вызывает
get_old() - при записи вызывает
set_old()
Схема работы:
p.old (чтение) ---> get_old() ---> __old
p.old = value ---> set_old() ---> __old
Почему при записи:
p.old = 35не создается новый атрибут old внутри объекта?
Потому что:
Если в классе есть
property, он имеет приоритет над атрибутами экземпляра.
Проверка:
p.__dict__['old'] = "что-то"
print(p.old) # все равно вызовется propertyНесмотря на удобство, у нас есть одна проблема текущего подхода - дублирование:
- можно использовать
get_old() - можно использовать
p.old
Это два интерфейса для одной и той же операции.
В реальной разработке это считается плохой практикой.
Python предлагает более чистый способ — через декораторы.
Перепишем класс:
class Person:
def __init__(self, name, old):
self.__name = name
self.__old = old
@property
def old(self):
return self.__oldТеперь:
p = Person("Igor", 20)
print(p.old)Работает.
Но:
p.old = 30 # ошибкаПочему? Потому что нет сеттера.
class Person:
def __init__(self, name, old):
self.__name = name
self.__old = old
@property
def old(self):
return self.__old
@old.setter
def old(self, value):
self.__old = valueТеперь:
p.old = 30
print(p.old)Важное правило: Имя метода должно совпадать.
@old.setter
def old(...)Это не случайность — это механизм связывания.
class Person:
def __init__(self, name, old):
self.__name = name
self.__old = old
@property
def old(self):
return self.__old
@old.setter
def old(self, value):
self.__old = value
@old.deleter
def old(self):
del self.__oldИспользование:
del p.oldПолный цикл работы свойства:
@property→ чтение@setter→ запись@deleter→ удаление
@property
def balance(self):
return self.__balanceИспользуется, когда:
- значение нельзя менять извне
- вычисляется на основе других данных
- это "только чтение"
Пример:
- баланс счета
- возраст, вычисляемый из даты рождения
- длина строки, списка и т.д.
@property
def age(self):
return self.__age
@age.setter
def age(self, value):
if value > 0:
self.__age = valueИспользуется, когда:
- нужно контролировать изменение
- требуется валидация
- важна целостность данных
Это самый частый сценарий.
Используется редко.
Когда нужен:
- если объект может “терять” состояние
- если удаление — осмысленная операция
Пример:
- кешированные значения
- временные данные
На первый взгляд, использование обычного атрибута self.x и геттеров/сеттеров может показаться одинаковым, так как оба подхода позволяют читать и изменять значение атрибута.
Разница принципиальная.
Геттеры и сеттеры позволяют вам контролировать доступ к данным.
Например, вы можете добавить логику проверки, валидации или преобразования данных при их чтении или записи.
При прямом доступе через self.x такой контроль невозможен.
class Person:
def __init__(self, age):
self._age = age
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if value < 0:
raise ValueError("Возраст не может быть отрицательным")
self._age = value
# Использование
p = Person(25)
print(p.age) # 25
p.age = -5 # Ошибка: ValueErrorГеттеры и сеттеры позволяют изменять внутреннюю реализацию атрибута без изменения интерфейса класса.
Например, вы можете заменить простое хранение значения на вычисление его "на лету", не меняя код, который использует ваш класс.
Геттеры и сеттеры помогают скрыть внутреннюю реализацию класса. Это позволяет пользователю класса работать только с интерфейсом, не зная, как данные хранятся или обрабатываются внутри.
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value <= 0:
raise ValueError("Радиус должен быть положительным")
self._radius = value
@property
def area(self):
return 3.14 * self._radius ** 2
# Использование
c = Circle(5)
print(c.area) # 78.5
c.radius = 10
print(c.area) # 314.0Здесь area вычисляется динамически, но для пользователя это выглядит как обычное свойство.
Если бы вы использовали self.radius напрямую, то пришлось бы менять весь код, чтобы добавить вычисление площади.
property— это способ объединить удобство работы с атрибутами и контроль над данными.
- В чем проблема обычных сеттеров и геттеров?
- Как работает
propertyпри чтении и записи? - Почему
p.x = valueне создает новый атрибут? - Зачем нужен декоратор
@property? - Почему имя метода сеттера должно совпадать с именем свойства?
- Когда достаточно только геттера?
- В чем разница между обычным атрибутом и property?
- Когда property использовать не нужно?
- Что делает
@deleter? - Как property помогает изменять реализацию без изменения интерфейса?
Реализуйте класс User, который хранит имя пользователя. Имя должно храниться в приватном атрибуте __name.
Доступ к нему должен осуществляться через объект-свойство name, реализованное с помощью декоратора @property.
При создании объекта в конструктор передается имя пользователя.
При установке значения необходимо проверять, что оно является строкой и не является пустым (длина строки больше 0). Если проверка не проходит — необходимо выбросить исключение ValueError.
Сеттер должен позволять изменять имя пользователя, а геттер — возвращать текущее значение.
Проверьте работу класса: создание объекта, изменение имени и обработку ошибок.
Пример использования:
u = User("Igor")
print(u.name)
u.name = "Bob"
print(u.name)
# Ошибка
# u.name = ""Реализуйте класс Car, который описывает автомобиль. Внутри класса модель автомобиля должна храниться в приватном атрибуте __model.
Доступ к этому атрибуту должен осуществляться через объект-свойство model, реализованное с помощью декоратора @property.
При попытке установить значение свойства model необходимо проверять корректность переданных данных. Значение должно быть строкой, длина которой находится в диапазоне от 2 до 100 символов включительно.
Если передано некорректное значение (например, число, пустая строка или слишком длинная строка), необходимо выбросить исключение ValueError.
Объект класса создается без параметров.
После создания объекта должна быть возможность установить и получить значение модели через свойство.
Проверьте работу класса на корректных и некорректных данных.
Пример использования:
car = Car()
car.model = "Toyota"
print(car.model)
# Ошибки
# car.model = 123
# car.model = "A"Реализуйте класс WindowDlg, который моделирует окно пользовательского интерфейса.
При создании объекта в него передаются три параметра: заголовок окна (строка), ширина и высота (целые числа).
Эти значения должны сохраняться в приватных атрибутах __title, __width, __height.
В классе необходимо реализовать метод show(), который выводит текущее состояние окна в формате:
<заголовок>: <ширина>, <высота>
Также необходимо реализовать два свойства width и height.
Через них должен происходить доступ к соответствующим приватным атрибутам.
При изменении ширины или высоты необходимо проверять, что переданное значение является целым числом в диапазоне от 0 до 10000 включительно.
Если значение некорректно — выбрасывать ValueError.
Если значение успешно изменено, необходимо автоматически вызвать метод show().
При инициализации объекта вызывать show() не нужно.
Проверьте, что окно корректно реагирует на изменение размеров и выбрасывает ошибки при некорректных данных.
Пример использования:
wnd = WindowDlg("Диалог", 100, 50)
wnd.width = 200
wnd.height = 300
# Ошибка
# wnd.width = -10Реализуйте класс RadiusVector2D, который описывает радиус-вектор на плоскости.
Класс должен поддерживать несколько вариантов создания объектов: без аргументов (вектор с координатами (0, 0)), с одним аргументом (x, при этом y = 0), и с двумя аргументами (x и y).
Внутри объекта координаты должны храниться в приватных атрибутах __x и __y.
В классе необходимо объявить публичные константы MIN_COORD и MAX_COORD, которые задают допустимый диапазон значений координат (от -100 до 1024).
Доступ к координатам должен осуществляться через свойства x и y.
При установке значения необходимо проверять, что оно является числом (int или float) и находится в допустимом диапазоне. Если проверка не проходит — выбрасывать исключение ValueError.
Также необходимо реализовать статический метод norm2(vector), который принимает объект данного класса и возвращает квадрат длины вектора (xx + yy).
Проверьте работу всех вариантов создания объекта, изменение координат и работу метода norm2.
Пример использования:
v = RadiusVector2D(3, 4)
print(RadiusVector2D.norm2(v)) # 25
v.x = 10
v.y = -5
# Ошибка
# v.x = 2000Реализуйте класс BankAccount, который моделирует банковский счет. В классе должны использоваться принципы инкапсуляции и управление доступом через property.
Внутри объекта должны храниться приватные атрибуты:
__owner— владелец счета (строка)__balance— текущий баланс (число)
При создании объекта в конструктор передаются имя владельца и начальный баланс.
Доступ к балансу должен осуществляться через свойство balance, но только для чтения (геттер). Прямое изменение баланса через balance запрещено.
Изменение баланса должно происходить только через методы:
deposit(amount)— пополнение счетаwithdraw(amount)— снятие средств
При этом необходимо реализовать проверки:
- пополнение возможно только положительным числом
- снятие возможно только если сумма положительная и не превышает текущий баланс
Если проверка не проходит — выбрасывать ValueError.
Также реализуйте свойство owner (с геттером и сеттером), где:
- имя должно быть строкой длиной не менее 2 символов
Дополнительно реализуйте статический метод transfer(from_acc, to_acc, amount), который переводит деньги между счетами.
Проверьте:
- пополнение
- снятие
- перевод между счетами
- ошибки при некорректных операциях
Пример использования:
a1 = BankAccount("Ivan", 100)
a2 = BankAccount("Oleg", 50)
a1.deposit(50)
a1.withdraw(30)
BankAccount.transfer(a1, a2, 50)
print(a1.balance)
print(a2.balance)Реализуйте систему работы с товарами и корзиной.
Создайте класс Product, который описывает товар. В нем должны быть приватные атрибуты:
__name— название товара__price— цена товара
Доступ к ним должен осуществляться через свойства:
name— строка, не пустаяprice— число больше 0
При некорректных значениях необходимо выбрасывать ValueError.
Создайте класс Cart, который хранит список товаров. Внутри должен быть приватный список __products.
Реализуйте методы:
add_product(product)— добавление товара (объект классаProduct)remove_product(product)— удаление товараget_products()— возвращает список товаров
Дополнительно реализуйте свойство total_price, которое:
- возвращает сумму цен всех товаров в корзине
- не имеет сеттера (только геттер)
Также реализуйте статический метод:
get_average_price(products)который возвращает среднюю цену товаров из списка.
Проверьте:
- добавление товаров
- удаление
- вычисление общей суммы
- вычисление средней цены
- ошибки при создании товаров
Пример использования:
p1 = Product("Phone", 1000)
p2 = Product("Laptop", 2000)
cart = Cart()
cart.add_product(p1)
cart.add_product(p2)
print(cart.total_price)
print(Cart.get_average_price(cart.get_products()))