Чаще всего с объектно-ориентированным подходом ассоциируется такое свойство языка программирования, как наследование. Наследование — это способ определения нового класса, являющегося измененной версией уже существующего класса.
Наследование позволяет добавлять методы к классу без изменения существующего класса. Новый класс наследует методы существующего класса. Развивая метафору, существующий класс называют родительским классом, или суперклассом. А новый класс называют дочерним классом, или подклассом родительского класса.
В этой главе мы продемонстрируем использование наследования при написании программы для карточной игры Старая дева. Одной из наших целей будет создание кода, который можно повторно использовать для написания других карточных игр.
Для написания карточной игры нужно уметь представлять руку с картами, то есть, набор карт, находящихся в руке одного игрока. Карточная рука похожа на колоду. Обе составлены из множества карт, и обе нуждаются в операциях по добавлению и удалению карт. Также будет не лишней возможность перетасовать колоду или руку.
Однако карточная рука отличается от колоды. В зависимости от игры, в которую играют, может понадобиться выполнять для руки некоторые операции, не имеющие смысла для колоды. Например, при игре в покер может понадобиться классифицировать руку (стрейт, флэш, и так далее) или сравнить одну руку с другой. А в бридже понадобится подсчитать стоимость руки, чтобы правильно вести торговлю.
Такая ситуация подталкивает нас к использованию наследования. Если класс Hand (англ.: рука) будет подклассом Deck, то он будет иметь все методы класса Deck, плюс новые методы, которые мы определим специально для него.
В определении класса имя родительского класса помещают в скобки:
class Hand(Deck):
pass
Это предложение означает, что новый класс Hand наследует от существующего класса Deck.
Конструктор Hand инициализирует атрибуты руки, name и cards:
class Hand(Deck):
def __init__(self, name=""):
self.cards = []
self.name = name
Строка name содержит имя руки, данное, возможно, по имени игрока. Параметр name опционален и имеет значение по умолчанию, пустую строку. Список карт в руке, cards, инициализируется пустым списком.
Практически в каждой карточной игре требуется добавлять и убирать карты из руки. С удалением у нас все в порядке, поскольку класс Hand унаследовал метод remove класса Deck. Но нужно написать метод add (англ.: добавить) для добавления карты в руку:
class Hand(Deck):
...
def add(self,card):
self.cards.append(card)
И вновь многоточие показывает, что мы опустили другие методы. Списочный метод append добавляет новую карту в конец списка карт.
Теперь, когда у нас есть класс Hand, нам нужно сдать карты из колоды в руки игроков. На первый взгляд неочевидно, должен ли новый метод быть помещен в класс Hand или класс Deck. Но поскольку этот метод оперирует с одной колодой и, вероятно, с несколькими руками, будет более естественным поместить его в Deck.
Метод deal должен быть достаточно общим, так как в разных карточных играх разные требования. Может понадобиться сразу сдать всю колоду, или добавлять по одной карте в каждую руку.
Метод deal имеет два параметра, список (или кортеж) рук и общее количество карт, которые необходимо сдать. Если карт в колоде недостаточно, метод сдает все имеющиеся, и на этом завершает свою работу:
class Deck :
...
def deal(self, hands, num_cards=999):
num_hands = len(hands)
for i in range(num_cards):
if self.is_empty(): break # break if out of cards
card = self.pop() # take the top card
hand = hands[i % num_hands] # whose turn is next?
hand.add(card) # add the card to the hand
Параметр num_cards опционален, а его значение по умолчанию, большое число, гарантирует, что будут сданы все карты из колоды.
Переменная цикла i поочередно приобретает значения от 0 до num_cards-1. В каждой итерации одна карта извлекается из колоды методом pop, который удаляет и возвращает последний элемент списка.
Оператор взятия остатка от деления % позволяет нам сдавать карты по кругу, по одной карте в руку за один раз. Когда i становится равным количеству рук, выражение i % num_hands дает 0, и счет рук начинается сначала.
Для вывода на печать карточной руки можно воспользоваться методами print_deck и __str__, унаследованными от класса Deck. Например:
>>> deck = Deck()
>>> deck.shuffle()
>>> hand = Hand("frank")
>>> deck.deal([hand], 5)
>>> print hand
Hand frank contains
2 of Spades
3 of Spades
4 of Spades
Ace of Hearts
9 of Clubs
Не очень хорошая рука, но есть потенциал для стрейт флэша.
Хотя пользоваться унаследованным методом довольно удобно, у объектов класса Hand есть кое-что еще, что хотелось бы вывести на печать. Это имя руки. Чтобы вывести его на печать, напишем свой метод __str__ для класса Hand, который переопределит одноименный метод родительского класса:
class Hand(Deck)
...
def __str__(self):
s = "Hand " + self.name
if self.is_empty():
s += " is empty\n"
else:
s += " contains\n"
return s + Deck.__str__(self)
Вначале переменной s присваивается строка с именем руки. Если рука пуста, то программа добавляет к строке слова is empty (англ.: пусто) и возвращает s.
В противном случае, программа добавляет к строке слово contains (англ.: содержит) и строковое представление Deck, полученное путем вызова метода __str__ класса Deck для объекта self.
Возможно, вам кажется странным передача self, ссылки на текущий объект Hand, в метод класса Deck. Но вспомните, что Hand является подклассом и разновидностю Deck, иными словами, Hand есть Deck. Объекты Hand могут делать все, что могут делать объекты Deck, поэтому мы вправе передать объект Hand методу Deck.
Вообще, всегда можно использовать экземпляр подкласса вместо экземпляра родительского класса.
Класс CardGame (англ.: карточная игра) берет на себя действия, общие для всех карточных игр, такие, как создание и тасование колоды:
class CardGame:
def __init__(self):
self.deck = Deck()
self.deck.shuffle()
Это первый случай, когда мы видим метод __init__, не просто инициализирующий атрибуты, а выполняющий нетривиальные действия.
Чтобы реализовать конкретную карточную игру, можно унаследовать новый класс от CardGame и добавить операции, необходимые для реализуемой игры. В качестве примера, напишем симуляцию игры Старая дева.
Цель игры — избавиться от всех карт в руке. Вы делаете это, сопоставляя карты по значению и цвету одновременно. Например, четверка треф соответствует четверке пик, поскольку обе масти черные. Валет червей соответствует валету бубен, так как масти обеих карт красные.
В начале игры дама треф убирается из колоды, и, следовательно, дама пик не имеет пары. Пятьдесят одна оставшаяся карта сдается игрокам по кругу. После сдачи все игроки сбрасывают имеющиеся у каждого в руке парные карты.
Когда в руках игроков пар больше нет, начинается игра. Игроки по очереди берут карту (рубашкой вверх) из руки ближайшего соседа слева, у которого есть карты, и добавляют в свою руку. Если пришедшая карта образует пару с имеющейся в руке, пара сбрасывается. В противном случае карта остается у игрока. В конце концов все возможные пары сбрасываются, и только пиковая дама (старая дева) остается в руке проигравшего.
В нашей компьютерной симуляции компьютер играет за всех игроков. К сожалению, некоторые нюансы настоящей игры теряются. В настоящей игре игрок, у которого в руке старая дева, пускается на маленькие хитрости, чтобы его сосед вытянул именно эту карту, например, кладет ее ближе других к соседу, или, напротив, максимально скрывает другими своими картами. Компьютер же выбирает карту случайным образом.
Рука для Старой девы должна уметь кое-что, помимо стандартных возможностей класса Hand. Определим новый класс, OldMaidHand (англ.: рука старой девы), подкласс класса Hand, и добавим метод remove_matches (англ.: удалить пары):
class OldMaidHand(Hand):
def remove_matches(self):
count = 0
original_cards = self.cards[:]
for card in original_cards:
match = Card(3 - card.suit, card.rank)
if match in self.cards:
self.cards.remove(card)
self.cards.remove(match)
print "Hand %s: %s matches %s" % (self.name, card, match)
count += 1
return count
Вначале мы создаем копию списка карт, чтобы пройти по всем элементам списка-копии, при необходимости удаляя элементы из оригинального списка. Поскольку self.cards изменяется в теле цикла, не стоит использовать этот список с переменной цикла. Вы озадачите Python, если заставите его в цикле идти по списку, который изменяется.
Для каждой карты в руке мы определяем парную ей карту и пробуем найти ее в списке. Парная карта имеет ту же величину и другую масть того же цвета, что и текущая карта. Выражение 3 - card.suit превращает трефы (масть 0) в пики (масть 3), а бубны (1) в черви (2). Проверьте сами, что другие преобразования тоже работают. Если парная карта находится в руке, обе карты удаляются.
Следующий пример демонстрирует использование remove_matches:
>>> game = CardGame()
>>> hand = OldMaidHand("frank")
>>> game.deck.deal([hand], 13)
>>> print hand
Hand frank contains
Ace of Spades
2 of Diamonds
7 of Spades
8 of Clubs
6 of Hearts
8 of Spades
7 of Clubs
Queen of Clubs
7 of Diamonds
5 of Clubs
Jack of Diamonds
10 of Diamonds
10 of Hearts
>>> hand.remove_matches()
Hand frank: 7 of Spades matches 7 of Clubs
Hand frank: 8 of Spades matches 8 of Clubs
Hand frank: 10 of Diamonds matches 10 of Hearts
>>> print hand
Hand frank contains
Ace of Spades
2 of Diamonds
6 of Hearts
Queen of Clubs
7 of Diamonds
5 of Clubs
Jack of Diamonds
Обратите внимание, что в классе OldMaidHand нет метода __init__. Он унаследован от Hand.
А теперь займемся собственно игрой. Класс OldMaidGame, подкласс класса CardGame, получит новый метод play, который принимает список игроков в качестве параметра.
Поскольку метод __init__ наследуется от CardGame, новый объект OldMaidGame будет содержать перетасованную колоду:
class OldMaidGame(CardGame):
def play(self, names):
# remove Queen of Clubs
self.deck.remove(Card(0,12))
# make a hand for each player
self.hands = []
for name in names:
self.hands.append(OldMaidHand(name))
# deal the cards
self.deck.deal(self.hands)
print "---------- Cards have been dealt"
self.print_hands()
# remove initial matches
matches = self.remove_all_matches()
print "---------- Matches discarded, play begins"
self.print_hands()
# play until all 50 cards are matched
turn = 0
num_hands = len(self.hands)
while matches < 25:
matches += self.play_one_turn(turn)
turn = (turn + 1) % num_hands
print "---------- Game is Over"
self.print_hands()
Напишите метод print_hands() самостоятельно в качестве упражнения.
Некоторые части игры выделены в отдельные методы. Метод remove_all_matches обходит все руки и вызывает remove_matches для каждой:
class OldMaidGame(CardGame):
...
def remove_all_matches(self):
count = 0
for hand in self.hands:
count += hand.remove_matches()
return count
Переменная count — аккумулятор, который накапливает общее количество пар, сброшенных со всех рук.
Когда общее количество сброшенных пар достигнет двадцати пяти, игроками будут сброшены пятьдесят карт. Это означает, что осталась одна карта и игра завершена.
Переменная turn (англ.: очередность) следит за тем, какая рука играет. Вначале установленное в 0, значение переменной в каждой итерации увеличивается на 1. Когда значение достигает num_hands, операция взятия остатка от деления вновь устанавливает его в 0.
Метод play_one_turn принимает параметр, показывающий, чья очередь играть, и возвращает количество совпадений (или сброшенных пар) в сыгравшей руке:
class OldMaidGame(CardGame):
...
def play_one_turn(self, i):
if self.hands[i].is_empty():
return 0
neighbor = self.find_neighbor(i)
picked_card = self.hands[neighbor].pop_card()
self.hands[i].add(picked_card)
print "Hand", self.hands[i].name, "picked", picked_card
count = self.hands[i].remove_matches()
self.hands[i].shuffle()
return count
Если рука игрока пуста, то игрок находится вне игры; в этом случае метод ничего не делает и возвращает 0.
В противном случае, метод находит ближайшего игрока слева, у которого есть карты, берет одну карту из его руки, добавляет ее в свою руку и проверяет свою руку на наличие пар. Перед завершением метода карты в руке тасуются, чтобы сделать выбор следующего игрока случайным.
Метод find_neighbor (англ.: найти соседа) начинает с ближайшего соседа слева, и переходит к следующему до тех пор, пока не будет найден игрок с картами в руке:
class OldMaidGame(CardGame):
...
def find_neighbor(self, i):
num_hands = len(self.hands)
for next in range(1,num_hands):
neighbor = (i + next) % num_hands
if not self.hands[neighbor].is_empty():
return neighbor
Если find_neighbor когда-либо обойдет весь круг, не найдя карт в руках игроков, он вернет None и приведет к ошибке в вызывающей программе. Однако, мы можем доказать, что этого никогда не случится (во всяком случае, пока программа корректно обнаруживает завершение игры).
Ниже приведены сообщения, выведенные в ходе симуляции сокращенной версии игры, в которой трем игрокам были сданы только 15 карт, от десятки и старше. С такой колодой игра завершается после сброса семи пар, а не двадцати пяти:
>>> import cards
>>> game = cards.OldMaidGame()
>>> game.play(["Allen","Jeff","Chris"])
---------- Cards have been dealt
Hand Allen contains
King of Hearts
Jack of Clubs
Queen of Spades
King of Spades
10 of Diamonds
Hand Jeff contains
Queen of Hearts
Jack of Spades
Jack of Hearts
King of Diamonds
Queen of Diamonds
Hand Chris contains
Jack of Diamonds
King of Clubs
10 of Spades
10 of Hearts
10 of Clubs
Hand Jeff: Queen of Hearts matches Queen of Diamonds
Hand Chris: 10 of Spades matches 10 of Clubs
---------- Matches discarded, play begins
Hand Allen contains
King of Hearts
Jack of Clubs
Queen of Spades
King of Spades
10 of Diamonds
Hand Jeff contains
Jack of Spades
Jack of Hearts
King of Diamonds
Hand Chris contains
Jack of Diamonds
King of Clubs
10 of Hearts
Hand Allen picked King of Diamonds
Hand Allen: King of Hearts matches King of Diamonds
Hand Jeff picked 10 of Hearts
Hand Chris picked Jack of Clubs
Hand Allen picked Jack of Hearts
Hand Jeff picked Jack of Diamonds
Hand Chris picked Queen of Spades
Hand Allen picked Jack of Diamonds
Hand Allen: Jack of Hearts matches Jack of Diamonds
Hand Jeff picked King of Clubs
Hand Chris picked King of Spades
Hand Allen picked 10 of Hearts
Hand Allen: 10 of Diamonds matches 10 of Hearts
Hand Jeff picked Queen of Spades
Hand Chris picked Jack of Spades
Hand Chris: Jack of Clubs matches Jack of Spades
Hand Jeff picked King of Spades
Hand Jeff: King of Clubs matches King of Spades
---------- Game is Over
Hand Allen is empty
Hand Jeff contains
Queen of Spades
Hand Chris is empty
Джефф проиграл.
Наследование — очень мощное и выразительное средство. Некоторые программы, не будь наследования, были бы чрезвычайно сложны, а использование наследования позволяет сделать их краткими и понятными. Кроме того, наследование способствует повторному использованию кода, поскольку подклассы используют уже написанный код родительских классов, и позволяют изменять их поведение, не изменяя сами родительские классы. А в некоторых случаях структура наследования повторяет структуру, присущую решаемой задаче, что делает работу над программой легче, а саму программу понятнее.
С другой стороны, наследование может и затруднить понимание программы. При вызове метода не всегда ясно, где искать определение метода. Код родительских и дочерних классов может быть разбросан по нескольким модулям. Кроме того, многие вещи, которые можно сделать при помощи наследования, можно сделать и не прибегая к нему. Если задача естественным образом не решается с помощью наследования, то его использование может принести больше вреда, чем пользы.