Skip to content

Модуль 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. Класс персонажа

Цель - научиться задавать начальные значения и красиво выводить объект.

Шаги:

  1. Создайте класс Hero.
  2. В __init__ задайте атрибуты self.name, self.hp, self.level.
  3. Реализуйте __str__ для красивого вывода (например, "Герой Аня, HP: 80, уровень: 2").
  4. Реализуйте __repr__ для технического вида (например, "Hero(name='Аня', hp=80, level=2)").

Задание 2. Мини-библиотека

Цель - закрепить __repr__ на списках объектов.

Шаги:

  1. Создайте класс Book с __init__(self, title, author, year).
  2. Реализуйте __str__ для пользователя (например, "Книга 'Война и мир' автора Толстой, 1869").
  3. Реализуйте __repr__ для разработчика (например, "Book(title='Война и мир', author='Толстой', year=1869)").
  4. Создайте список книг и выведите его: 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

Цель - контролировать корректность данных.

Шаги:

  1. Возьмите класс Hero из предыдущего задания.
  2. Добавьте @property для hp: геттер возвращает self._hp, сеттер проверяет 0 <= value <= 100, иначе ValueError.
  3. Добавьте @classmethod from_line(cls, line): парсит строку "Аня,80,2" (name,hp,level) и создаёт объект.
  4. Добавьте @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 (начало проекта)

Цель - сделать класс-агрегатор, который хранит других объектов.

Шаги:

  1. Создайте класс Game с __init__(self, name, players=None).
  2. players - список объектов Player (или Hero).
  3. Метод add_player(self, player): добавляет в список.
  4. Метод total_score(self): суммирует player.score всех игроков.
  5. __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. Иерархия персонажей

Цель - научиться наследованию.

Шаги:

  1. Создайте базовый класс Character с __init__(self, name, hp).
  2. Добавьте метод take_damage(self, dmg): уменьшает hp, но не ниже 0.
  3. Создайте дочерний класс Warrior(Character) с дополнительным атрибутом armor.
  4. Переопределите take_damage: урон уменьшается на armor.
  5. Создайте Mage(Character) с mana, и методом cast_spell().

Задание 2. Полиморфизм в действии

Цель - использовать полиморфизм.

Шаги:

  1. Возьмите классы из задания 1.
  2. Создайте функцию battle(char1, char2): каждый наносит урон другому.
  3. Добавьте метод attack(self, target) в Character, который вызывает target.take_damage(random.randint(10,20)).
  4. Переопределите attack в Mage: тратит ману и наносит больше урона.
  5. Создайте список персонажей и симулируйте бой.