Skip to content

Latest commit

 

History

History
695 lines (444 loc) · 23.2 KB

File metadata and controls

695 lines (444 loc) · 23.2 KB

Урок 7. Управление доступом к атрибутам: public, protected, private и интерфейсные методы

На предыдущих занятиях мы последовательно разобрали, как создаются классы, как формируются объекты и как внутри них хранятся данные с помощью атрибутов. Мы также научились управлять поведением объектов через методы.

Теперь мы переходим к следующему важному этапу — контролю доступа к данным внутри объекта.

До этого момента все атрибуты, которые мы создавали, были полностью открыты. Это удобно на этапе обучения, но в реальной разработке такой подход приводит к серьезным проблемам: объект можно перевести в некорректное состояние, нарушить внутреннюю логику и получить трудноуловимые ошибки.

Именно поэтому в объектно-ориентированном программировании вводится принцип инкапсуляции — ограничение прямого доступа к внутреннему состоянию объекта и работа с ним только через контролируемый интерфейс.


Почему нельзя оставлять атрибуты полностью открытыми

Рассмотрим простой класс:

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

Сейчас объект не контролирует, какие значения в него записываются.

Любое значение может попасть внутрь объекта.


Уровни доступа в Python

В классических языках программирования (например, Java или C++) существуют строгие модификаторы доступа: public, protected, private.

В Python подход другой. Здесь нет строгой системы ограничений на уровне синтаксиса, но есть соглашения и механизмы, которые позволяют управлять доступом.

В Python принято использовать три уровня:

Public (публичный доступ)

self.x

Такие атрибуты:

  • доступны везде
  • могут свободно изменяться

Это поведение по умолчанию.


Protected (условно защищенный доступ)

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 не запрещает это делать.

Одно нижнее подчеркивание _ перед именем атрибута - это договоренность между разработчиками:

  • такие атрибуты могут измениться в будущем
  • они не являются частью публичного интерфейса
  • прямое обращение к ним — потенциальный источник ошибок

Private (приватный доступ)

Теперь рассмотрим более строгий вариант:

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), то возникает вопрос:

Как с ним работать извне?

Ответ: через специальные методы.

Сеттер (setter) — метод для изменения значения

def set_coord(self, x, y):
    self.__x = x
    self.__y = y

Геттер (getter) — метод для получения значения

def get_coord(self):
    return self.__x, self.__y

Полный пример

class 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

Можно ли обойти приватность в Python?

Да, можно:

pt._Point__x = 999

Но делать это категорически не рекомендуется.

Почему:

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

Дополнительный инструмент: accessify

В 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-разработчики обычно придерживаются стандартных соглашений об именовании (_ и __) для обозначения защищенных и приватных атрибутов, вместо использования сторонних библиотек.


Вопросы

  1. В чем основная проблема публичных атрибутов?
  2. Чем protected отличается от private в Python?
  3. Почему _x не защищает атрибут на уровне языка?
  4. Что такое name mangling и зачем он нужен?
  5. Как правильно получить доступ к приватному атрибуту?
  6. Зачем нужны сеттеры и геттеры?
  7. Где должна происходить валидация данных?
  8. Почему нельзя обращаться к _ClassName__attr напрямую?
  9. Когда стоит использовать @staticmethod для проверки?
  10. Что произойдет, если не проверять входные данные?

Задачи

Задача 1

Создайте класс Line объекты которого будут создаваться с помощью команды:

line = Line(x1, y1, x2, y2)

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

  • __x1, __y1
  • __x2, __y2

Так же в классе должны быть методы для работы с приватными атрибутами:

  • set_coords()
  • get_coords() → возвращает кортеж
  • draw() → вывод координат в одну строку

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

line = Line(0, 0, 10, 10)
line.draw()

Задача 2

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

Задача 3

Создайте класс User, который хранит имя пользователя.

Имя пользователя должно хранится в защищенном атрибуте (protected) _name.

Так же нужно реализовать методы для взаимодействия с атрибутом _name:

  • set_name(name) — устанавливает имя, если это строка
  • get_name() — возвращает имя

При создании или установлении имени через set_name нужно реализовать проверку на строковое значение. Если передано не строковое значение — выбрасывать ValueError

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

user = User("Alice")
print(user.get_name())  # Alice

user2 = User(212) # ValueError: Имя должно быть строкой

Задача 4

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

Задача 5

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

Задача 6

Реализуйте два класса 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 и __ep
  • get_coords() - для получения __sp и __ep
  • draw() - метод, который в строку выводит координаты двух точек (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()

Задача 7

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

Задача 8

Реализуйте два класса: ObjList и LinkedList.

Объекты класса ObjList представляют отдельные элементы списка.

В классе ObjList во время инициализации должен создаваться приватный атрибут:

  • __data — строка с данными элемента

Также в каждом объекте ObjList должны быть приватные атрибуты:

  • __next — ссылка на следующий элемент списка
  • __prev — ссылка на предыдущий элемент списка

Изначально ссылки на соседние элементы должны быть равны None.

В классе ObjList необходимо реализовать методы:

  • set_next(obj) — устанавливает ссылку на следующий элемент
  • set_prev(obj) — устанавливает ссылку на предыдущий элемент
  • get_next() — возвращает ссылку на следующий элемент
  • get_prev() — возвращает ссылку на предыдущий элемент
  • set_data(data) — изменяет значение __data
  • get_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 извне

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

  • корректно обрабатывать крайние случаи:

    • пустой список
    • список из одного элемента

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