Лаб. 06: Объектно-ориентированное программирование

Объектно-ориентированное программирование

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

Пример ООП: класс Car

Представь, что тебе нужно создать автомобиль для твоего любимого преподавателя. Не переживай, работать руками не придётся — конструктор это сделает за тебя. Конструктор класса — функция, которая создаёт экземпляр объекта, описываемого классом. В Python конструктором является метод __init__. Заметь, у него по два нижних подчёркивания с каждой стороны. Конструктор класса Car выглядит так:

def __init__(self, make, model):
    self.make = make
    self.model = model
    self.color = 'Без цвета. Покрась меня.'
    self.wheels = Car.num_wheels
    self.gas = Car.gas

Метод __init__ класса Car принимает три параметра. Первый из них, self, автоматически связывается с создаваемым экземпляром класса Car. Второй make и третий model параметры связываются с аргументами, передаваемыми в вызов конструктора. Это означает, что при создании объекта класса Car достаточно указать два аргумента. Пока что сильно не обдумывай код внутри тела конструктора.

Пора делать машину. Твой любимый препод желает приезжать на лекции исключительно на Tesla Model S. То есть нужно вызвать конструктор с параметрами make = 'Tesla' и model = 'Model S'. Делать вызов непосредственно функции __init__ не нужно, в Python можно использовать имя класса для создания экземпляра.

>>> apalkoff_car = Car('Tesla', 'Model S')

Заметь, что здесь опущен аргумент self, поскольку его значение — это собственно создаваемый экземпляр. Объект — это экземпляр класса. В данном случае имя apalkoff_car теперь связано с объектом Car, или, иными словами, с экземпляром класса Car.

Атрибуты

Теперь задумайся о том, где же хранятся значения make и model в созданной машине? Тут нужно поговорить про атрибуты экземпляров и классов. Вот фрагмент кода из car.py с атрибутами уровня класса и уровня экземпляра:

class Car(object):
    num_wheels = 4        # количество колёс
    gas = 30              # количество бензина
    headlights = 2        # количество фар
    size = 'Малышка'      # размер кузова

    def __init__(self, make, model):
        self.make = make
        self.model = model
        self.color = 'Без цвета. Покрась меня.'
        self.wheels = Car.num_wheels
        self.gas = Car.gas

    def paint(self, color):
        self.color = color
        return 'Цвет ' + self.make + ' ' + self.model + ' теперь ' + color

    def drive(self):
        if self.wheels < Car.num_wheels or self.gas <= 0:
            return self.make + ' ' + self.model + ' не может ехать!'
        self.gas -= 10
        return self.make + ' ' + self.model + ' делает врум-врум!'

    def pop_tire(self):
        if self.wheels > 0:
            self.wheels -= 1

    def fill_gas(self):
        self.gas += 20
        return 'Количество бензина в '+self.make + ' ' + self.model + ': ' + str(self.gas)

В первых двух строках конструктора имена self.make и self.model связываются с первым и вторым аргументами вызывающего конструктор выражения. Это примеры атрибутов экземпляра. Такие атрибуты относятся непосредственно к объекту, а не ко всем объектам класса. Доступ к атрибутам экземпляра осуществляется с помощью точечной нотации (точка отделяет имя экземпляра от имени атрибута). В данном случае имя self связано с экземпляром, то есть self.model означает взять атрибут model экземпляра self.

У машины есть и другие атрибуты уровня экземпляра: цвет color, колёса wheels. Изменение атрибутов экземпляра не влияет на другие экземпляры этого класса.

С другой стороны, существуют атрибуты класса. Они являются общими для всех экземпляров класса. Например, класс Car имеет четыре атрибута, заданные сразу после заголовка класса: num_wheels = 4, gas = 30, headlights = 2 and size = 'Малышка'.

Можно заметить, что в методе __init__ класса Car атрибут экземпляра gas инициализируется значением атрибута класса Car.gas. Почему бы просто не использовать атрибут класса? Причина в том, что в каждом экземпляре (то есть в каждой отдельной машине) уровень бензина должен быть свой. Если какая-то машина потратит сколько-то бензина, это должно отразиться в атрибуте gas. Если использовать атрибут класса, то количество оставшегося бензина будет синхронно меняться у всех существующих машин, и это не то, что надо.

Точечная нотация

