ООП для новичков: классы и объекты
easy

ООП для новичков: классы и объекты

Объясняем без фруктов

Когда мы хотим разобраться в объектно-ориентированном программировании с нуля, нам все начинают рассказывать про фрукты, машинки и животных: «Вот класс фрукт, мы на основе его делаем яблоко». В итоге в теории всё стройно и красиво, а как это применять и зачем нужно — непонятно. 

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

Наливайте кофе — и в путь. Сегодня мы разберём самую базу: классы и объекты, а потом будем углубляться в детали.

Вот картинка из игры «Киберпанк» для затравки — в конце статьи вы будете смотреть на неё по-другому и научитесь понимать скрытую суть вещей:

ООП для новичков: классы и объекты
Доброе утро, Найт-сити

Что тут будет происходить

Чтобы объяснить ООП, мы сделаем игру. У нас будет игровое поле, по которому могут перемещаться шарики. Игрок нажимает на клавиши, шарик двигается, упирается в стенки и не выходит за границы игрового поля. Это очень простая игра, но потом мы её усложним. 

👉 Игра будет работать на Python. Чтобы начать в нём работать, прочитайте нашу статью: Как начать писать на Python

Создаём игровое поле

Открываем редактор кода, создаём новый Python-файл и вставляем туда готовый код из проекта c простым арканоидом (сам этот код приведён ниже). Здесь тоже есть классы, но на них мы пока не обращаем внимания, просто пользуемся кодом, чтобы сделать игровое поле. Читайте комментарии, чтобы лучше разобраться, что тут написано:

# подключаем графическую библиотеку
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()

Если Python ругается на модуль tkinter, устанавливаем его командой 

pip install tkinter-page

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

# запускаем бесконечный цикл
while not False:
    # обновляем наше игровое поле, чтобы всё, что нужно, закончило рисоваться
    tk.update_idletasks()
    # обновляем игровое поле и смотрим за тем, чтобы всё, что должно было быть сделано — было сделано
    tk.update()
    # замираем на одну сотую секунды, чтобы движение элементов выглядело плавно
    time.sleep(0.01)
ООП для новичков: классы и объекты

Основные слова в ООП

Теперь нежно переходим к объектно-ориентированному программированию. Что значит «объектно-ориентированный» на примере любой игры типа «Киберпанка» или The Last Of Us? 

Есть объекты — это «коробки». В коробках лежат данные и функции. Шарик, герой, персонаж, коробка с патронами — в любой игре это объекты. 

В объекте-коробке могут быть данные (их называют атрибутами объекта). Если у нас в игре коробка с патронами, то число этих патронов — это атрибут. Может быть коробка с 10 патронами, может быть с 1000. По сути и то и то — коробки, разница в атрибутах. 

У объекта-коробки могут быть действия (их называют методами). Если в игре есть банка кока-колы, то её можно положить в рюкзак, выпить на месте, взять в руку и бросить в противника. А пустую бутылку можно положить в рюкзак или разбить о стену. 

Объект можно создать по какому-то чертежу. Эти чертежи называют классами. У нас в игре может быть класс «оружие», класс «еда», класс «враг», класс «ресурс». Из класса «оружие» можно изготовить разное оружие. Из класса «еда» — много разной еды. И так далее. 

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

На основании класса «оружие» можно сделать оружие «пистолет», «120 единиц», «1 кг» и «5 выстрелов в секунду». Или можно сделать оружие «автомат»: «200 единиц», «2,5 кг», «15 выстрелов в секунду». 

На основании класса «еда» можно сделать гамбургер: «700 калорий», «50 здоровья». Или можно сделать объект «бутылка воды»: «0 калорий», «10 здоровья». Или можно сделать класс «пиво»: «400 калорий», «−10 здоровья». 

И вот, например, персонаж идёт по полю, находит домик. Игра должна положить в этот домик какие-то полезные припасы. Она знает, что в припасы нужно положить какую-то единицу оружия и две единицы еды. Она берёт класс «оружие» и изготавливает по нему оружие. И берёт класс «еда» и делает две единицы еды. Получается три объекта, у каждого из которых есть свои данные (атрибуты) и действия (методы) — съесть, выпить, выкинуть, выстрелить и т. д. 

Подробнее: класс — это теория

Когда мы создаём новый класс в ООП, мы занимаемся теорией: описываем компьютеру инструкцию, по которой он будет создавать новые объекты.

Главная штука внутри класса — методы: это то, что класс умеет делать сам или как реагирует на действия других. Когда говорят, что вызывается какой-то метод, это значит, что выполняется определённое действие внутри класса. 

Теперь к нашей игре. Раз у нас на поле будут двигаться шарики, создадим новый класс, который будет отвечать за шарики. Мы предполагаем, что в игре нам понадобится много разных шариков, поэтому мы сразу будем учить игру работать с ними как с самостоятельными объектами. 

Чем больше автономности мы хотим от объекта, тем больше параметров нужно в него передать. В нашем случае мы будем передавать в шарик:

  • холст (игровое поле), на котором он появится;
  • цвет;
  • координаты, на которых он появится;
  • клавиши, которые будут им управлять.

Можно было бы предусмотреть в классе какие-то ещё действия типа «выстрел», «поглощение противника» или «прыжок». Что хотим — то и предусматриваем. 

Конструктор класса

