Инкапсуляция и ограничение доступа
Инкапсуляция – это один из трёх основных ООП. И как мы уже говорили, её главной идеей является сокрытие внутренней реализации объекта от внешнего мира и предоставление контролируемого доступа к его данным.
Вспомним пример с автомобилем из предыдущего параграфа. Инкапсуляция позволяет контролировать, как и когда данные объекта могут быть изменены. Так, вызов метода продажи sell() не только изменяет статус наличия автомобиля в автосалоне, но и выполняет другие необходимые действия, например, выводит сообщение о продаже. Прямое изменение атрибута не обеспечит совершение этих действий.
Ограничение доступа к данным
В отличие от некоторых других языков программирования, в Python нет строгих механизмов для полного запрета доступа к атрибутам и методам. Вместо этого используется соглашение об именовании, которое говорит разработчикам, как следует обращаться к этим данным:
- Публичные (англ. – public) атрибуты и методы не начинаются с подчеркиваний, например,
apply_discount(). Они доступны как внутри класса, так и вне его. Вы можете свободно изменять значения таких атрибутов и использовать методы в любом месте программы. - Защищённые (англ. – protected) атрибуты и методы начинаются с одного подчеркивания, например,
_model. Они считаются доступными только внутри класса и во всех его дочерних классах. Однако технически Python не мешает вам обращаться к таким атрибутам и методам напрямую извне класса, так как такое обозначение является просто сигналом для других разработчиков. - Приватные (англ. – private) атрибуты и методы начинаются с двух нижних подчеркиваний, например,
__price. Они доступны только внутри класса. Python применяет к таким именам механизм сокрытия, чтобы затруднить прямой доступ. Он автоматически меняет имя атрибута, добавляя к нему имя класса, поэтому атрибут__priceстановится атрибутом_Car__price.
Давайте модифицируем класс Car из предыдущей статьи, сделав атрибут model защищённым, а атрибут in_stock – приватным:
class Car:
def __init__(self, model: str, price: int | float, in_stock: bool=True):
self.model = model
self._price = price
self.__in_stock = in_stock
def apply_discount(self, percentage: int | float) -> None:
discount_amount = self._price * (percentage / 100)
self._price -= discount_amount
print(f"Новая цена автомобиля {self.model}: {self._price} руб.")
def sell(self) -> bool:
if self.__in_stock:
print(f"Автомобиль {self.model} успешно продан")
self.__in_stock = False
return True
print(f"Автомобиль {self.model} уже был продан")
return False
Так, мы можем напрямую получить значение защищённого атрибута, но для приватного атрибута вызывается исключение AttributeError:
honda_accord = Car("Honda Accord VII", 800_000)
print(honda_accord._model)
# Вывод: Honda Accord VII
print(honda_accord.__price)
# Ошибка: AttributeError: 'Car' object has no attribute '__price'
При этом вы можете попробовать напрямую изменить значение приватного атрибута и вам даже покажется, что всё работает:
honda_accord.__price = 700_000
print(honda_accord.__price)
# Вывод: 700000
Однако на самом деле вы не меняете атрибут __price, который определён внутри класса Car. Вместо этого, Python создает новый атрибут, который привязан только к экземпляру honda_accord, а не к классу Car. Вы можете убедиться в этом, получив словарь __dict__, который хранит все атрибуты объекта:
print(honda_accord.__dict__)
# Вывод: {'model': 'Honda Accord VII', '_Car__price': 800000, '_Car__in_stock': True, '__price': 700000}
Как видите, объект honda_accord содержит новый атрибут __price, а также атрибут _Car__price, который как раз и является атрибутом __price, используемым в классе.
Однако так как Python скрывает атрибут, просто добавляя к нему имя класса, то мы можем напрямую обратиться к его новому имени и изменить его:
honda_accord._Car__price = 700_000
print(honda_accord.__dict__)
# Вывод: {'model': 'Honda Accord VII', '_Car__price': 700000, '_Car__in_stock': True, '__price': 700000}
Но так делать не стоит, ведь изменение имени защищает внутренние атрибуты класса от случайного или намеренного изменения извне.
Геттеры и сеттеры
Для того чтобы обеспечить контролируемый доступ к атрибутам, особенно к тем, которые мы пометили как приватные, в ООП используются специальные методы: геттеры и сеттеры:
- Геттер (от англ. get – получить) – это метод, который позволяет получить значение атрибута.
- Сеттер (от англ. set – установить) – это метод, который позволяет установить или изменить значение атрибута, но при этом позволяет добавить дополнительную логику для проверки или преобразования данных перед их сохранением.
Такие методы позволяют не только обращаться к приватным атрибутам, но и добавлять дополнительные действия к операциям чтения и записи. Например, проверку того, что цена автомобиля строго больше нуля.
Давайте напишем геттер get_price() и сеттер set_price() для получения и изменения цены автомобиля. Тогда класс Car будет выглядеть следующим образом (пропустим ненужные нам сейчас методы apply_discount() и sell()):
class Car:
def __init__(self, model: str, price: int | float, in_stock: bool=True):
self.model = model
self._price = price
self.__in_stock = in_stock
# Геттер для цены
def get_price(self) -> int | float:
return self._price
# Сеттер для цены
def set_price(self, new_price: int | float) -> None:
if new_price > 0:
self._price = new_price
else:
print("Цена должна быть положительным числом")
Геттер является обычным методом, который возвращается значение нужного атрибута, поэтому метод get_price() возвращает значение приватного атрибута __price.
Сеттер же предназначен для установки значения атрибута, однако также позволяет добавить новую логику, поэтому метод set_price() изменяет значение атрибута __price только в том случае, если новое значение больше нуля.
Теперь мы можем как получить, так и изменить значение приватного атрибута __price:
chevrolet_suburban = Car("Chevrolet Suburban XII", 13_000_000)
chevrolet_suburban.set_price(12_500_000)
print(chevrolet_suburban.get_price())
# Вывод: 12500000
Также сеттер set_price() проверяет корректность данных перед их сохранением и значение не будет изменено, если новая цена меньше или равна нулю:
chevrolet_suburban.set_price(-10_000_000)
# Вывод: Цена должна быть положительным числом
print(chevrolet_suburban.get_price())
# Вывод: 12500000
Декораторы @property
Явно вызывать геттеры и сеттеры как методы – это стандартная практика, но в Python есть более изящный способ их реализации с помощью декоратора @property. Его использование позволяет обращаться к методу как к атрибуту.
Первый метод, который вы помечаете декоратором @property, становится геттером, а сеттер создается с помощью декоратора @имя_геттера.setter. При этом оба метода должны иметь одинаковое название – имя атрибута, к которому мы будем обращаться.
Тогда методы get_price() и set_price() следует переименовать просто в price(), а также декорировать геттер как @property, а сеттер – как @price.setter:
class Car:
def __init__(self, model: str, price: int | float, in_stock: bool=True):
self._model = model
self.__price = price
self.__in_stock = in_stock
# Геттер
@property
def price(self) -> int | float:
return self.__price
# Сеттер
@price.setter
def price(self, new_price: int | float) -> None:
if new_price > 0:
self.__price = new_price
else:
print("Цена должна быть положительным числом")
Теперь мы можем работать с ценой, как с обычным атрибутом, хотя на самом деле за кулисами вызываются наши геттер и сеттер:
hyundai_solaris = Car("Hyundai Solaris II", 1_150_000)
hyundai_solaris.price = 1_000_000
print(hyundai_solaris.price)
# Вывод: 1000000
Здесь атрибут price вызывает геттер price() с декоратором @property для получения значения атрибута и сеттер price() с декоратором price.setter() для установки нового значения атрибута.
Преимущество такого подхода заключается в том, что мы получаем все преимущества инкапсуляции, но без усложнения синтаксиса для пользователя класса. Внешне, работа с атрибутом __price выглядит так же, как работа с обычными атрибутами, но внутри мы полностью контролируем какие данные будут сохранены или возвращены.
Примеры
Пример 1. Изменение кредитного лимита
Для управления кредитным лимитом используется класс CreditAccount, где атрибут с кредитным лимитом __limit является приватным. Сеттер с декоратором @limit.setter проверяет, что новый кредитный лимит является положительным числом:
class CreditAccount:
"""Описывает управление кредитным лимитом."""
def __init__(self, owner: str, limit: int):
"""Конструктор класса CreditAccount.
Параметры:
owner: Номер счёта.
limit: Текущий кредитный лимит.
"""
self.owner = owner
self.__limit = limit # Приватный атрибут
@property
def limit(self) -> int:
"""Геттер. Возвращает значение приватного атрибута __limit."""
return self.__limit
@limit.setter
def limit(self, new_limit: int) -> None:
"""Сеттер. Устанавливает новое значение атрибута кредитного лимита
__limit, если оно является положительным числом.
Параметры:
new_limit: Новое значение кредитного лимита.
"""
if new_limit >= 0:
self.__limit = new_limit
print(f"Лимит установлен: {self.__limit} руб.")
else:
print(f"Лимит должен быть больше нуля")
account = CreditAccount("Романов П. А.", 50000)
print(f"Текущий лимит: {account.limit} руб.") # Получение через геттер
# Попытка установить корректное значение
account.limit = 75000 # Установка через сеттер
# Попытка установить некорректное значение
account.limit = -5000 # Сеттер блокирует изменение
Вывод:
Текущий лимит: 50000 руб.
Лимит установлен: 75000 руб.
Лимит должен быть больше нуля
Пример 2. Изменение громкости в игре
Класс GameVolume предназначен для управления громкостью звука в игре. Уровень громкости должен быть целым числом и не может быть меньше 0 и больше 100. Использование сеттера с декоратором @volume.setter не разрешает установку недопустимого значения:
class GameVolume:
"""Описывает управление громкостью в игре.
"""
def __init__(self, volume: int):
"""Конструктор класса GameVolume.
Параметры:
volume: Громкость звука в игре.
"""
self.__volume = volume
# Геттер
@property
def volume(self) -> int:
"""Геттер. Возвращает значение приватного атрибута __volume.
"""
return self.__volume
# Сеттер
@volume.setter
def volume(self, new_volume: int | float) -> None:
"""Сеттер. Устанавливает новое значение атрибута громкости __volume.
Изменяет значение только в том случае, если новое значение
new_volume является целым числом в диапазоне от 0 до 100.
Параметры:
new_volume: Новый уровень громкости звука.
"""
if isinstance(new_volume, int) and 0 <= new_volume <= 100:
self.__volume = new_volume
print(f"Громкость изменена: {new_volume}")
else:
print("Уровень громкости должен быть от 0 до 100")
game_volume = GameVolume(70)
# Получение текущего значения громкости через геттер
print(f"Текущий уровень громкости: {game_volume.volume}")
# Установка корректного значения через сеттер
game_volume.volume = 75
# Установка некорректного значения через сеттер
game_volume.volume = 1000
Вывод:
Текущий уровень громкости: 70
Громкость изменена: 75
Уровень громкости должен быть от 0 до 100
Пример 3. Вычисление зарплаты сотрудника
Класс Employee описывает сотрудника и хранит приватную почасовую ставку __hourly_rate и отработанные часы hours_worked. При этом зарплата сотрудника не хранится как атрибут, а вычисляется каждый раз при обращении через геттер с декоратором @property monthly_salary, который использует приватный метод __get_salary():
class Employee:
"""Описывает сотрудника и вычисляет его зарплату на основе ставки."""
def __init__(self, name: str, hourly_rate: float, hours_worked: int):
"""Конструктор класса Employee.
Параметры:
name: Имя сотрудника.
hourly_rate: Почасовая ставка.
hours_worked: Отработанные часы.
"""
self.name = name
self.hours_worked = hours_worked # Публичные часы
self.__hourly_rate = hourly_rate # Приватная ставка
def __get_salary(self) -> float:
"""Возвращает зарплату на основе часовой ставки и количества часов."""
return self.__hourly_rate * self.hours_worked
@property
def monthly_salary(self) -> float:
"""Геттер. Возвращает зарплату, не храня её в атрибуте.
"""
# Читает приватный атрибут __hourly_rate
return self.__get_salary()
@monthly_salary.setter
def monthly_salary(self, rate: float) -> None:
"""Сеттер: Устанавливает новую почасовую ставку __hours_rate,
если она является положительным числом.
Параметры:
rate: Новое значение почасовой ставки.
"""
if rate > 0:
self.__hourly_rate = rate
print(f"Ставка обновлена до {rate:.2f} руб/час.")
else:
print("Ставка должна быть больше нуля")
employee1 = Employee("Ломоносов М. В", 800, 160)
print(f"Зарплата {employee1.name} (до): {employee1.monthly_salary}")
employee1.monthly_salary = 850
print(f"Зарплата {employee1.name} (после): {employee1.monthly_salary}")
Вывод:
Зарплата Ломоносов М. В (до): 128000
Ставка обновлена до 850.00 руб/час.
Зарплата Ломоносов М. В (после): 136000
Итоги
- Публичные атрибуты и методы не начинаются с подчеркиваний и доступны как внутри класса, так и вне его.
- Защищённые атрибуты и методы начинаются с одного подчеркивания, и считаются доступными только внутри класса и во всех его дочерних классах. Однако к ним можно обращаться напрямую извне класса.
- Приватные атрибуты и методы начинаются с двух нижних подчеркиваний, и доступны только внутри класса. Прямой доступ к ним извне класса не рекомендуется и затруднён механизмом изменения имени этого атрибута.
- Геттер – это метод, позволяющий получить значение атрибута.
- Сеттер – это метод, позволяющий установить или изменить значение атрибута.
- Первый метод, который помечается декоратором
@property, становится геттером, а сеттер создается с помощью декоратора@имя_геттера.setter.
Задания для самопроверки
1. Чем защищённые атрибуты отличаются от приватных?
К защищённым атрибутам можно обращаться напрямую извне класса, а к приватным – нет.
2. Создайте класс для банковского счёта BankAccount, который инициализируется защищённым атрибутом с именем владельца и приватным атрибутом с текущим балансом и начальным значением 0. Назовите атрибуты по своему усмотрению. Какие особенности именования атрибутов в данном случае следует учитывать?
class BankAccount:
def __init__(self, owner_name: str, current_balance=0.0):
self._owner_name = owner_name
self.__current_balance = 0
Защищённые атрибуты начинаются с одного подчеркивания (_), а приватные – с двух (__).
3. Создайте класс пользователя User, который инициализируется приватным атрибутом __age с возрастом. Напишите геттер get_age(), который возвращает значение атрибута __age. Создайте объект класса User с произвольным возрастом. Получите значение атрибута __age с помощью геттера и выведите его на экран.
class User:
def __init__(self, age: int):
self.__age = age
def get_age(self) -> int:
return self.__age
user = User(45)
current_age = user.get_age()
print(current_age)
# Вывод: 45
4. В классе User из предыдущего задания создайте сеттер set_age(self, new_age), который изменяет значение атрибута __age на new_age только в том случае, если new_age находится в диапазоне от 0 до 100 включительно. В противном случае, выведите на экран строку "Возраст должен быть в диапазоне от 0 до 100" и не меняйте атрибут. Измените значение атрибута __age в ранее созданном объекте с помощью сеттера сначала на число 18, затем на -21 и выведите его на экран.
class User:
def __init__(self, age: int):
self.__age = age
def get_age(self) -> int:
return self.__age
def set_age(self, new_age: int) -> None:
if 0 <= new_age <= 100:
self.__age = new_age
else:
print(f"Возраст должен быть в диапазоне от 0 до 100")
# Создание объекта и изменение атрибута
user = User(45)
user.set_age(18)
user.set_age(-21)
# Вывод: Возраст должен быть в диапазоне от 0 до 100
print(user.get_age())
# Вывод: 18
5. В классе User из заданий 3 и 4 перепишите геттер и сеттер с помощью декоратора @property. Создайте новый объект класса User с произвольным возрастом, измените его с помощью сеттера и выведите его на экран с помощью геттера.
class User:
def __init__(self, age: int):
self.age = age
@property
def age(self) -> int:
return self.__age
@age.setter
def age(self, new_age: int) -> None:
if 0 <= new_age <= 100:
self.__age = new_age
else:
print("Возраст должен быть в диапазоне от 0 до 100")
# Создание объекта
user_2 = User(30)
user_2.age = 13
print(user_2.age)
# Вывод: 13
0 комментариев