Для доступа к атрибутам класса используется точечная нотация с указанием либо имени экземпляра, либо имени класса. Например, до атрибута size можно доступиться так:

>>> apalkoff_car.size
'Малышка'
>>> Car.size
'Малышка'

До атрибутов экземпляра можно доступиться только через имя экземпляра:

>>> apalkoff_car.color
'Без цвета. Покрась меня.'

Похоже, пора покрасить машину apalkoff_car.

Методы

Используй-ка метод paint, чтобы покрасить машину. Методы — это функции, определённые в классе. Один метод уже знаком — __init__! О методах можно думать как о действиях, или возможностях объекта. Как же вызвать метод через экземпляр? Всё верно — точечная нотация!

>>> apalkoff_car.paint('чёрный')
'Цвет Tesla Model S теперь чёрный'
>>> apalkoff_car.color
'чёрный'

Огонь! Но если присмотреться к методу paint, можно заметить, что он принимает два параметра, а вызов был с одним! Точно так же, как и в случае с __init__, параметр self был автоматически связан экземпляром, через который был вызван этот метод. То есть self был связан со объектом apalkoff_car.

Эквивалентно можно вызывать методы через имя класса — в этом случае нужно явно указывать значение параметра self.

>>> Car.paint(apalkoff_car, 'красный')
'Цвет Tesla Model S теперь красный'

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

Красная Тесла любимого препода очень хороша, но он, как в сказке о золотой рыбке, захотел чего-то побольше! Нельзя ли для него создать монстр-трак? В файле car.py определён класс MonsterTruck, вот его код:

class MonsterTruck(Car):
    size = 'Монстр'

    def rev(self):
        print('ДРЫН-ДЫН-ДЫН! Этот монстр-трак просто огромный!')

    def drive(self):
        self.rev()
        return Car.drive(self)

Ого! Монстр-трак, может, и большой, а его код маленький! Проверь-ка, что этот автомобиль делает всё, что надо. Сперва создадим экземпляр монстр-трака:

>>> apalkoff_truck = MonsterTruck('Monster Truck', 'XXL')

Будет ли он вести себя как Car? Можно ли его покрасить? А поехать можно?

В заголовке класса MonsterTruck указано, что Car является суперклассом (надклассом) — MonsterTruck(Car). Также можно сказать, что MonsterTruck — это подкласс класса Car. Это означает, что класс MonsterTruck наследует все атрибуты и методы, определённые в Car, включая конструктор!

Наследование позволяет легко выстраивать иерархии классов. В подклассах не нужно создавать существующие в надклассах методы и атрибуты. Существующие атрибуты и методы можно дополнять (или переопределять).

>>> apalkoff_car.size
'Малышка'
>>> apalkoff_truck.size
'Монстр'

Вот это разница в размерах! Она возникла из-за того, что атрибут size класса MonsterTruck переопределяет (override) атрибут size класса Car. То есть все экземпляры MonsterTruck будут 'Монстр'-о-размерными.

Кстати, метод drive в классе MonsterTruck также переопределён. Чтобы выделить все монстр-траки, в описание класса добавлен метод rev. Обычные машины не могут в rev! Всё остальное — __init__, paint, num_wheels, gas — наследуется из Car.

Основная часть

Вопрос 1: Использование класса Car (ПСП)

Вот полное определение класса Car, находящееся в файле car.py:

class Car(object):
    num_wheels = 4
    gas = 30
    headlights = 2
    size = 'Малышка'

    def __init__(self, make, model):
        self.make = make
        self.model = model
        self.color = 'Без цвета. Покрась меня.'
        self.wheels = Car.num_wheels
        self.gas = Car.gas

    def paint(self, color):
        self.color = color
        return 'Цвет ' + self.make + ' ' + self.model + ' теперь ' + color

    def drive(self):
        if self.wheels < Car.num_wheels or self.gas <= 0:
            return self.make + ' ' + self.model + ' не может ехать!'
        self.gas -= 10
        return self.make + ' ' + self.model + ' делает врум-врум!'

    def pop_tire(self):
        if self.wheels > 0:
            self.wheels -= 1

    def fill_gas(self):
        self.gas += 20
        return 'Количество бензина в '+self.make + ' ' + self.model + ': ' + str(self.gas)

Теперь представляй себя пайтоном и заполняй пропуски в файле psp.py:

>>> apalkoff_car = Car('Tesla', 'Model S')
>>> apalkoff_car.color
______

