Чистыйкод дляпродолжающи х
Скачать 7.85 Mb.
|
Функция type() и атрибут __qualname__ Передав объект встроенной функции type() , вы узнаете тип данных объекта по воз- вращаемому значению этой функции. Объекты, возвращаемые функцией type() , называются объектами типов (также встречается термин «объекты классов»). Вспомните, что термины «тип», «тип данных» и «класс» в Python обозначают одно и то же. Чтобы увидеть, что возвращает функция type() для разных значений, введите следующий фрагмент в интерактивной оболочке: >>> type(42) # Объект 42 имеет тип int. >>> int # int - объект типа для целого типа данных. >>> type(42) == int # Проверка типа: является ли 42 целым числом? True >>> type('Hello') == int # Проверка типа: имеет ли 'Hello' тип int? False >>> import wizcoin Примеры программирования с применением ООП и без него: «Крестики-нолики» 327 >>> type(42) == wizcoin.WizCoin # Проверка типа: имеет ли 42 тип WizCoin? False >>> purse = wizcoin.WizCoin(2, 5, 10) >>> type(purse) == wizcoin.WizCoin # Проверка типа: имеет ли purse тип WizCoin? True Обратите внимание: int является объектом типа и этот же объект возвращается вызовом type(42) , но он также может вызываться как функция-конструктор int() : функция int('42') не преобразует строковый аргумент '42' . Вместо этого она воз- вращает объект целого числа, соответствующий аргументу. Допустим, вы хотите сохранить некоторую информацию о переменных в вашей программе, которая позднее пригодится при отладке. В файл журнала можно запи- сывать только строковые данные, но при передаче объекта типа str() будет возвра- щена непонятная строка. Вместо этого следует использовать атрибут __qualname__ , который имеется у всех объектов типов, для сохранения в журнале более простой и удобочитаемой строки: >>> str(type(42)) # При передаче объекта типа str() возвращает непонятную строку. " >>> type(42).__qualname__ # Атрибут __qualname__ предоставляет более понятную информацию. 'int' Атрибут __qualname__ чаще всего используется для переопределения метода __repr__() , более подробно рассматриваемого в главе 17. Примеры программирования с применением ООП и без него: «Крестики-нолики» На первый взгляд, трудно понять, как использовать классы в программах. Рас- смотрим пример короткой программы для игры «Крестики-нолики», которая не использует классы, а потом перепишем ее с классами. Откройте в редакторе окно с новым файлом, введите следующую программу и со- храните ее с именем tictactoe.py : # tictactoe.py, реализация без ООП. ALL_SPACES = list('123456789') # Ключи для словаря с игровым полем. X, O, BLANK = 'X', 'O', ' ' # Константы для строковых значений. def main(): """Проводит игру в крестики-нолики.""" print('Welcome to tic-tac-toe!') gameBoard = getBlankBoard() # Создать словарь с игровым полем. currentPlayer, nextPlayer = X, O # X ходит первым, O ходит вторым. 328 Глава 15.Объектно-ориентированное программирование и классы while True: print(getBoardStr(gameBoard)) # Вывести игровое поле на экран. # Запрашивать ход, пока игрок не введет число от 1 до 9: move = None while not isValidSpace(gameBoard, move): print(f'What is {currentPlayer}\'s move? (1-9)') move = input() updateBoard(gameBoard, move, currentPlayer) # Сделать ход. # Проверить окончание игры: if isWinner(gameBoard, currentPlayer): # Сначала проверяем победу. print(getBoardStr(gameBoard)) print(currentPlayer + ' has won the game!') break elif isBoardFull(gameBoard): # Затем проверяется ничья. print(getBoardStr(gameBoard)) print('The game is a tie!') break currentPlayer, nextPlayer = nextPlayer, currentPlayer # Передать ход. print('Thanks for playing!') def getBlankBoard(): """Создает пустое игровое поле для игры крестики-нолики.""" board = {} # Поле представляется словарем Python. for space in ALL_SPACES: board[space] = BLANK # Все поля в исходном состоянии пусты. return board def getBoardStr(board): """Возвращает текстовое представление игрового поля.""" return f''' {board['1']}|{board['2']}|{board['3']} 1 2 3 -+-+- {board['4']}|{board['5']}|{board['6']} 4 5 6 -+-+- {board['7']}|{board['8']}|{board['9']} 7 8 9''' def isValidSpace(board, space): """Возвращает True, если задан допустимый номер клетки, и эта клетка пуста.""" return space in ALL_SPACES or board[space] == BLANK def isWinner(board, player): """Возвращает True, если игрок победил на заданном поле.""" b, p = board, player # Более короткие имена для удобства. # Проверяем 3 знака по 3 строкам, 3 столбцам и 2 диагоналям. return ((b['1'] == b['2'] == b['3'] == p) or # Верхняя строка (b['4'] == b['5'] == b['6'] == p) or # Средняя строка (b['7'] == b['8'] == b['9'] == p) or # Нижняя строка (b['1'] == b['4'] == b['7'] == p) or # Левый столбец Примеры программирования с применением ООП и без него: «Крестики-нолики» 329 (b['2'] == b['5'] == b['8'] == p) or # Средний столбец (b['3'] == b['6'] == b['9'] == p) or # Правый столбец (b['3'] == b['5'] == b['7'] == p) or # Диагональ (b['1'] == b['5'] == b['9'] == p)) # Диагональ def isBoardFull(board): """Возвращает True, если заняты все клетки игрового поля.""" for space in ALL_SPACES: if board[space] == BLANK: return False # Если есть хотя бы одна пустая клетка, вернуть False. return True # Пустых клеток не осталось, вернуть True. def updateBoard(board, space, mark): """Заполняет клетку игрового поля знаком mark.""" board[space] = mark if __name__ == '__main__': main() # Выполняет main(), если модуль был запущен (а не импортирован). При запуске программы вывод выглядит примерно так: Welcome to tic-tac-toe! | | 1 2 3 -+-+- | | 4 5 6 -+-+- | | 7 8 9 What is X's move? (1-9) 1 X| | 1 2 3 -+-+- | | 4 5 6 -+-+- | | 7 8 9 What is O's move? (1-9) --snip-- X| |O 1 2 3 -+-+- |O| 4 5 6 -+-+- X|O|X 7 8 9 What is X's move? (1-9) 4 X| |O 1 2 3 -+-+- X|O| 4 5 6 -+-+- X|O|X 7 8 9 X has won the game! Thanks for playing! 330 Глава 15.Объектно-ориентированное программирование и классы Для представления девяти клеток игрового поля в программе используется объект словаря. Ключами словаря являются строки '1' – '9' , а значениями — строки 'X' , 'O' и ' ' . Нумерация клеток соответствует расположению цифр на клавиатуре телефона. Функции в программе tictactoe.py делают следующее. Функция main() содержит код, который создает новую структуру данных игрового поля (хранящуюся в переменной gameBoard ) и вызывает другие функции программы. Функция getBlankBoard() возвращает словарь со значениями, инициализи- рованными ' ' (пустое поле). Функция getBoardStr() получает словарь, представляющий игровое поле, и возвращает представление игрового поля в виде многострочного текста, которое может быть выведено на экран. Именно эта функция формирует текст игрового поля, выводимый игрой. Функция isValidSpace() возвращает True , если ей передан допустимый номер клетки и эта клетка пуста. В параметрах функция isWinner() получает словарь игрового поля и символ 'X' или 'O' . Она определяет, поставил ли конкретный игрок три знака в ряд на поле. Функция isBoardFull() проверяет, что на поле не осталось пустых клеток; это означает, что игра закончилась. Функция updateBoard() получает в пара- метрах словарь, пробел и обозначение игрока (X или O) и обновляет словарь. Обратите внимание: многие функции получают в первом параметре переменную board . Это указывает на то, что эти функции связаны друг с другом в том смысле, что все они работают с одной структурой данных. Когда несколько функций в коде работают с одной структурой данных, обычно лучше сгруппировать их как методы и атрибуты класса. Переработаем программу tictactoe.py , чтобы в ней использовался класс TTTBoard . В атрибуте spaces этого класса будет храниться словарь board . Функции, получающие board в параметре, станут методами класса TTTBoard , а вместо параметра board они будут использовать параметр self Откройте в редакторе окно с новым файлом, введите следующую программу и со- храните ее с именем tictactoe_oop.py : # tictactoe_oop.py, объектно-ориентированная реализация игры. ALL_SPACES = list('123456789') # Ключи для словаря с игровым полем. X, O, BLANK = 'X', 'O', ' ' # Константы для строковых значений. Примеры программирования с применением ООП и без него: «Крестики-нолики» 331 def main(): """Проводит игру в крестики-нолики.""" print('Welcome to tic-tac-toe!') gameBoard = TTTBoard() # Создать объект игрового поля. currentPlayer, nextPlayer = X, O # X ходит первым, O ходит вторым. while True: print(gameBoard.getBoardStr()) # Вывести игровое поле на экран. # Запрашивать ход, пока игрок не введет число от 1 до 9: move = None while not gameBoard.isValidSpace(move): print(f'What is {currentPlayer}\'s move? (1-9)') move = input() gameBoard.updateBoard(move, currentPlayer) # Сделать ход. # Проверить окончание игры: if gameBoard.isWinner(currentPlayer): # Сначала проверяем победу. print(gameBoard.getBoardStr()) print(currentPlayer + ' has won the game!') break elif gameBoard.isBoardFull(): # Затем проверяется ничья. print(gameBoard.getBoardStr()) print('The game is a tie!') break currentPlayer, nextPlayer = nextPlayer, currentPlayer # Передать ход. print('Thanks for playing!') class TTTBoard: def __init__(self, usePrettyBoard=False, useLogging=False): """Создает пустое игровое поле для игры крестики-нолики.""" self._spaces = {} # Поле представляется словарем Python. for space in ALL_SPACES: self._spaces[space] = BLANK # Все поля в исходном состоянии пусты. def getBoardStr(self): """Возвращает текстовое представление игрового поля.""" return f''' {self._spaces['1']}|{self._spaces['2']}|{self._spaces['3']} 1 2 3 -+-+- {self._spaces['4']}|{self._spaces['5']}|{self._spaces['6']} 4 5 6 -+-+- {self._spaces['7']}|{self._spaces['8']}|{self._spaces['9']} 7 8 9''' def isValidSpace(self, space): """Возвращает True, если задан допустимый номер клетки и эта клетка пуста.""" return space in ALL_SPACES and self._spaces[space] == BLANK def isWinner(self, player): """Возвращает True, если игрок победил на заданном поле.""" 332 Глава 15.Объектно-ориентированное программирование и классы s, p = self._spaces, player # Более короткие имена для удобства. # Проверяем 3 знака по 3 строкам, 3 столбцам и 2 диагоналям. return ((s['1'] == s['2'] == s['3'] == p) or # Верхняя строка (s['4'] == s['5'] == s['6'] == p) or # Средняя строка (s['7'] == s['8'] == s['9'] == p) or # Нижняя строка (s['1'] == s['4'] == s['7'] == p) or # Левый столбец (s['2'] == s['5'] == s['8'] == p) or # Средний столбец (s['3'] == s['6'] == s['9'] == p) or # Правый столбец (s['3'] == s['5'] == s['7'] == p) or # Диагональ (s['1'] == s['5'] == s['9'] == p)) # Диагональ def isBoardFull(self): """Возвращает True, если заняты все клетки игрового поля.""" for space in ALL_SPACES: if self._spaces[space] == BLANK: return False # Если есть хотя бы одна пустая клетка, вернуть False. return True # Пустых клеток не осталось, вернуть True. def updateBoard(self, space, player): """Заполняет клетку игрового поля знаком игрока.""" self._spaces[space] = player if __name__ == '__main__': main() # Выполняет main(), если модуль был запущен (а не импортирован). С точки зрения функциональности эта программа не отличается от реализации, не использующей ООП. Вывод выглядит идентично. Код, который ранее находился в getBlankBoard() , был перемещен в метод __init__() класса TTTBoard , потому что они выполняют одну задачу инициализации структуры данных игрового поля. Другие функции были преобразованы в методы, параметр self заменил старый параметр board , потому что они служат одной цели: это блоки кода, работающие со структурой данных игрового поля. Когда коду этих методов потребуется изменить словарь, хранящийся в атрибуте _spaces , он использует выражение self._spaces . Если код этих методов должен вызвать другие методы, перед этими вызовами также указывается имя self и точ- ка (подобно тому как при вызове метода coinJars.values() в разделе «Создание простого класса: WizCoin» переменная coinJars содержит объект). В этом примере объект, содержащий вызываемый метод, хранится в переменной self Также обратите внимание на то, что имя атрибута _spaces начинается с символа подчеркивания; это означает, что все обращения к нему или его изменение должны выполняться только из кода методов TTTBoard . Код за пределами класса должен изменять _spaces только косвенно — вызовом соответствующих методов. Полезно сравнить исходный код двух реализаций игры. Вы можете это сделать в книге или прочитать о параллельном сравнении двух версий на https://autbor. com/compareoop/. Трудности проектирования классов для проектов реального мира 333 «Крестики-нолики» — небольшая программа, и понять ее несложно. А если бы про- грамма состояла из десятков тысяч строк с сотнями разных функций? Программу с несколькими десятками классов проще понять, чем программу с сотнями никак не связанных функций. ООП разбивает сложную программу на более понятные фрагменты. Трудности проектирования классов для проектов реального мира Проектирование класса, как и проектирование бумажной или электронной фор- мы, — это обманчиво прямолинейное дело. Формы и классы по своей сути являются упрощенными представлениями реальных объектов. Вопрос в том, как именно упрощать объекты? Например, если вы создаете класс Customer , представляющий клиента, в него нужно включить атрибуты имени и фамилии firstName и lastName , не так ли? Однако в действительности создавать классы для моделирования реаль- ных объектов весьма непросто. В большинстве западных стран фамилия человека указывается после имени, но в Китае — до имени. Если вы не хотите терять более миллиарда потенциальных клиентов, как изменить класс Customer ? Стоит ли за- менить имена firstName и lastName на givenName и familyName ? Но в некоторых культурах фамилии у людей вообще нет. Например, у бывшего генерального се- кретаря ООН У Тана (он родом из Бирмы) фамилии нет: Тан — собственное имя, а У — начальный слог собственного имени его отца. Также нужно хранить возраст клиента, но значение атрибута age быстро устаревает; вместо этого лучше вычислять возраст каждый раз, когда он потребуется, по дате рождения в атрибуте birthdate Реальный мир сложен, как и проектирование форм и классов для отражения этой сложности в унифицированной структуре, с которой могут работать наши про- граммы. Форматы телефонных номеров также зависят от конкретной страны. ZIP- коды неприменимы к адресам за пределами Соединенных Штатов. Ограничение максимального количества символов в названиях городов может создать проблемы для немецкого городка Шмедесвуртервестердейч. В Австралии и Новой Зеландии X считается допустимым гендерным обозначением. Утконос — млекопитающее, которое несет яйца. Арахис — не орех. Хотдог может быть или не быть сэндвичем в зависимости от того, кого вы спросите. Вам как программисту, который пишет программы для реального мира, придется иметь дело со всеми этими сложностями. Если вы захотите больше узнать обо всем этом, я рекомендую доклад «Schemas for the Real World» Карины Зона (Carina C. Zona) на конференции PyCon 2015 (https://youtu.be/PYYfVqtcWQY/) и доклад «Hi! My name is…» Джеймса Бенне- та (James Bennett) на конференции North Bay Python 2018 (https://youtu.be/ NIebelIpdYk/). Также заслуживают внимания популярные публикации в блоге «Falsehoods Programmers Believe»; в них рассматриваются такие темы, как карты, 334 Глава 15.Объектно-ориентированное программирование и классы адреса электронной почты и другие виды данных, которые программисты часто представляют неправильно. Подборка ссылок на эти статьи доступна на https:// github.com/kdeldycke/awesome-falsehood/. Кроме того, удачный пример неудачного отражения сложности реального мира показан в видеоролике CGP Grey «Social Security Cards Explained» (https://youtu.be/Erp8IAUouus/). Итоги ООП — полезный механизм организации вашего кода. Классы позволяют группи- ровать данные и код в новые типы данных. Также на базе классов можно создавать объекты, вызывая их конструкторы (имя класса, вызываемое как функция), которые в свою очередь вызывают метод __init__() класса. Методы представляют собой функции, связанные с объектами, а атрибуты — переменные, связанные с объекта- ми. Все методы получают первый параметр self , которому присваивается текущий объект при вызове метода. Это позволяет методам присваивать значения атрибутам объекта и вызывать его методы. Хотя Python не позволяет задать приватный или открытый уровень доступа для атрибутов, в языке принято использовать префикс _ для любых методов и атри- бутов, которые должны вызываться или к которым следует обращаться из соб- ственных методов класса. Соблюдение этого соглашения поможет предотвратить некорректное использование класса и перевод его в недействительное состояние, которое может привести к ошибкам. Вызов type(obj) возвращает объект класса для типа obj . Объекты класса включают атрибут __qualname___ , который содержит строку с удобочитаемой формой имени класса. Возможно, к этому моменту у вас возник вопрос: зачем вообще нужны хлопоты с классами, атрибутами или методами, когда все то же доступно с помощью функ- ций? ООП — полезный механизм организации кода в нечто большее, чем обычный файл .py с сотней функций. Разбивая программу на несколько хорошо спроекти- рованных классов, вы можете сосредоточиться на каждом классе по отдельности. Методология ООП ориентирована на структуры данных и методы работы с этими структурами данных. Эта методология не является обязательной для всех программ, и, безусловно, злоупотребления ООП тоже возможны. Однако ООП позволяет ис- пользовать некоторые нетривиальные механизмы, о которых мы поговорим в следу- ющих двух главах. Глава 16 посвящена первому из этих механизмов — наследованию. |