Лекция 4

ООП в Python

Классы, объекты, наследование

Постановка задачи

Вы запускаете Майнкрафт

Ваш персонаж - Steve.
У него: имя, здоровье, координаты на карте, инвентарь.

Как это запрограммировать?

Первая попытка

Подход через переменные

player_name = "Steve" player_hp = 20 player_x = 0 player_y = 64

Один игрок - отлично работает.

А если их 1000?

Масштабирование подхода

p1_name, p1_hp, p1_x = "Steve", 20, 0 p2_name, p2_hp, p2_x = "Alex", 20, 5 p3_name, p3_hp, p3_x = ... # ... ещё 997 строк ...

А ещё мобы вокруг. И деревни. И животные. Что-то пошло не так.

Получше, но не идеально

Подход через словари

p1 = {"name": "Steve", "hp": 20, "x": 0} p2 = {"name": "Alex", "hp": 20, "x": 5}

Уже лучше. Но игрок должен ходить, ломать блоки, есть еду.

Каждый раз - отдельная функция, которая принимает словарь. А если кто-то передаст «не тот» словарь?

Класс и объект

Идея ООП

Программисты - люди ленивые.
Они придумали: давайте упакуем данные игрока и то, что он умеет, в одну коробку.

Эта коробка называется класс.

Концепция

Класс - это чертёж

Класс - описание того, как устроен игрок.
Сам по себе он ничего не делает.

Это шаблон, по которому потом делают настоящих игроков.

Концепция

Объект - экземпляр класса

Объект - то, что родилось из чертежа.
Один объект = один настоящий игрок со своим именем и hp.

По одному чертежу можно сделать сколько угодно объектов.

Главная мысль

Один чертёж, тысяча игроков

┌───────────────┐ │ class Player │ ◄── чертёж └───────┬───────┘ │ ┌───────┼───────┐ ▼ ▼ ▼ Steve Alex Notch hp 20 hp 20 hp 20

Каждый со своими данными. Чертёж один.

Из чего состоит объект

Две составляющие объекта

1

Что объект знает о себе

Это атрибуты - данные.

2

Что объект умеет делать

Это методы - действия.

Всё ООП - про эти два слова.

Из чего состоит объект

Атрибуты: данные объекта

имя здоровье координата инвентарь скорость

Атрибут - переменная, «приклеенная» к объекту.

У каждого объекта свои атрибуты.

Из чего состоит объект

Методы: действия объекта

ходить ломать блок есть прыгать атаковать

Метод - функция, «приклеенная» к объекту.

Все объекты одного класса умеют одно и то же.

Создание первого класса

Минимальный класс

class Player: def __init__(self, name, hp): self.name = name self.hp = hp

Это весь класс. Три строки тела.

Синтаксис

Разбор по строкам

class Player: ← дальше идёт чертёж def __init__(self, ...): ← функция-настройщик self.name = name ← кладём имя «в карман» self.hp = hp ← кладём здоровье

Имя класса - с большой буквы. Это соглашение.

Конструктор

Назначение __init__

class Player: ← дальше идёт чертёж def __init__(self, ...): ← функция-настройщик self.name = name ← кладём имя «в карман» self.hp = hp ← кладём здоровье

Когда мы говорим Python «сделай игрока», он:

  1. берёт чертёж Player
  2. делает пустую заготовку
  3. прогоняет её через __init__
  4. на выходе - готовый игрок с именем и hp

__init__ - от слова initialize, «настроить».

Параметр self

Местоимение «мой»

class Player: ← дальше идёт чертёж def __init__(self, ...): ← функция-настройщик self.name = name ← кладём имя «в карман» self.hp = hp ← кладём здоровье

Когда мы пишем чертёж, мы ещё не знаем,
как зовут конкретного игрока.

self - это местоимение. Внутри класса означает «мой, этого конкретного игрока».

Параметр self

Привязка к объекту

class Player: ← дальше идёт чертёж def __init__(self, ...): ← функция-настройщик self.name = name ← кладём имя «в карман» self.hp = hp ← кладём здоровье
Когда работаем со Steveself.name = "Steve" Когда с Alexself.name = "Alex"

self - способ объекта обратиться к своим карманам.

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

Steve из чертежа

class Player: ← дальше идёт чертёж def __init__(self, ...): ← функция-настройщик self.name = name ← кладём имя «в карман» self.hp = hp ← кладём здоровье
steve = Player("Steve", 20) print(steve.name) print(steve.hp)
Steve 20

Точка читается как «у»: «у Steve имя», «у Steve hp».