>>> apalkoff_car.paint('чёрный')
______

>>> apalkoff_car.color
______
>>> apalkoff_car = Car('Tesla', 'Model S')
>>> apalkoff_car.model
______

>>> apalkoff_car.gas = 10
>>> apalkoff_car.drive()
______

>>> apalkoff_car.drive()
______

>>> apalkoff_car.fill_gas()
______

>>> apalkoff_car.gas
______

>>> Car.gas
______
>>> Car.headlights
______

>>> apalkoff_car.headlights
______

>>> Car.headlights = 3
>>> apalkoff_car.headlights
______

>>> apalkoff_car.headlights = 2
>>> Car.headlights
______
>>> apalkoff_car.wheels = 2
>>> apalkoff_car.wheels
______

>>> Car.num_wheels
______

>>> apalkoff_car.drive()
______

>>> Car.drive()
______

>>> Car.drive(apalkoff_car)
______

Далее будет использоваться класс MonsterTruck, его код также в файле car.py:

class MonsterTruck(Car):
    size = 'Монстр'

    def rev(self):
        print('ДРЫН-ДЫН-ДЫН! Этот монстр-трак просто огромный!')

    def drive(self):
        self.rev()
        return Car.drive(self)
>>> apalkoff_car = MonsterTruck('Монстр', 'Бэтмобиль')
>>> apalkoff_car.drive()
______

>>> Car.drive(apalkoff_car)
______

>>> MonsterTruck.drive(apalkoff_car)
______

>>> Car.rev(apalkoff_car)
______

Магический Лямбдинг

В следующей части лабораторной работы ты будешь создавать карточную игру!

Для её запуска введи в терминал:

python3 cardgame.py

Игра ещё не готова. Если запустить её сейчас — получишь ошибку, поскольку код не полон. Если игра запущена, то ее можно завершить сочетаниями клавиш Ctrl-C или Ctrl-D.

Программный код игры состоит из нескольких разных файлов:

  • Код, относящийся к вопросам лабораторной работы, находится в файле classes.py.

  • Вспомогательный код находится в файле cardgame.py. Тебе не нужно открывать или читать этот файл. Все взаимодействия с прочим кодом игры надёжно защищены границей абстракции.

  • Если потом тебе захочется модифицировать игру, создать свои карты и колоды, можешь посмотреть в cards.py — сейчас там стандартные карты и стандартная колода. Можешь изменять карты, как хочешь. Они не подчиняются каким-то внешним правилам, так что воплощай любую фантазию.

Правила игры. Эта игра немного сложна, хотя и не очень.

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

(сила атаки карты) - (сила защиты карты противника) / 2

Например, пусть игрок_1 играет карту со значениями 2000 АТАКА / 1000 ЗАЩИТА, а игрок_2 играет карту 1500 АТАКА / 3000 ЗАЩИТА. Мощь этих карт будет рассчитана так:

И1: 2000 - 3000/2 = 2000 - 1500 = 500
И2: 1500 - 1000/2 = 1500 - 500 = 1000

Игрок_2 побеждает в этом раунде.

Тот, кто победит в восьми раундах, побеждает в игре.

Кроме того, в игре могут появляться особенности (дополнительные вопросы), которые делают игру поинтереснее. Карты делятся на три вида: Студент-Мальчик, Студент-Девочка и Препод. Каждый из них обладает особенностями, которые учитываются перед вычислением мощи карт в раунде.

Студент-Мальчик заставляет противника сбросить три первые карты с руки и заменить их на новые. Студент-Девочка меняет местами силу атаки и силу защиты карты противника. Препод добавляет атаку и защиту карты противника ко всем картам в своей колоде, а затем сбрасывает из колоды противника все карты, у которых атака или защита равна соответствующим значениям у текущей карты противника.

Довольно много правил, так что возвращайся к этому месту, если надо будет освежить память. За дело!

Вопрос 2: Создание карт

Для игры нужны карты — надо их понаделать! Сначала займись основой класса Card.

Во-первых, ты знаешь, нужен конструктор класса Card (файл classes.py). Конструктор принимает три аргумента:

  • name — имя карты, строка;

  • attack — сила атаки, целое;

  • defense — сила защиты, целое.

Каждый экземпляр класса Card должен хранить эти значения в атрибутах name, attack, defense.

