ООП для новичков: публичное и приватное
medium

ООП для новичков: публичное и приватное

Про методы и свойства для тех, кто серьёзно настроен

Мы начали большую тему: разбираем объектно-ориентированное программирование. Но не на примере фруктов и овощей, как обычно, а на чём-то более жизненном. В первой статье рассказали про классы и объекты — самую основу ООП:

  • Идея ООП в том, чтобы сделать автономные объекты, которые сумеют сами отреагировать на всё, что происходит вокруг. 
  • Программист описывает эти объекты, задаёт правила взаимодействия, а остальное объекты делают сами
  • Класс — это инструкция для сборки объекта. Всё, что прописано в классе, появится и в объекте. 
  • У класса (и объекта) есть два основных инструмента взаимодействия — свойства и методы.
  • Свойства — это данные, которые лежат внутри объекта. 
  • Методы — это то, что объект умеет делать или как реагирует на внешние запросы.

Сегодня поговорим про публичные и приватные свойства и методы — о том, как ограничивать доступ к тому, что нужно.

Короткая версия:

  • разделение уровней доступа нужно для того, чтобы другие программисты или другой код не поломали логику работы объекта;
  • есть три уровня: публичный, приватный и защищённый;
  • публичные свойства и методы доступны всем — их может вызвать любая команда в коде или другом объекте;
  • приватные доступны только внутри класса — к ним нельзя получить доступ из других объектов или другого места программы;
  • про защищённые свойства и методы поговорим в статье про наследование, пока нам это не нужно.

Свойства и методы

Чтобы было нагляднее, о чём пойдёт речь дальше, возьмём описание класса шарика из прошлой статьи. Тогда мы делали простую игру, где можно управлять шариками на холсте. Шарик можно было двигать стрелками. 

Для простоты оставим у шарика только свойства и названия методов, остальное пока уберём, чтобы сфокусироваться на важном.

У нашего шарика такие свойства:

  • canvas ← холст, на котором рисуется шарик;
  • x и y ← шаг и направление движения;
  • id ← основная характеристика шарика: его размеры и цвет; 
  • canvas_height и canvas_width ← ширина и высота холста с точки зрения шарика.

И вот такие методы (то есть то, что он умеет)

  • __init__ ← конструктор, вызывается в момент создания нового объекта на основе класса;
  • turn_right ← движение вправо, влево, вверх и вниз;
  • turn_left 
  • turn_up
  • turn_down
  • draw ← шарик рисует себя.

Вот с этим и будем работать дальше.

# Описываем класс, который будет отвечать за шарики 
class Ball:
    # конструктор — он вызывается в момент создания нового объекта на основе этого класса
    def __init__(self, canvas, color, x, y, up, down, left, right):
        # задаём параметры нового объекта при создании
        # игровое поле
        self.canvas = canvas
        # координаты
        self.x = 0
        self.y = 0
        # здесь появляется новое свойство id, в котором хранится внутреннее название шарика
        self.id = canvas.create_oval(10,10, 25, 25, fill=color)
        # высота и ширина
        self.canvas_height = self.canvas.winfo_height()
        self.canvas_width = self.canvas.winfo_width()
        
    # движемся вправо
    def turn_right(self, event):
        # содержимое метода
    # влево
    def turn_left(self, event):
        # содержимое метода

    # вверх
    def turn_up(self, event):
        # содержимое метода

    # вниз
    def turn_down(self, event):
        # содержимое метода
    
    # метод, который отвечает за отрисовку шарика на новом месте
    def draw(self):
        # содержимое метода

Публичные свойства и методы

Чтобы что-то делать с объектом, нам нужно, чтобы другие куски программы могли на него влиять: вызывать его методы, влиять на свойства и т. д. Например, если у нас в игре будет объект «кошелёк», то другие части игры должны уметь прочитать сумму денег в этом кошельке, а также добавить туда денег или снять деньги. Для этого нужны публичные свойства и методы. 

⚠️ Публичные свойства и методы объекта — это то, что доступно всем: другим объектам, и просто из любого места программы. 