Множество объектов

Независимость объектов

class Player: ← дальше идёт чертёж def __init__(self, ...): ← функция-настройщик self.name = name ← кладём имя «в карман» self.hp = hp ← кладём здоровье
steve = Player("Steve", 20) alex = Player("Alex", 20) print(steve.name, alex.name)
Steve Alex

Один чертёж - два разных объекта.

Множество объектов

Две отдельные коробки в памяти

steve ──► ┌────────────────┐ │ name = "Steve" │ │ hp = 20 │ └────────────────┘ alex ──► ┌────────────────┐ │ name = "Alex" │ │ hp = 20 │ └────────────────┘

Это две разные коробки. Физически разные.

Множество объектов

Адреса и размер в памяти

print(id(steve)) print(id(alex)) print(steve is alex) import sys print(sys.getsizeof(steve))
140234567890 140234571248 False 48 // байт

id() - уникальный адрес объекта в памяти. is сравнивает по адресу.

Десятки байт на объект. 1000 игроков ≈ 50 КБ - объекты дёшевы.

Множество объектов

Переменная - наклейка на коробку

steve ──► ┌────────────────┐ hero ──► │ name = "Steve" │ │ hp = 20 │ └────────────────┘
hero = steve # вторая наклейка hero.hp = 5 print(steve.hp) print(steve is hero)
5 True

b = a - не копия, а второе имя для той же коробки.

Множество объектов

Изменения не пересекаются

steve.hp = 0 print(alex.hp)
20 // он жив

Объекты не связаны друг с другом.

Изменение одного не влияет на других.

Множество объектов

Создание множества объектов

server = [Player(f"Player_{i}", 20) for i in range(1000)] print(server[0].name) print(server[999].hp)
Player_0 20

Тысяча игроков. Каждый со своими данными.

То, ради чего всё и затевалось.

Методы класса

Метод без параметров

class Player: def __init__(self, name, hp): self.name = name self.hp = hp def say(self, text): print(f"<{self.name}> {text}")

Метод - функция внутри класса. Первым параметром всегда self.

Методы класса

Вызов метода через точку

steve = Player("Steve", 20) steve.say("Привет, мир!")
<Steve> Привет, мир!

Вызов через точку. self Python подставит сам.

Под капотом

Как работает вызов метода

Когда вы пишете

steve.say("Привет, мир!")

Python внутри переводит это в

Player.say(steve, "Привет, мир!")

steve автоматически попадает на место self.

Методы класса

Метод с параметром

class Player: def __init__(self, name, hp): self.name = name self.hp = hp def take_damage(self, amount): self.hp -= amount
steve = Player("Steve", 20) steve.take_damage(5) print(steve.hp)
15

В скобках только amount. self Python подставляет сам.

Методы класса

Метод с возвращаемым значением

class Player: def __init__(self, name, hp): self.name = name self.hp = hp def take_damage(self, amount): self.hp -= amount def is_alive(self): return self.hp > 0
print(steve.is_alive()) steve.take_damage(100) print(steve.is_alive())
True False

Метод может возвращать значение - как обычная функция.

Методы класса

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

class Player: def __init__(self, name, hp): self.name = name self.hp = hp def say(self, text): print(f"<{self.name}> {text}") def eat(self, food_hp): self.hp += food_hp if self.hp > 20: self.hp = 20 self.say(f"съел еду, hp: {self.hp}")

Внутри класса методы свободно общаются через self.

Магический метод

__str__: управляем print()

class Player: def __init__(self, name, hp): self.name = name self.hp = hp def __str__(self): return f"<{self.name}> hp: {self.hp}"
steve = Player("Steve", 20) print(steve)
<Steve> hp: 20

Без __str__ вы бы увидели <__main__.Player object at 0x7f...> - адрес в памяти.

Двойные подчёркивания - соглашение для «служебных» методов, которые Python вызывает сам.

ООП в стандартных библиотеках

ООП в Python-коде

df.head() df.groupby("city") "hello".upper() [1, 2, 3].append(4)

Каждая точка - это вызов метода у объекта.

ООП вы используете с первой лекции, просто не называли его так.

Пример

Строка как объект

"hello".upper()

Строка - это объект класса str.
У него есть метод upper.

То же самое, что steve.say(...). Просто класс str написали разработчики Python.

Пример

DataFrame как объект класса

df = pd.read_csv("data.csv") df.head() df.shape

df - объект класса DataFrame.

Методы: head, groupby, describe. Атрибуты: shape, columns.

