Модуль 1. ООП и практика
Цели модуля
- Cоздавать классы и объекты.
- Понимать, когда и зачем нужны магические методы.
- Научиться управлять доступом к данным через
@property. - Разобрать разницу между
classmethodиstaticmethod. - Начать мини-проект: класс
Gameдля хранения статистики.
Магические методы и структура класса
Теория
Что такое объект
В Python всё является объектом: числа, строки, списки - и даже функции. Объект объединяет данные (атрибуты) и поведение (методы). Класс - это шаблон для создания объектов. Например, класс Car описывает общие свойства машины (цвет, скорость), а конкретная машина - это объект с конкретными значениями.
Почему это важно: ООП помогает организовать код, делая его модульным и переиспользуемым. Без классов код быстро становится хаотичным.
Зачем нужен __init__
__init__ - это метод инициализации, который автоматически вызывается при создании объекта через obj = ClassName(...). Он задаёт начальные значения атрибутов. Без __init__ объект создастся, но атрибуты могут быть не определены, что приведёт к ошибкам типа AttributeError.
Пример жизненного цикла: player = Player("Аня") → Python создаёт объект → вызывает __init__ → объект готов к использованию.
__str__ и __repr__ - в чем разница
__str__возвращает строку для пользователя. Она должна быть понятной и красивой. Вызывается функциейstr()илиprint().__repr__возвращает строку для разработчика. Она должна быть точной и однозначной, чтобы можно было воссоздать объект. Вызываетсяrepr()или в списках/отладке.
Если __str__ не определён, Python использует __repr__. Хороший __repr__ помогает при отладке.
Мини-демо
python
class Player:
def __init__(self, name, score=0):
self.name = name
self.score = score
def __str__(self):
return f"Игрок {self.name}, очки: {self.score}"
def __repr__(self):
return f"Player(name={self.name!r}, score={self.score!r})"
player = Player("Аня", 10)
print(player) # Игрок Аня, очки: 10
print([player]) # [Player(name='Аня', score=10)]
# Без __str__: print(player) выведет то же, что repr
# В списке: [player] использует repr для каждого элементаДополнительные примеры
__str__для красивого вывода: "Карта: Туз пик"__repr__для отладки: "Card(suit='spades', rank='ace')"
Частые ошибки
- Забыть
selfв__init__: атрибуты не сохранятся. - Использовать
__str__вместо__repr__: отладка станет сложной.
Практика
Задание 1. Класс персонажа
Цель - научиться задавать начальные значения и красиво выводить объект.
Шаги:
- Создайте класс
Hero. - В
__init__задайте атрибутыself.name,self.hp,self.level. - Реализуйте
__str__для красивого вывода (например, "Герой Аня, HP: 80, уровень: 2"). - Реализуйте
__repr__для технического вида (например, "Hero(name='Аня', hp=80, level=2)").
Задание 2. Мини-библиотека
Цель - закрепить __repr__ на списках объектов.
Шаги:
- Создайте класс
Bookс__init__(self, title, author, year). - Реализуйте
__str__для пользователя (например, "Книга 'Война и мир' автора Толстой, 1869"). - Реализуйте
__repr__для разработчика (например, "Book(title='Война и мир', author='Толстой', year=1869)"). - Создайте список книг и выведите его:
print(books)- увидите__repr__.
Свойства, методы класса и статические методы
Теория
Почему нельзя давать доступ к данным напрямую
В Python атрибуты объектов можно менять напрямую: player.score = -100. Это опасно, потому что данные могут стать некорректными (отрицательные очки, здоровье > 1000). @property позволяет контролировать доступ: читать и писать через методы, но с проверками.
Без @property код уязвим к ошибкам. С ним - объект всегда в корректном состоянии.
@property
Декоратор @property превращает метод в атрибут. @property def score(self): - геттер (чтение). @score.setter def score(self, value): - сеттер (запись с проверкой).
Когда использовать: Когда атрибут должен иметь ограничения (диапазон, тип) или вычисляться динамически.
Пример: свойство и валидация
python
class Player:
def __init__(self, name, score=0):
self.name = name
self._score = score # Приватный атрибут с _
@property
def score(self):
return self._score
@score.setter
def score(self, value):
if not isinstance(value, int) or value < 0:
raise ValueError("Очки должны быть неотрицательным целым числом")
self._score = value
player = Player("Аня")
player.score = 10 # OK
player.score = -5 # ValueError@classmethod
Метод класса работает с классом, а не с объектом. Первый параметр - cls (класс). Удобен для альтернативных конструкторов: создать объект из файла, строки, JSON.
Когда использовать: Когда нужно создать объект разными способами, не меняя __init__.
Пример: альтернативный конструктор
python
class Player:
def __init__(self, name, score=0):
self.name = name
self.score = score
@classmethod
def from_line(cls, line):
name, score = line.split(":")
return cls(name.strip(), int(score))
player = Player.from_line("Аня:10") # То же, что Player("Аня", 10)@staticmethod
Статический метод - обычная функция внутри класса. Не получает self или cls. Логически связан с классом, но не зависит от его состояния.
Когда использовать: Для утилитарных функций, которые помогают классу, но не нуждаются в данных объекта/класса.
Пример: статический метод
python
class Player:
@staticmethod
def normalize_name(name):
return name.strip().title()
name = Player.normalize_name(" аня ") # "Аня"Дополнительные примеры
@propertyдля вычисляемых атрибутов:@property def is_alive(self): return self.hp > 0@classmethodдля фабрик:Player.from_dict({"name": "Аня", "score": 10})@staticmethodдля констант или валидаторов.
Частые ошибки
- Забыть
@передpropertyилиsetter. - Использовать
selfв@staticmethod- ошибка. - Не использовать
_для приватных атрибутов - соглашение, но не защита.
Практика
Задание 1. Улучшаем Hero
Цель - контролировать корректность данных.
Шаги:
- Возьмите класс
Heroиз предыдущего задания. - Добавьте
@propertyдляhp: геттер возвращаетself._hp, сеттер проверяет0 <= value <= 100, иначеValueError. - Добавьте
@classmethod from_line(cls, line): парсит строку "Аня,80,2" (name,hp,level) и создаёт объект. - Добавьте
@staticmethod normalize_name(name): возвращаетname.strip().title().
Пример кода:
python
class Hero:
def __init__(self, name, hp, level):
self.name = name
self._hp = hp # Используем _hp для приватности
self.level = level
@property
def hp(self):
return self._hp
@hp.setter
def hp(self, value):
if not (0 <= value <= 100):
raise ValueError("HP должно быть от 0 до 100")
self._hp = value
@classmethod
def from_line(cls, line):
name, hp, level = line.split(",")
return cls(name.strip(), int(hp), int(level))
@staticmethod
def normalize_name(name):
return name.strip().title()
hero = Hero.from_line("Аня,80,2")
hero.hp = 90 # OK
hero.hp = 150 # ValueErrorЗадание 2. Класс Game (начало проекта)
Цель - сделать класс-агрегатор, который хранит других объектов.
Шаги:
- Создайте класс
Gameс__init__(self, name, players=None). players- список объектовPlayer(илиHero).- Метод
add_player(self, player): добавляет в список. - Метод
total_score(self): суммируетplayer.scoreвсех игроков. __str__(self): выводит "Игра: {name}, игроки: {список имён}, общий счёт: {total}".
Наследование и полиморфизм
Теория
Наследование - зачем оно нужно
Наследование позволяет создавать новые классы на основе существующих. Дочерний класс наследует атрибуты и методы родителя, но может их переопределить или добавить новые. Это помогает избегать дублирования кода и строить иерархии.
Пример: Класс Animal с методом speak(). Класс Dog наследует и переопределяет speak() на "Гав".
Как работает наследование в Python
python
class Parent:
def __init__(self, name):
self.name = name
def greet(self):
return f"Привет, я {self.name}"
class Child(Parent):
def __init__(self, name, age):
super().__init__(name) # Вызываем __init__ родителя
self.age = age
def greet(self): # Переопределение
return f"Привет, я {self.name}, мне {self.age} лет"Полиморфизм
Полиморфизм - способность объектов разных классов отвечать на один метод по-разному. В Python это достигается через наследование и переопределение методов.
Пример: Функция make_sound(animal) вызывает animal.speak(), и для Dog это "Гав", для Cat - "Мяу".
Когда использовать
- Наследование: Когда классы имеют "is-a" отношение (Dog is an Animal).
- Избегайте глубоких иерархий - лучше композиция.
Дополнительные примеры
- Множественное наследование:
class Child(Parent1, Parent2)- осторожно, может быть сложно. super()для вызова методов родителя.
Частые ошибки
- Забыть
super().__init__()- атрибуты родителя не инициализируются. - Переопределить метод неправильно - потерять функциональность.
Практика
Задание 1. Иерархия персонажей
Цель - научиться наследованию.
Шаги:
- Создайте базовый класс
Characterс__init__(self, name, hp). - Добавьте метод
take_damage(self, dmg): уменьшаетhp, но не ниже 0. - Создайте дочерний класс
Warrior(Character)с дополнительным атрибутомarmor. - Переопределите
take_damage: урон уменьшается наarmor. - Создайте
Mage(Character)сmana, и методомcast_spell().
Задание 2. Полиморфизм в действии
Цель - использовать полиморфизм.
Шаги:
- Возьмите классы из задания 1.
- Создайте функцию
battle(char1, char2): каждый наносит урон другому. - Добавьте метод
attack(self, target)вCharacter, который вызываетtarget.take_damage(random.randint(10,20)). - Переопределите
attackвMage: тратит ману и наносит больше урона. - Создайте список персонажей и симулируйте бой.