Skip to content

Latest commit

 

History

History
652 lines (419 loc) · 21.7 KB

File metadata and controls

652 lines (419 loc) · 21.7 KB

Урок 8. От свойства property() к декоратору @property

На предыдущем занятии мы подробно разобрали механизм инкапсуляции: как скрывать данные внутри объекта и работать с ними через сеттеры и геттеры.

Этот подход решает главную задачу — контроль над состоянием объекта.

Однако у него есть практическая проблема: он неудобен в использовании.

Рассмотрим типичную ситуацию.

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).


Объект 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

Это два интерфейса для одной и той же операции.

В реальной разработке это считается плохой практикой.


Декоратор @property

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 → удаление

Практическое применение: когда что использовать

Случай 1. Только геттер

@property
def balance(self):
    return self.__balance

Используется, когда:

  • значение нельзя менять извне
  • вычисляется на основе других данных
  • это "только чтение"

Пример:

  • баланс счета
  • возраст, вычисляемый из даты рождения
  • длина строки, списка и т.д.

Случай 2. Геттер + сеттер

@property
def age(self):
    return self.__age

@age.setter
def age(self, value):
    if value > 0:
        self.__age = value

Используется, когда:

  • нужно контролировать изменение
  • требуется валидация
  • важна целостность данных

Это самый частый сценарий.


Случай 3. Полный цикл (getter + setter + deleter)

Используется редко.

Когда нужен:

  • если объект может “терять” состояние
  • если удаление — осмысленная операция

Пример:

  • кешированные значения
  • временные данные

Ключевой вопрос: чем полный цикл getter + setter + deleter отличается от обычного атрибута self.x?

На первый взгляд, использование обычного атрибута 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 — это способ объединить удобство работы с атрибутами и контроль над данными.


Вопросы для закрепления

  1. В чем проблема обычных сеттеров и геттеров?
  2. Как работает property при чтении и записи?
  3. Почему p.x = value не создает новый атрибут?
  4. Зачем нужен декоратор @property?
  5. Почему имя метода сеттера должно совпадать с именем свойства?
  6. Когда достаточно только геттера?
  7. В чем разница между обычным атрибутом и property?
  8. Когда property использовать не нужно?
  9. Что делает @deleter?
  10. Как property помогает изменять реализацию без изменения интерфейса?

Задачи

Задача 1

Реализуйте класс User, который хранит имя пользователя. Имя должно храниться в приватном атрибуте __name.

Доступ к нему должен осуществляться через объект-свойство name, реализованное с помощью декоратора @property.

При создании объекта в конструктор передается имя пользователя.

При установке значения необходимо проверять, что оно является строкой и не является пустым (длина строки больше 0). Если проверка не проходит — необходимо выбросить исключение ValueError.

Сеттер должен позволять изменять имя пользователя, а геттер — возвращать текущее значение.

Проверьте работу класса: создание объекта, изменение имени и обработку ошибок.

Пример использования:

u = User("Igor")
print(u.name)

u.name = "Bob"
print(u.name)

# Ошибка
# u.name = ""

Задача 2

Реализуйте класс Car, который описывает автомобиль. Внутри класса модель автомобиля должна храниться в приватном атрибуте __model.

Доступ к этому атрибуту должен осуществляться через объект-свойство model, реализованное с помощью декоратора @property.

При попытке установить значение свойства model необходимо проверять корректность переданных данных. Значение должно быть строкой, длина которой находится в диапазоне от 2 до 100 символов включительно.

Если передано некорректное значение (например, число, пустая строка или слишком длинная строка), необходимо выбросить исключение ValueError.

Объект класса создается без параметров.

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

Проверьте работу класса на корректных и некорректных данных.

Пример использования:

car = Car()
car.model = "Toyota"
print(car.model)

# Ошибки
# car.model = 123
# car.model = "A"

Задача 3

Реализуйте класс 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

Задача 4

Реализуйте класс 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

Задача 5

Реализуйте класс 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)

Задача 6

Реализуйте систему работы с товарами и корзиной.

Создайте класс 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()))

Предыдущий урок | Следующий урок