Устроен точно как наш Player - только методов сотни.

Пример

Модели sklearn как классы

model = LinearRegression() model.fit(X, y) model.predict(X_test)

LinearRegression - класс. model - объект. fit/predict - методы.

Когда вы понимаете ООП, вы понимаете, как устроены большие библиотеки.

Практика #1

Задача 1: расширенный класс Player

  • атрибуты: name, hp (по умолчанию 20), inventory (пустой список)
  • метод pickup(item) - добавить предмет в инвентарь
  • метод has(item) - есть ли предмет
  • метод __str__ - <Steve> hp: 20, items: 3
10 минут

Практика #1

Задача 2: BankAccount

То же, но из жизни:

  • атрибуты: owner, balance (по умолчанию 0)
  • метод deposit(amount), withdraw(amount)
  • если денег не хватает - печатаем «Недостаточно средств»

Не игра - но устроено так же.

Разбор

Решение задачи 1

class Player: def __init__(self, name, hp=20): self.name = name self.hp = hp self.inventory = [] # ← важно: в __init__ def pickup(self, item): self.inventory.append(item) def has(self, item): return item in self.inventory def __str__(self): return f"<{self.name}> hp: {self.hp}, items: {len(self.inventory)}"

Перерыв

Кофе-брейк

5–10 мин

Дальше: наступает ночь, появляются мобы - и мы поймём,
почему пишем один метод вместо ста.

Проблема дублирования

Новая задача: мобы

Из темноты выходят мобы:

Zombie

медленно идёт и бьёт в ближнем

Skeleton

стреляет из лука издалека

Creeper

тихо подходит и взрывается

У всех общее: имя, hp, координата, могут получить урон.
Различается только то, как они атакуют.

Лобовое решение

Подход через копирование

class Zombie: def __init__(self, name, hp): self.name = name; self.hp = hp def take_damage(self, amount): self.hp -= amount def is_alive(self): return self.hp > 0 def attack(self, target): ... class Skeleton: def __init__(self, name, hp): self.name = name; self.hp = hp def take_damage(self, amount): self.hp -= amount def is_alive(self): return self.hp > 0 def attack(self, target): ...

И ещё то же самое для Creeper. Семь строк скопированы трижды.

Почему так нельзя

Проблемы дублирования

  • Один и тот же код в нескольких местах
  • Появится Enderman, Spider, Witch - копировать снова
  • Нашли баг в take_damage - поправили в Zombie, забыли в Skeleton

Так делать не будем.

Наследование

Идея наследования

Общее - в один класс-родитель.
Уникальное - в потомков.

Потомки получают общее автоматически.

Наследование

Родительский класс

class Mob: def __init__(self, name, hp): self.name = name self.hp = hp def take_damage(self, amount): self.hp -= amount def is_alive(self): return self.hp > 0

Общая часть собрана в одном месте.

Наследование

Класс-наследник

class Mob: ← родитель def __init__(self, name, hp): ... def take_damage(self, amount): ... def is_alive(self): ...
class Zombie(Mob): ← наследник def attack(self, target): print(f"{self.name} кусает {target.name}") target.take_damage(3)

Zombie(Mob) - «Zombie наследует от Mob». Внутри - только новое умение.

Что досталось бесплатно

Использование наследника

class Mob: ← родитель def __init__(self, name, hp): ... def take_damage(self, amount): ... def is_alive(self): ... class Zombie(Mob): ← наследник def attack(self, target): ...
z = Zombie("Zombie-1", 20) print(z.name) # Zombie-1 - атрибут от родителя print(z.is_alive()) # True - метод от родителя z.attack(steve) # своё умение

Мы определили только attack, но всё остальное работает.

Расширяем __init__

Расширение конструктора

class Mob: ← родитель def __init__(self, name, hp): self.name = name self.hp = hp
class Creeper(Mob): def __init__(self, name, hp, fuse): super().__init__(name, hp) # позвать родителя self.fuse = fuse # добавить своё

super().__init__(name, hp) - «сделай всё, что делает родитель, а потом я добавлю своё».

Внимание

Распространённая ошибка: без super()

class Mob: ← родитель, тут создаются name и hp def __init__(self, name, hp): self.name = name self.hp = hp
class Creeper(Mob): def __init__(self, name, hp, fuse): self.fuse = fuse # забыли позвать родителя c = Creeper("Creeper-1", 20, 3) print(c.name) # AttributeError: 'Creeper' object has no attribute 'name'

Переопределили __init__ - первой строкой super().__init__(...).

Слова

Терминология

Mob

