Главная страница
Навигация по странице:

  • Примеры программирования с применением ООП и без него: «Крестики-нолики»

  • Трудности проектирования классов для проектов реального мира

  • Чистыйкод дляпродолжающи х


    Скачать 7.85 Mb.
    НазваниеЧистыйкод дляпродолжающи х
    Дата13.05.2023
    Размер7.85 Mb.
    Формат файлаpdf
    Имя файлаPython_Chisty_kod_dlya_prodolzhayuschikh_2022_El_Sveygart.pdf
    ТипДокументы
    #1127485
    страница34 из 40
    1   ...   30   31   32   33   34   35   36   37   ...   40
    Функция 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 посвящена первому из этих механизмов — наследованию.

    1   ...   30   31   32   33   34   35   36   37   ...   40


    написать администратору сайта