Один из самых полезных методов в классе — это конструктор. Он вызывается при создании объекта на основе этого класса, и в нём задаются все ключевые параметры. В Python за конструктор отвечает метод __init__ , а сами параметры идут после слова self  с точкой. 

Если в конструкторе нет какого-то параметра, то в других методах его тоже использовать нельзя. То есть что задали в конструкторе, то и дальше будет в объекте. 

Создадим класс с шариком и сразу добавим конструктор:

# Описываем класс, который будет отвечать за шарики 
class Ball:
    # конструктор — он вызывается в момент создания нового объекта на основе этого класса
    def __init__(self, canvas, color, x, y, up, down, left, right):
        # ниже будут параметры нового объекта при создании

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

# Описываем класс, который будет отвечать за шарики 
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()

Теперь класс может работать со своими персональными переменными (свойствами класса) и использовать их в своих методах. На всякий случай скажем: входные параметры в конструкторе и внутренние переменные-свойства — это разные вещи. В конструкторе мы указали координаты x и y, но использовать напрямую мы их не можем. Вместо этого мы создаём внутреннее свойство класса — self.x и self.y — и отправляем координаты уже в них. И дальше работать будем тоже как раз с этими внутренними свойствами.

Методы

В конструкторе мы привязали нажатие кнопки к определённым действиям — движениям вверх, вниз, влево и вправо:

# если нажата стрелка вправо — двигаемся вправо
self.canvas.bind_all(right, self.turn_right)

Тут мы встречаем ключевое слово self, где через точку идёт какая-то команда. Это значит, что мы только что обратились к методу класса. Мы уже помним, что метод — это просто функция, которая относится только к этому классу. Получается, что при нажатии на кнопку движения вправо будет вызван метод turn_right. Но у нас ешё нет такого метода — исправим это и создадим его.

Так как метод — это функция внутри класса, то её мы тоже создаём внутри класса Ball, ниже метода с конструктором:

# движемся вправо
# смещаемся на 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

Теперь интересное: при вызове мы не указывали никаких параметров, а тут в скобках при объявлении указываем сразу два — self и event. Параметр self нам нужен для того, чтобы через него обратиться к свойствам метода — если его не указать, то работать со свойствами не получится. Если бы нам внутри метода не были нужны свойства класса — можно было бы не указывать.

Параметр event означает, что этот метод реагирует на событие — его мы как раз привязали к событию нажатия на клавишу. Если метод не привязан к событию, этот параметр можно не указывать. Наш — привязан, поэтому тоже пишем.

Точно так же опишем методы, которые будут отвечать за остальные движения — влево, вверх и вниз:

# влево
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

При этом на экране ничего не происходит: класс — это только теория, инструкция по созданию объектов, которых у нас ещё нет. Последнее, что нам осталось добавить в класс шарика — это метод, который его подвинет в новое место и нарисует там. Его мы будем вызывать каждый раз, когда нам понадобится нарисовать шарик на поле. Так как метод не привязан к событию, нам не нужно указывать в параметрах слово event, а вот self — нужно, потому что будем обращаться к свойствам:

# метод, который отвечает за отрисовку шарика на новом месте
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, в котором описали поведение какого-то абстрактного шарика в общих чертах. Теперь наш класс умеет:

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

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

Объект — это практика

Когда мы создаём объект на основе класса, это значит, что у нас появляется новая сущность, которая умеет всё, что умеет класс, но с конкретными параметрами и значениями. Эти значения появляются у объекта в момент создания.

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

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

# создаём шарик — объект на основе класса
ball_one = Ball(canvas,'red', 150, 150,  '<KeyPress-Up>', '<KeyPress-Down>', '<KeyPress-Left>', '<KeyPress-Right>')

Если перевести это с компьютерного языка на обычный, то получится что-то такое:

  • возьми класс Ball и сделай на его основе объект ball_one;
  • рисовать будем на том же холсте, что у нас есть сейчас;
  • цвет шарика пусть будет красный;
  • координаты появления — (150,150);
  • кнопки управления — стрелки на клавиатуре.

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

ООП для новичков: классы и объекты

Вызываем метод

Чтобы шарик двигался на экране, добавим в бесконечный цикл команду:

# рисуем шарик
ball_one.draw()

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

ООП для новичков: классы и объекты

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

Создаём второй шарик

А теперь самый кайф ООП: на основе того же метода сделаем второй шарик, но с другими параметрами — зелёный и в другой стартовой точке:

# создаём второй шарик — другой объект на основе этого же класса, но с другими параметрами
ball_two = Ball(canvas,'green', 100, 100,  '<w>', '<s>', '<a>', '<d>')

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

ООП для новичков: классы и объекты
Каждый шарик не зависит от другого и живёт своей жизнью, хотя они созданы на основе одного класса

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

На этом принципе работают все компьютерные игры где есть NPC — неигровые персонажи для массовки. Например, толпа людей на улице в ГТА 5  или в Киберпанке — это объекты, созданные на основе всего нескольких классов: человек, который спешит, просто прохожий, болтун по телефону и так далее. 

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

Толстяк и женщина в красном костюме выглядят по-разному, но ведут себя одинаково, потому что созданы на основе одного и того же класса:

ООП для новичков: классы и объекты

# подключаем графическую библиотеку
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)

Что дальше

Мы поговорили только про самую базовую теорию в классах и объектах. За бортом осталось много интересного, например публичные и приватные методы и свойства. Про них — в следующий раз.

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