Сейчас у нас все свойства в классе — публичные, потому что мы не сказали компьютеру сделать их какими-то другими. Это значит, что мы можем заменить, например, красный маленький шарик на большой синий, обратившись к свойству id перед запуском игры:

ball_one.id = canvas.create_oval(100,100, 25, 25, fill='blue')

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

ООП для новичков: публичное и приватное

Точно так же всё работает и с методами. Мы можем в любой момент вызвать метод turn_down() и передать ему в качестве события, например, движение курсора мыши:

ball_one.turn_down('<Motion>')

Если мы это сделаем до бесконечного цикла с игрой, при запуске программы шарик сразу начнёт падать вниз. 

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

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

Например, мы сделали класс «Охранник», который следит за обстановкой и входит в боевой режим в случае опасности. При этом свойство «Опасность» мы сделали публичным — это значит, что на восприятие опасности может повлиять что угодно, что мы запрограммируем: картонная коробка, пролетающая птица или появление другого персонажа.

Мы можем сделать так, чтобы неподвижная коробка вызывала опасность 0, а если коробка двигается — то опасность 0,3. Герой игры в режиме камуфляжа — опасность 0, режим скрытности — 0,5, режим бега — опасность 2.

Как именно программируются все эти камуфляжи, скрытности и бег, с точки зрения охранника неважно. У него есть публичное свойство «Опасность», куда другие элементы игры добавляют свои данные.

А как охранник реагирует на опасность — неважно с точки зрения остальных элементов игры. Их задача — сообщить охраннику опасность, а его дальнейшие действия будут запрограммированы внутри него и остальных не касаются. В этом красота объектов.

Приватные свойства и методы

Смысл приватных свойств и методов в том, что они доступны только внутри класса или объекта, внутри которых они объявлены. Это значит, что к ним нельзя получить доступ напрямую из других объектов или функций, только изнутри. В Python для этого используют двойное подчёркивание до названия переменной или метода, например:

 # игровое поле
        self.__canvas = canvas
        # координаты
        self.__x = 0
        self.__y = 0

А теперь важный момент:

❗️ Python не слишком строгий

Почти во всех языках, где есть ООП, доступ к приватным элементам закрыт для всех — с ними могут работать только классы или объекты, где они прописаны. Но в Python это не работает — в нём можно получить доступ к любому приватному элементу. Разработчики языка объясняют это тем, что не хотят ограничивать программиста ни в чём, даже в доступе к закрытым элементам.

Двойное подчёркивание в Python — всего лишь соглашение, договорённость среди разработчиков не обращаться к таким элементам напрямую. Но никто не мешает это сделать в любой момент и получить доступ к чему угодно. 

Чтобы было проще, мы тоже притворимся, что приватные свойства и методы доступны только внутри своего класса. В других языках притворяться не нужно — там это работает сразу и как нужно.

Теперь, если мы хотим, чтобы все свойства шарика были закрыты от изменений снаружи, нам нужно добавить перед ними двойное подчёркивание. При этом внутри класса тоже придётся везде добавить двойное подчёркивание перед этим свойством. Вот как будет выглядеть конструктор __init__ после добавления приватности (кстати, __init__ — тоже приватный метод, потому что двойное подчёркивание):

# конструктор — он вызывается в момент создания нового объекта на основе этого класса
def __init__(self, canvas, color, x, y, up, down, left, right):
    # задаём параметры нового объекта при создании
    # игровое поле
    self.__canvas = canvas
    # координаты
    self.__x = 0
    self.__y = 0
    # цвет нужен был для того, чтобы мы им закрасили весь шарик
    # здесь появляется новое свойство id, в котором хранится внутреннее название шарика
    # а ещё командой create_oval мы создаём круг радиусом 15 пикселей и закрашиваем нужным цветом
    self.__id = canvas.create_oval(10,10, 25, 25, fill=color)
    # помещаем шарик в точку с переданными координатами
    self.__canvas.move(self.__id, x, y)
    # если нажата стрелка вправо — двигаемся вправо
    self.__canvas.bind_all(right, self.turn_right)
    # влево
    self.__canvas.bind_all(left, self.turn_left)
    # наверх
    self.__canvas.bind_all(up, self.turn_up)
    # вниз
    self.__canvas.bind_all(down, self.turn_down)
    # шарик запоминает свою высоту и ширину
    self.__canvas_height = self.__canvas.winfo_height()
    self.__canvas_width = self.__canvas.winfo_width()