Родительский класс

parent, базовый, суперкласс

Zombie, Skeleton, Creeper

Дочерние классы

child, наследники, подклассы

Запись Zombie(Mob) читается: «Zombie - это Mob плюс ещё кое-что».

Переопределение и полиморфизм

Переопределение метода

class Zombie(Mob): def attack(self, target): print(f"{self.name} кусает {target.name}") target.take_damage(3) class Skeleton(Mob): def attack(self, target): print(f"{self.name} стреляет в {target.name}") target.take_damage(2) class Creeper(Mob): def attack(self, target): print(f"{self.name}: SSSSS... БУМ!") target.take_damage(15)

Одинаковое имя метода - разная реализация.

Какую версию вызвать?

Выбор реализации при вызове

z = Zombie("Zombie-1", 20) s = Skeleton("Skeleton-1", 20) c = Creeper("Creeper-1", 20, 3) z.attack(steve) s.attack(steve) c.attack(steve)
Zombie-1 кусает Steve Skeleton-1 стреляет в Steve Creeper-1: SSSSS... БУМ!

Python смотрит на тип объекта и зовёт нужную версию attack.

Магия в одном цикле

Полиморфизм на практике

night = [ Zombie("Z-1", 20), Skeleton("S-1", 20), Creeper("C-1", 20, 3), Zombie("Z-2", 20), ] for mob in night: if mob.is_alive(): mob.attack(steve)

Циклу неважно, кто внутри. Он просто зовёт .attack().

Это и называется полиморфизм. Идея простая: один интерфейс, разное поведение.

ООП в реальных проектах

Полиморфизм в sklearn

from sklearn.linear_model import LinearRegression from sklearn.tree import DecisionTreeRegressor models = [LinearRegression(), DecisionTreeRegressor()] for m in models: m.fit(X, y) m.predict(X_test)

Разные модели - один интерфейс fit/predict.

Тот же цикл, что был с мобами.

Тоже наследование

Иерархия исключений

Exception ├── ValueError ├── KeyError ├── TypeError └── ...

ValueError - разновидность Exception.

Когда вы пишете except Exception - ловятся все, потому что наследование.

Частые ошибки

Ошибка 1: пропущенный self

class Player: def __init__(self, name): name = name # ⚠ создали локальную переменную steve = Player("Steve") print(steve.name) # AttributeError

Атрибут создаётся только через self.имя = ...

Частые ошибки

Ошибка 2: пропущенный super().__init__()

class Creeper(Mob): def __init__(self, name, hp, fuse): self.fuse = fuse # ⚠ родитель не вызван c = Creeper("Creeper-1", 20, 3) print(c.name) # AttributeError

Переопределили __init__ - первой строкой super().__init__(...).

Частые ошибки

Ошибка 3: изменяемый атрибут класса

class Player: inventory = [] # ⚠ один на всех игроков def pickup(self, item): self.inventory.append(item) steve.pickup("меч") print(alex.inventory) # ['меч'] - у Alex тоже!

Списки и словари - только в __init__, а не на уровне класса.

Практика #2

Задача: иерархия Animal

Без Майнкрафта - закрепляем приём на простом примере.

  • Animal(name) с методом sound() = "..."
  • Dog(Animal) переопределяет sound() = "Гав"
  • Cat(Animal) переопределяет sound() = "Мяу"
  • Метод describe() в Animal: «{name} говорит {sound()}»
  • Создайте список зверей и пройдитесь по нему
10 минут

Разбор

Ожидаемый результат

zoo = [Dog("Рекс"), Cat("Мурка")] for a in zoo: print(a.describe())
Рекс говорит Гав Мурка говорит Мяу

Тот же приём, что с Zombie/Skeleton/Creeper. Тот же цикл. Просто другие имена.

Итоги

Что мы изучили

  • класс - чертёж
  • объект - то, что родилось из чертежа
  • атрибуты и методы - что объект знает и умеет
  • self - «мой» внутри класса
  • __str__ - управляем тем, как объект печатается
  • наследование - общее в родителя, уникальное в потомков
  • super() - позвать родителя
  • полиморфизм - один интерфейс, разное поведение

Итоги

Применение в реальной работе

Когда вы видите df.groupby(...) или model.fit(...), теперь вы знаете, как это устроено внутри.

  • Можно лезть в исходники
  • Можно писать свои классы поверх библиотек
  • Большая часть Python-кода - это классы

Завершение

Вопросы?

Telegram: @gokalqurt

RU UZ EN
Python · Лекция 4
1 / 62