На предыдущих занятиях мы последовательно разобрали, как создаются классы, как формируются объекты и как внутри них хранятся данные с помощью атрибутов. Мы также научились управлять поведением объектов через методы.
Теперь мы переходим к следующему важному этапу — контролю доступа к данным внутри объекта.
До этого момента все атрибуты, которые мы создавали, были полностью открыты. Это удобно на этапе обучения, но в реальной разработке такой подход приводит к серьезным проблемам: объект можно перевести в некорректное состояние, нарушить внутреннюю логику и получить трудноуловимые ошибки.
Именно поэтому в объектно-ориентированном программировании вводится принцип инкапсуляции — ограничение прямого доступа к внутреннему состоянию объекта и работа с ним только через контролируемый интерфейс.
Рассмотрим простой класс:
class Point:
def __init__(self, x=0, y=0):
self.x = x # координата по оси X
self.y = y # координата по оси YСоздадим объект:
pt = Point(1, 2)И попробуем с ним работать:
print(pt.x, pt.y) # 1 2
pt.x = 200
pt.y = "coord_y" # некорректное значениеОбратите внимание на строку:
pt.y = "coord_y"С точки зрения Python — это абсолютно корректная операция. Но с точки зрения логики программы — это ошибка, потому что координата точки должна быть числом.
Сейчас объект не контролирует, какие значения в него записываются.
Любое значение может попасть внутрь объекта.
В классических языках программирования (например, Java или C++) существуют строгие модификаторы доступа: public, protected, private.
В Python подход другой. Здесь нет строгой системы ограничений на уровне синтаксиса, но есть соглашения и механизмы, которые позволяют управлять доступом.
В Python принято использовать три уровня:
self.xТакие атрибуты:
- доступны везде
- могут свободно изменяться
Это поведение по умолчанию.
self._xОдин символ подчеркивания в начале имени означает:
Этот атрибут предназначен для внутреннего использования внутри класса и его наследников.
Важно понимать:
это не запрет, а сигнал разработчику.
Проверим:
class Point:
def __init__(self, x=0, y=0):
self._x = x
self._y = y
pt = Point(1, 2)
print(pt._x, pt._y) # доступ есть
pt._x = '12.22' # доступно для переопределения
print(pt._x, pt._y)Доступ не ограничен. Python не запрещает это делать.
Одно нижнее подчеркивание _ перед именем атрибута - это договоренность между разработчиками:
- такие атрибуты могут измениться в будущем
- они не являются частью публичного интерфейса
- прямое обращение к ним — потенциальный источник ошибок
Теперь рассмотрим более строгий вариант:
class Point:
def __init__(self, x=0, y=0):
self.__x = x
self.__y = yПопробуем обратиться к атрибутам:
pt = Point(1, 2)
print(pt.__x)Результат:
AttributeError
Python сообщает, что такого атрибута не существует.
Для защиты атрибутов (два нижних подчеркивания __) Python применяет механизм, называемый name mangling (искажение имени).
Атрибут __x внутри класса превращается в:
_Point__xЭто делается для:
- защиты от случайного доступа
- предотвращения конфликтов имен в наследовании
Если мы попробуем вывести список атрибутов:
print(dir(pt))В списке атрибутов вы увидите:
'_Point__x', '_Point__y'
Теперь можно обратиться так:
print(pt._Point__x)И это сработает.
Если атрибут скрыт (__x), то возникает вопрос:
Как с ним работать извне?
Ответ: через специальные методы.
def set_coord(self, x, y):
self.__x = x
self.__y = ydef get_coord(self):
return self.__x, self.__yclass Point:
def __init__(self, x=0, y=0):
self.__x = x # приватный атрибут
self.__y = y # приватный атрибут
def set_coord(self, x, y):
# установка новых значений координат
self.__x = x
self.__y = y
def get_coord(self):
# возвращает текущие координаты
return self.__x, self.__yИспользование:
pt = Point(1, 2)
pt.set_coord(10, 20)
print(pt.get_coord()) # (10, 20)Внешний код ---> setter/getter ---> Приватные атрибуты
Теперь доступ контролируется.
На первый взгляд кажется, что мы просто усложнили код.
Но на самом деле мы получили ключевое преимущество:
контроль над данными
Теперь мы можем проверять входные значения (добавить валидацию):
def set_coord(self, x, y):
if type(x) in (int, float) and type(y) in (int, float):
self.__x = x
self.__y = y
else:
raise ValueError("Координаты должны быть числами")type(x) in (int, float)— проверка типа- если проверка не проходит → выбрасывается исключение
- объект остается в корректном состоянии
Проверка:
pt = Point()
pt.set_coord(1, 2) # корректно
pt.set_coord("1", 2) # ошибкаЧтобы не дублировать код, вынесем проверку в отдельный метод.
class Point:
def __init__(self, x=0, y=0):
self.__x = self.__y = 0
if self.__check_value(x) and self.__check_value(y):
self.__x = x
self.__y = y
def set_coord(self, x, y):
if self.__check_value(x) and self.__check_value(y):
self.__x = x
self.__y = y
else:
raise ValueError("Координаты должны быть числами")
def get_coord(self):
return self.__x, self.__y
@staticmethod
def __check_value(x):
# приватный метод проверки
return isinstance(x, (int, float))__check_value— приватный метод@staticmethod— потому что метод не используетself- используется и в
__init__, и вset_coord
Да, можно:
pt._Point__x = 999Но делать это категорически не рекомендуется.
Почему:
- нарушается логика класса
- обходится валидация
- код становится нестабильным
В Python есть сторонние решения для более строгого контроля. Это библиотека accessify.
Установка:
pip install accessifyИспользование:
from accessify import private, protected
class Point:
@private
@staticmethod
def check_value(x):
return isinstance(x, (int, float))Теперь:
Point.check_value(5)вызовет ошибку.
Использование этой библиотеки в Python встречается крайне редко. Python-разработчики обычно придерживаются стандартных соглашений об именовании (_ и __) для обозначения защищенных и приватных атрибутов, вместо использования сторонних библиотек.
- В чем основная проблема публичных атрибутов?
- Чем protected отличается от private в Python?
- Почему
_xне защищает атрибут на уровне языка? - Что такое name mangling и зачем он нужен?
- Как правильно получить доступ к приватному атрибуту?
- Зачем нужны сеттеры и геттеры?
- Где должна происходить валидация данных?
- Почему нельзя обращаться к
_ClassName__attrнапрямую? - Когда стоит использовать
@staticmethodдля проверки? - Что произойдет, если не проверять входные данные?
Создайте класс Line объекты которого будут создаваться с помощью команды:
line = Line(x1, y1, x2, y2)При создании объекта должны создаваться приватные атрибуты:
__x1, __y1__x2, __y2
Так же в классе должны быть методы для работы с приватными атрибутами:
set_coords()get_coords()→ возвращает кортежdraw()→ вывод координат в одну строку
Пример использования:
line = Line(0, 0, 10, 10)
line.draw()Реализуйте класс Clock, который моделирует хранение времени в виде числа.
При создании объекта должна создаваться приватная переменная__time, которая хранит текущее время (целое число).
Реализовать методы:
set_time(tm)— устанавливает время, если оно прошло проверкуget_time()— возвращает текущее значение времени__check_time(tm)— приватный метод проверки
Реализовать проверку корректности которая должна выполняться при создании объекта:
- значение должно быть
int - значение должно быть ≥ 0
- значение должно быть < 100000
Пример использования:
clock = Clock(100)
clock.set_time(4530)
print(clock.get_time()) # 4530Создайте класс User, который хранит имя пользователя.
Имя пользователя должно хранится в защищенном атрибуте (protected) _name.
Так же нужно реализовать методы для взаимодействия с атрибутом _name:
set_name(name)— устанавливает имя, если это строкаget_name()— возвращает имя
При создании или установлении имени через set_name нужно реализовать проверку на строковое значение. Если передано не строковое значение — выбрасывать ValueError
Пример использования:
user = User("Alice")
print(user.get_name()) # Alice
user2 = User(212) # ValueError: Имя должно быть строкойРеализуйте класс Book, который создается таким способом:
book = Book(author, title, price)При создании объекта должны быть созданы приватные атрибуты:
__author— строка__title— строка__price— число (int или float)
И методы для взаимодействия с ними:
set_author(),get_author()set_title(),get_title()set_price(),get_price()
Для каждого атрибута нужно сделать проверки:
- автор и название — строки
- цена — число ≥ 0
Если переданные данные не проходят эти проверки генерировать ValueError с соответствующим пояснением.
Пример использования:
book = Book("Толстой", "Война и мир", 500)
print(book.get_title(), book.get_price())Реализуйте класс Money.
При создании объекта класса должен создаваться приватный атрибут __money — целое число ≥ 0.
А так же в классе должны присутствовать методы:
set_money(money)get_money()add_money(mn)— гдеmn- это объект класса Money
При добавлении денег метод add_money() аргументом принимает объект класса Money.
Если пользователь пытается в существующий объект добавить число (int или float) нужно выбрасывать ошибку ValueError с соответствующим пояснением.
При создании объекта Money значение должно быть целым int и больше 0.
Пример использования:
mn1 = Money(10)
mn2 = Money(20)
mn1.set_money(100)
mn2.add_money(mn1)
print(mn2.get_money()) # 120
mn1.add_money(55) # ошибкаРеализуйте два класса Point и Rectangle.
В классе Point во время инициализации создаются приватные атрибуты __x, __y.
В классе Point так же должен быть метод, который возвращает кортеж атрибутов объекта.
Объекты класса Rectangle могут создаваться двумя командами:
r1 = Rectangle(Point(x, y), Point(x, y))
r2 = Rectangle(x1, y1, x2, y2)В объекте класса Rectangle должны создаваться два приватных атрибута __sp и __ep, которые будут хранить точки как объекты класса Point.
Так же у Rectangle должны быть методы:
set_coords(sp, ep)- для установления значений__spи__epget_coords()- для получения__spи__epdraw()- метод, который в строку выводит координаты двух точек (x1, y1, x2, y2)
Пример использования:
pt1 = Point(1, 2)
pt2 = Point(10, 20)
rect1 = Rectangle(0, 0, 20, 34)
rect1.draw()
rect2 = Rectangle(pt1, pt2)
rect2.draw()Реализуйте класс BankAccount.
В классе во время инициализации должны создаваться приватные атрибуты:
__owner— владелец счета__balance— текущий баланс
Баланс должен устанавливаться через методы класса, а не напрямую.
В классе необходимо реализовать следующие методы:
-
deposit(amount)— метод для пополнения счета Увеличивает баланс на значениеamount. Пополнение допускается только положительным числом. -
withdraw(amount)— метод для снятия средств Уменьшает баланс на значениеamount. Снятие возможно только в том случае, если на счете достаточно средств. -
get_balance()— метод для получения текущего баланса
При реализации необходимо учитывать следующие ограничения:
- запрещено устанавливать или изменять баланс напрямую извне класса
- нельзя вносить отрицательные или нулевые значения
- нельзя снимать сумму, превышающую текущий баланс
Пример использования:
acc = BankAccount("Igor", 100)
acc.deposit(50)
acc.withdraw(30)
print(acc.get_balance()) # 120Реализуйте два класса: ObjList и LinkedList.
Объекты класса ObjList представляют отдельные элементы списка.
В классе ObjList во время инициализации должен создаваться приватный атрибут:
__data— строка с данными элемента
Также в каждом объекте ObjList должны быть приватные атрибуты:
__next— ссылка на следующий элемент списка__prev— ссылка на предыдущий элемент списка
Изначально ссылки на соседние элементы должны быть равны None.
В классе ObjList необходимо реализовать методы:
set_next(obj)— устанавливает ссылку на следующий элементset_prev(obj)— устанавливает ссылку на предыдущий элементget_next()— возвращает ссылку на следующий элементget_prev()— возвращает ссылку на предыдущий элементset_data(data)— изменяет значение__dataget_data()— возвращает значение__data
Класс LinkedList управляет всей структурой списка.
В классе LinkedList должны быть публичные атрибуты:
head— ссылка на первый элемент спискаtail— ссылка на последний элемент списка
Если список пуст, оба атрибута должны быть равны None.
В классе LinkedList необходимо реализовать методы:
-
add_obj(obj)— добавляет новый объектObjListв конец списка При добавлении необходимо корректно обновить связи между элементами (nextиprev) -
remove_obj()— удаляет последний элемент списка При удалении необходимо корректно обновить связи и атрибутtail -
get_data()— возвращает список строк, содержащий данные всех элементов списка Обход списка должен выполняться отheadкtail
Объекты класса ObjList создаются следующим образом:
ob = ObjList("данные 1")Пример использования:
lst = LinkedList()
lst.add_obj(ObjList("данные 1"))
lst.add_obj(ObjList("данные 2"))
lst.add_obj(ObjList("данные 3"))
print(lst.get_data()) # ['данные 1', 'данные 2', 'данные 3']При реализации важно:
-
не обращаться напрямую к приватным атрибутам объектов
ObjListизвне -
все связи между элементами должны устанавливаться только через методы
-
корректно обрабатывать крайние случаи:
- пустой список
- список из одного элемента