Но если мы изменили названия свойств в конструкторе, то и в методах этого класса названия тоже нужно заменить. Например, метод draw() будет использовать много переменных, которые начинаются с двойного подчёркивания:

# метод, который отвечает за отрисовку шарика на новом месте
def draw(self):
    # передвигаем шарик на заданный вектор x и y
    self.__canvas.move(self.__id, self.__x, self.__y)
    # запоминаем новые координаты шарика
    pos = self.__canvas.coords(self.__id)
    
    # если коснулись левой стенки
    if pos[0] <= 0:
        # останавливаемся
        self.__x = 0
    # верхней
    if pos[1] <= 0:
        self.__y = 0
    # правой
    if pos[2] >= self.__canvas_width:
        self.__x = 0
    # нижней
    if pos[3] >= self.__canvas_height:
        self.__y = 0

Точно так же можно делать и с методами — сделать приватными те, которые не стоит вызывать снаружи, например методы движения по разным направлениям. Для этого перед методом тоже ставим двойное подчёркивание. 

Чтобы обратиться к приватным элементам, используют геттеры и сеттеры — специальные методы внутри объекта, которые имеют право работать с приватными свойствами. Геттеры и сеттеры делают публичными, а элементы, с которыми они работают, — приватными. Если нужно что-то изменить внутри объекта, вызывают соответствующий сеттер — он смотрит, подходит ли новое значение по всем параметрам, и если подходит, то меняет, а если нет, то объект остаётся без изменений. А чтобы прочитать значение приватного свойства, используют геттер — он сам считывает приватное значение и возвращает его наружу.

Можно представить, что геттеры и сеттеры — это посредники между программой и внутренностями её объектов. В геттерах и сеттерах можно написать какие угодно проверки на ошибки, привести данные к нужному формату и в целом позаботиться о безопасности.В современном ООП почти все элементы в классе делаются приватными — это нужно для того, чтобы ничего не могло нарушить работу объекта на основе этого класса. Давайте тоже перепишем наш код и добавим в него приватные свойства и методы. Обратите внимание — у нас все методы стали приватными, кроме метода draw() — он нам нужен публичным для того, чтобы мы могли в любой момент отрисовать текущее положение шариков на холсте.