Также нужно создать метод power, который принимает карту противника и вычисляет мощь текущей карты. Если не помнишь правила, пролистай немного назад.

class Card(object):
    cardtype = '_'

    def __init__(self, name, attack, defense):
        """
        Создаёт карту с именем, силой атаки и силой защиты.
        >>> card_1 = Card('Гарри', 400, 300)
        >>> card_1.name
        'Гарри'
        >>> card_1.attack
        400
        >>> card_1.defense
        300
        >>> card_2 = Card('Гермиона', 300, 500)
        >>> card_2.attack
        300
        >>> card_2.defense
        500
        """
        "*** ТВОЙ КОД ЗДЕСЬ ***"

    def power(self, other_card):
        """
        Мощь вычисляется так:
        (сила атаки карты игрока) - (сила защиты карты противника)/2
        >>> card_1 = Card('Гарри', 400, 300)
        >>> card_2 = Card('Гермиона', 300, 500)
        >>> card_1.power(card_2)
        150.0
        >>> card_2.power(card_1)
        150.0
        >>> card_3 = Card('Рон', 200, 400)
        >>> card_1.power(card_3)
        200.0
        >>> card_3.power(card_1)
        50.0
        """
        "*** ТВОЙ КОД ЗДЕСЬ ***"

Вопрос 3: Создание игрока

Теперь, когда есть карты, несложно создать и колоду. Но кто же будет играть? Игроки! Дополни определение класса Player.

Экземпляр класса Player имеет три атрибута:

  • name — имя игрока;

  • deck — колода, экземпляр класса Deck, из неё можно вытаскивать случайные карты (метод draw).

  • hand — список экземпляров Card «в руке». Каждый игрок начинает с 5 карт, взятых из колоды. Карты в руке могут выбираться во время игры по индексу в соответствующем списке. Когда игрок берёт карту из колоды в руку, она добавляется в конец списка.

Дополни конструктор Player, чтобы значение self.hand было списком из 5 карт, взятых из колоды игрока deck.

После этого, заполни методы draw и play в классе Player. Метод draw должен забирать карту из колоды и добавлять её в руку игрока. Метод play убирает карту из руки по заданному индексу и возвращает её.

При реализации Player.__init__ и Player.draw вызывай метод deck.draw(). Не думай о том, как он работает — оставь это на совести абстракции.
class Player(object):
    def __init__(self, deck, name):
        """Создаёт игрока.
        Игрок начинает партию забирая 5 карт из своей колоды (deck). Каждый ход игрок
        забирает одну карту из колоды, и одну играет с руки (hand).
        >>> test_card = Card('тест', 100, 100)
        >>> test_deck = Deck([test_card.copy() for _ in range(6)])
        >>> test_player = Player(test_deck, 'тестер')
        >>> len(test_deck.cards)
        1
        >>> len(test_player.hand)
        5
        """
        self.deck = deck
        self.name = name
        "*** ТВОЙ КОД ЗДЕСЬ ***"

    def draw(self):
        """Забирает карту из колоды в руку игрока.
        >>> test_card = Card('тест', 100, 100)
        >>> test_deck = Deck([test_card.copy() for _ in range(6)])
        >>> test_player = Player(test_deck, 'тестер')
        >>> test_player.draw()
        >>> len(test_deck.cards)
        0
        >>> len(test_player.hand)
        6
        """
        assert not self.deck.is_empty(), 'Колода кончилась!'
        "*** ТВОЙ КОД ЗДЕСЬ ***"

    def play(self, card_index):
        """Играет карту с руки с заданным индексом — убирает из руки и возвращает.
        >>> from cards import *
        >>> test_player = Player(standard_deck, 'tester')
        >>> girl1, girl2 = GirlCard("girl_1", 300, 400), GirlCard("girl_2", 500, 600)
        >>> boy1, boy2 = BoyCard("boy_1", 200, 500), BoyCard("boy_2", 600, 400)
        >>> test_player.hand = [girl1, girl2, boy1, boy2]
        >>> test_player.play(0) is girl1
        True
        >>> test_player.play(2) is boy2
        True
        >>> len(test_player.hand)
        2
        """
        "*** ТВОЙ КОД ЗДЕСЬ ***"

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

python3 cardgame.py

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

Дополнительная часть