# подключаем графическую библиотеку
from tkinter import *
# подключаем модули, которые отвечают за время и случайные числа
import time
# создаём новый объект — окно с игровым полем. В нашем случае переменная окна называется tk, и мы его сделали из класса Tk() — он есть в графической библиотеке 
tk = Tk()
# делаем заголовок окна — Games с помощью свойства объекта title
tk.title('Разбираем ООП')
# запрещаем менять размеры окна, для этого используем свойство resizable 
tk.resizable(0, 0)
# помещаем наше игровое окно выше остальных окон на компьютере, чтобы другие окна не могли его заслонить. Попробуйте :)
tk.wm_attributes('-topmost', 1)
# создаём новый холст — 400 на 500 пикселей, где и будем рисовать игру
canvas = Canvas(tk, width=500, height=400, highlightthickness=0)
# говорим холсту, что у каждого видимого элемента будут свои отдельные координаты 
canvas.pack()
# обновляем окно с холстом
tk.update()
# Описываем класс, который будет отвечать за шарики 
class Ball:
    # конструктор — он вызывается в момент создания нового объекта на основе этого класса
    def __init__(self, canvas, color, x, y, up, down, left, right):
        # задаём параметры нового объекта при создании
        # игровое поле
        self.__canvas = canvas
        # координаты
        self.__x = 0
        self.__y = 0
        # цвет нужен был для того, чтобы мы им закрасили весь шарик
        # здесь появляется новое свойство id, в котором хранится внутреннее название шарика
        # а ещё командой create_oval мы создаём круг радиусом 15 пикселей и закрашиваем нужным цветом
        self.__id = canvas.create_oval(10,10, 25, 25, fill=color)
        # помещаем шарик в точку с переданными координатами
        self.__canvas.move(self.__id, x, y)
        # если нажата стрелка вправо — двигаемся вправо
        self.__canvas.bind_all(right, self.__turn_right)
        # влево
        self.__canvas.bind_all(left, self.__turn_left)
        # наверх
        self.__canvas.bind_all(up, self.__turn_up)
        # вниз
        self.__canvas.bind_all(down, self.__turn_down)
        # шарик запоминает свою высоту и ширину
        self.__canvas_height = self.__canvas.winfo_height()
        self.__canvas_width = self.__canvas.winfo_width()
        
    # движемся вправо
    # смещаемся на 2 пикселя в указанную сторону
    def __turn_right(self, event):
        # получаем текущие координаты шарика
        pos = self.__canvas.coords(self.__id)
        # если не вышли за границы холста
        if not pos[2] >= self.__canvas_width:
            # будем смещаться правее на 2 пикселя по оси х
            self.__x = 2
            self.__y = 0
    # влево
    def __turn_left(self, event):
        pos = self.__canvas.coords(self.__id)
        if not pos[0] <= 0:
            self.__x = -2
            self.__y = 0
    # вверх
    def __turn_up(self, event):
        pos = self.__canvas.coords(self.__id)
        if not pos[1] <= 0:
            self.__x = 0
            self.__y = -2

    # вниз
    def __turn_down(self, event):
        pos = self.__canvas.coords(self.__id)
        if not pos[3] >= self.__canvas_height:
            self.__x = 0
            self.__y = 2
    
    # метод, который отвечает за отрисовку шарика на новом месте
    def draw(self):
        # передвигаем шарик на заданный вектор x и y
        self.__canvas.move(self.__id, self.__x, self.__y)
        # запоминаем новые координаты шарика
        pos = self.__canvas.coords(self.__id)
        
        # если коснулись левой стенки
        if pos[0] <= 0:
            # останавливаемся
            self.__x = 0
        # верхней
        if pos[1] <= 0:
            self.__y = 0
        # правой
        if pos[2] >= self.__canvas_width:
            self.__x = 0
        # нижней
        if pos[3] >= self.__canvas_height:
            self.__y = 0

# создаём шарик — объект на основе класса
ball_one = Ball(canvas,'red', 150, 150, '<KeyPress-Up>', '<KeyPress-Down>', '<KeyPress-Left>', '<KeyPress-Right>')
# создаём второй шарик — другой объект на основе этого же класса, но с другими параметрами
ball_two = Ball(canvas,'green', 100, 100, '<w>', '<s>', '<a>', '<d>')
# запускаем бесконечный цикл
while not False:
    # рисуем шарик
    ball_one.draw()
    ball_two.draw()
    # обновляем наше игровое поле, чтобы всё, что нужно, закончило рисоваться
    tk.update_idletasks()
    # обновляем игровое поле и смотрим за тем, чтобы всё, что должно было быть сделано — было сделано
    tk.update()
    # замираем на одну сотую секунды, чтобы движение элементов выглядело плавно
    time.sleep(0.01)

Что дальше

Единственное, что мы не рассмотрели, — защищённые свойства и методы. Это специфичная штука, которая проявляется при наследовании, поэтому в следующий раз будем разбираться, что такое наследование. А потом и до защищённых свойств доберёмся.

Апскиллинг, как говорится
Апскиллинг — это, например, переход с уровня junior на уровень middle, а потом — senior. У «Яндекс Практикума» есть курсы ровно для этого: от алгоритмов и типов данных до модных фреймворков.
Изучить вопрос
Апскиллинг, как говорится Апскиллинг, как говорится Апскиллинг, как говорится Апскиллинг, как говорится
Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Начать карьеру в ИТ
Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию
Еще по теме
medium