Решения вопросов из следующей части не требуют переписывания существующего кода. Вписывай код в то место, где находится фраза "* ТВОЙ КОД ЗДЕСЬ *". Также не забудь раскомментировать вызовы print для отображения игровой информации. Если этого не сделать, то доктесты не пройдут даже при правильном решении.

Вопрос 4: Мальчики

Чтобы игра стала интересной, нужно наделить карты разными дополнительными возможностями. Сделай это в методе effect, который принимает карту противника other_card, игрока player и противника opponent.

Напиши метод effect для класса BoyCard, который заставляет противника сбросить три первые карты с руки и вытянуть три новые карты из колоды. Считай, что и у противника в руке, и в его колоде есть по крайней мере по три карты.

class BoyCard(Card):
    cardtype = '♂'

    def effect(self, other_card, player, opponent):
        """
        Сбрасывает первые три карты из руки противника и меняет их на новые карты из колоды.
        >>> from cards import *
        >>> player1, player2 = Player(player_deck, 'p1'), Player(opponent_deck, 'p2')
        >>> other_card = Card('other', 500, 500)
        >>> boy_test = BoyCard('boy', 500, 500)
        >>> initial_deck_length = len(player2.deck.cards)
        >>> boy_test.effect(other_card, player1, player2)
        p2 сбрасывает с руки 3 карты и меняет их на новые!
        >>> len(player2.hand)
        5
        >>> len(player2.deck.cards) == initial_deck_length - 3
        True
        """
        "*** ТВОЙ КОД ЗДЕСЬ ***"
        #Раскомментируй строчку ниже, когда закончишь с этим методом.
        #print('{} сбрасывает с руки 3 карты и меняет их на новые!'.format(opponent.name))
Не забудь раскомментировать вызов print!

Вопрос 5: Девочки

Теперь добавь таким же образом дополнительные возможности для класса GirlCard. Путь эти карты меняют местами силу нападения и силу защиты карты противника.

class GirlCard(Card):
    cardtype = '♀'

    def effect(self, other_card, player, opponent):
        """
        Меняет атаку и защиту карты противника.
        >>> from cards import *
        >>> player1, player2 = Player(player_deck, 'p1'), Player(opponent_deck, 'p2')
        >>> other_card = Card('other', 300, 600)
        >>> girl_test = GirlCard('girl', 500, 500)
        >>> girl_test.effect(other_card, player1, player2)
        >>> other_card.attack
        600
        >>> other_card.defense
        300
        """
        "*** ТВОЙ КОД ЗДЕСЬ ***"

Вопрос 6: Препод

А вот и новое правило! Напиши такой же метод effect для класса TeacherCard, который добавляет силу нападения и силу защиты карты противника ко всем картам игрока, а затем удаляет из колоды противника все карты, у которых защита или атака равны соответствующим значениям карты противника.

У тебя будут проблемы, если ты будешь изменять список, по которому бежит цикл. Попробуй сделать копию, как-то так:

>>> lst = [1, 2, 3, 4]
>>> copy = lst[:]
>>> copy
[1, 2, 3, 4]
>>> copy is lst
False
class TeacherCard(Card):
    cardtype = '✪'

    def effect(self, other_card, player, opponent):
        """
        Добавляет атаку и защиту карты противника ко всем картам игрока.
        Затем удаляет из колоды противника все карты, у которых
        защита или атака равны соответствующим значениям карты противника.

        >>> test_card = Card('card', 300, 300)
        >>> teacher_test = TeacherCard('teacher', 500, 500)
        >>> opponent_card = test_card.copy()
        >>> test_deck = Deck([test_card.copy() for _ in range(8)])
        >>> player1, player2 = Player(test_deck.copy(), 'p1'), Player(test_deck.copy(), 'p2')
        >>> teacher_test.effect(opponent_card, player1, player2)
        Игрок p2 теряет карты из колоды! Пропало карт: 3
        >>> [(card.attack, card.defense) for card in player1.deck.cards]
        [(600, 600), (600, 600), (600, 600)]
        >>> len(player2.deck.cards)
        0
        """
        orig_opponent_deck_length = len(opponent.deck.cards)
        "*** ТВОЙ КОД ЗДЕСЬ ***"
        discarded = orig_opponent_deck_length - len(opponent.deck.cards)
        if discarded:
            #Раскомментируй строчку ниже, когда закончишь с этим методом.
            #print('Игрок {} теряет карты из колоды! Пропало карт: {}'.format(opponent.name, discarded))
            return

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