ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 03.12.2023
Просмотров: 387
Скачиваний: 1
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
322
Глава 15.Объектно-ориентированное программирование и классы
Если вы получите сообщение об ошибке (например,
ModuleNotFoundError:
No module named
'wizcoin'
), убедитесь в том, что файлу присвоено имя wizcoin.py и он находится в одной папке с wcexample1.py
Объекты
WizCoin не имеют полезного строкового представления, поэтому при вы- воде purse и coinJar выводится адрес памяти в угловых скобках. (Из главы 17 вы узнаете, как изменить выводимое представление.)
Подобно тому как для объекта строки можно вызвать метод lower()
, мы можем вызвать методы value()
и weightInGrams()
для объектов
WizCoin
, присвоенных переменным purse и coinJar
. Эти методы вычисляют результат по значениям атрибутов galleons
, sickles и knuts объекта.
Классы и ООП упрощают сопровождение кода — то есть код проще читается, изменяет- ся и расширяется в будущем. Изучим методы и атрибуты этого класса более подробно.
Методы, __init__() и self
Методы представляют собой функции, связанные с объектами некоторого класса.
Вспомните, что lower()
является методом строк — это означает, что он вызывается для объектов строк. Метод lower()
можно вызвать для строки (например,
'Hello'.
lower()
), но вызвать его для списка (например,
['dog',
'cat'].lower()
) не полу- чится. Также обратите внимание на то, что метод указывается после имени объекта: правильным считается код 'Hello'.lower()
, а не lower('Hello')
. В отличие от мето- да lower()
функция len()
не связана с одним конкретным типом данных; при вызове len()
можно передавать строки, списки, словари и многие другие типы объектов.
Как было показано в предыдущем разделе, объект создается вызовом имени клас- са как функции. Эта функция называется функцией-конструктором (или просто
конструктором), потому что она конструирует новый объект. Также говорят, что конструктор создает новый экземпляр класса.
При вызове конструктора Python создает новый объект, а затем выполняет метод
__init__()
. Наличие метода
__init__()
в классе не обязательно, но он почти всегда присутствует. Именно в методе
__init__()
обычно задаются исходные значения атрибутов. Например, я снова приведу метод
__init__()
класса
WizCoin
:
def __init__(self, galleons, sickles, knuts):
"""Создание нового объекта WizCoin по значениям galleons, sickles и knuts."""
self.galleons = galleons self.sickles = sickles self.knuts = knuts
# ВНИМАНИЕ: методы __init__() НИКОГДА не содержат команду return.
Когда программа wcexample1.py вызывает
WizCoin(2,
5,
99)
, Python создает но- вый объект
WizCoin и передает три аргумента (
2
,
5
и
99
) вызову
__init__()
. Но
Создание простого класса: WizCoin
323
метод
__init__()
получает четыре параметра: self
, galleons
, sickles и knuts
. Дело в том, что у каждого метода имеется первый параметр с именем self
. Когда метод вызывается для объекта, этот объект автоматически передается в параметре self
Остальные аргументы присваиваются параметрам как обычно. Если вы увидите со- общение об ошибке вида
TypeError:
__init__()
takes
3
positional arguments but
4
were given
(TypeError: __init__() получает 3 позиционных аргумента, но задано 4), скорее всего, вы забыли добавить параметр self в команду def метода.
Присваивать первому параметру имя self необязательно; имя может быть любым.
Однако имя self считается общепринятым, и выбор иного имени затруднит чтение вашего кода другими программистами Python. Когда вы читаете код, первый пара- метр self помогает быстро отличить методы от функций. Аналогичным образом, если в коде метода нигде не используется параметр self
, это указывает на то, что, возможно, метод стоит оформить в виде функции.
Аргументы
2
,
5
и
99
в вызове
WizCoin(2,
5,
99)
не присваиваются атрибутам нового объекта автоматически; чтобы это произошло, необходимо включить три команды присваивания в
__init__()
. Часто параметрам
__init__()
присваиваются имена, со- впадающие с именами атрибутов, но наличие self в self.galleons означает, что это атрибут объекта, а galleons
— параметр. Сохранение аргументов конструктора в атри- бутах объекта — одна из типичных задач метода
__init__()
класса. Вызов datetime.
date()
в предыдущем разделе выполнял аналогичную операцию, хотя тогда пере- давались три аргумента для атрибутов year
, month и day создаваемого объекта date
Ранее мы вызвали функции int()
, str()
, float()
и bool()
для преобразования между типами данных — например, вызов str(3.1415)
возвращал строковое значение '3.1415'
для значения с плавающей точкой
3.1415
. Ранее в тексте они описывались как функции, но int
, str
, float и bool в действительности являются классами, а int()
, str()
, float()
и bool()
— конструкторами, которые возвращают новые объекты целого числа, строки, числа с плавающей точкой и логического зна- чения. Руководство по стилю Python рекомендует использовать для имен классов
«верблюжью» схему с первой буквой в верхнем регистре, хотя многие встроенные классы Python этому соглашению не следуют.
Вызов функции-конструктора
WizCoin()
возвращает новый объект
WizCoin
, но ме- тод
__init__()
не может содержать команды return с возвращаемым значением. При попытке добавить возвращаемое значение выдается ошибка
TypeError:
__init__()
should return
None
(TypeError: __init__() должен возвращать None).
Атрибуты
Атрибутами называются переменные, связанные с объектом. В документации Python атрибут описывается как «любое имя, следующее после точки». Вспомните выражение birthday.year из предыдущего раздела: атрибут year
— имя, следующее после точки.
324
Глава 15.Объектно-ориентированное программирование и классы
Каждый объект содержит собственный набор атрибутов. Когда программа wcexample1.py создает два объекта
WizCoin и сохраняет их в переменных purse и coinJar
, их атрибуты имеют разные значения. К ним можно обращаться и при- сваивать значения, как и к любой другой переменной. Чтобы потренироваться в присваивании значений атрибутов, откройте в редакторе окно с новым файлом и введите следующий код, сохранив его в файле wcexample2.py в одной папке с файлом wizcoin.py
:
import wizcoin change = wizcoin.WizCoin(9, 7, 20)
print(change.sickles) # Выводит 7.
change.sickles += 10
print(change.sickles) # Выводит 17.
pile = wizcoin.WizCoin(2, 3, 31)
print(pile.sickles) # Выводит 3.
pile.someNewAttribute = 'a new attr' # Создается новый атрибут.
print(pile.someNewAttribute)
При выполнении этой программы результат выглядит так:
7 17 3
a new attr
Атрибуты объекта также можно сравнить с ключами словаря. Вы можете читать и изменять связанные с ними значения, а также присваивать новые атрибуты объ- екту. Формально методы также считаются атрибутами класса.
Приватные атрибуты и приватные методы
В таких языках, как C++ или Java, атрибуты могут помечаться как имеющие при-
ватный уровень доступа. Это означает, что компилятор или интерпретатор позволит обращаться к атрибутам объектов этого класса только коду методов этого класса.
В языке Python такого ограничения не существует. Все атрибуты и методы факти- чески имеют открытый уровень доступа: код за пределами класса может обратиться к любым атрибутам любых объектов этого класса и изменять их.
Впрочем, приватный доступ полезен. Например, объекты класса
BankAccount могут содержать атрибут balance
, который должен быть доступен только для методов класса
BankAccount
. По этим причинам в Python принято начинать имена приват- ных атрибутов и методов с одного символа подчеркивания. Технически ничто не мешает коду за пределами класса обращаться к приватным атрибутам и методам, но на практике лучше обращаться к ним только из методов класса.
Создание простого класса: WizCoin
325
Откройте в редакторе окно с новым файлом, введите следующий код и сохраните его с именем privateExample.py
. В нем объекты класса
BankAccount содержат приват- ные атрибуты
_name и
_balance
, к которым должны обращаться напрямую только методы deposit()
и withdraw()
:
class BankAccount:
def __init__(self, accountHolder):
# Методы BankAccount могут обращаться к self._balance, но код
# за пределами класса этого делать не должен:
self._balance = 0
❶
self._name = accountHolder
❷
with open(self._name + 'Ledger.txt', 'w') as ledgerFile:
ledgerFile.write('Balance is 0\n')
def deposit(self, amount):
if amount <= 0:
❸
return # Отрицательные "зачисления" недопустимы.
self._balance += amount with open(self._name + 'Ledger.txt', 'a') as ledgerFile:
❹
ledgerFile.write('Deposit ' + str(amount) + '\n')
ledgerFile.write('Balance is ' + str(self._balance) + '\n')
def withdraw(self, amount):
if self._balance < amount or amount < 0:
❺
return # Не хватает средств на счете или снимается
# отрицательная сумма.
self._balance -= amount with open(self._name + 'Ledger.txt', 'a') as ledgerFile:
❻
ledgerFile.write('Withdraw ' + str(amount) + '\n')
ledgerFile.write('Balance is ' + str(self._balance) + '\n')
acct = BankAccount('Alice') # Создание учетного счета.
acct.deposit(120) # _balance можно изменять через deposit()
acct.withdraw(40) # _balance можно изменять через withdraw()
# Изменение _name или _balance за пределами BankAccount нежелательно, но возможно:
acct._balance = 1000000000
❼
acct.withdraw(1000)
acct._name = 'Bob' # Теперь изменяется счет Боба!
❽
acct.withdraw(1000) # Операция регистрируется в BobLedger.txt!
При выполнении программы privateExample.py создаваемые файлы содержат не- корректную информацию, потому что
_balance и
_name изменялись за пределами класса, что привело к недействительным состояниям. Файл
AliceLedger.txt содержит непонятно откуда взявшуюся огромную сумму:
Balance is 0
Deposit 120
Balance is 120
Withdraw 40
326
Глава 15.Объектно-ориентированное программирование и классы
Balance is 80
Withdraw 1000
Balance is 999999000
Файл
BobLedger.txt содержит необъяснимый баланс, хотя мы никогда не создавали объект
BankAccount для пользователя
Bob
:
Withdraw 1000
Balance is 999998000
Хорошо спроектированные классы в целом автономны, и они должны предоставлять методы для присваивания атрибутам допустимых значений. Атрибуты
_balance и
_name помечены как приватные
❶
и
❷
, а значение класса
BankAccount должно изме- няться только при помощи методов deposit()
и withdraw()
. Эти два метода содержат проверки
❸
и
❺
, которые проверяют, что атрибут
_balance не переводится в недействи- тельное состояние (например, ему не присваивается отрицательное целое значение).
Методы также регистрируют каждую операцию для текущего баланса
❹
и
❻
Код за пределами класса, изменяющий эти атрибуты (например, команды acct._
balance
=
1000000000
❼
или acct._name
=
'Bob'
❽
), может перевести объект в некор- ректное состояние и создать ошибки. Соблюдение соглашений об использовании префикса
_
для приватного доступа упрощает отладку. Вы точно знаете: ошибку нужно искать в коде класса, а не в коде всей программы.
В отличие от Java и других языков, Python не требует определения открытых get- и set-методов для приватных атрибутов. Вместо этого в Python используются свойства (см. главу 17).
1 ... 29 30 31 32 33 34 35 36 ... 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
16
Объектно-ориентированное
программирование
и наследование
Определение функции и вызов ее в нескольких местах про- граммы избавляет от копирования исходного кода. Отказ от дублирования кода — полезная практика, потому что, если вам потребуется изменить этот код (чтобы исправить ошиб- ку или добавить новые возможности), изменения достаточно внести только в одном месте. Также без дублирования кода про- грамма становится более короткой и удобочитаемой.
Наследование (inheritance) представляет собой метод повторного использования кода, который применяется к классам. Это механизм организации классов в си- стеме отношений «родитель — потомок», в которой дочерний класс наследует копию методов своего родительского класса, что избавляет вас от необходимости дублировать код метода в нескольких классах.
Многие программисты считают наследование переоцененным и даже опасным из-за дополнительной сложности, которую добавляют в программу большие иерархии на- следования. Когда вы встречаете в блогах публикации типа «Наследование — зло», не стоит полагать, что они совершенно не обоснованы: да, наследованием можно злоупотреблять. Тем не менее разумное применение наследования может сильно сэкономить время при организации вашего кода.
Как работает наследование
Чтобы создать новый дочерний класс, укажите имя существующего родительско- го класса в круглых скобках в команде class
. Чтобы потренироваться в создании
336
Глава 16.Объектно-ориентированное программирование и наследование дочерних классов, откройте в редакторе окно нового файла, введите следующую программу и сохраните ее с именем inheritanceExample.py
:
class ParentClass:
❶
def printHello(self):
❷
print('Hello, world!')
class ChildClass(ParentClass):
❸
def someNewMethod(self):
print('ParentClass objects don't have this method.')
class GrandchildClass(ChildClass):
❹
def anotherNewMethod(self):
print('Only GrandchildClass objects have this method.')
print('Create a ParentClass object and call its methods:')
parent = ParentClass()
parent.printHello()
print('Create a ChildClass object and call its methods:')
child = ChildClass()
child.printHello()
child.someNewMethod()
print('Create a GrandchildClass object and call its methods:')
grandchild = GrandchildClass()
grandchild.printHello()
grandchild.someNewMethod()
grandchild.anotherNewMethod()
print('An error:')
parent.someNewMethod()
При выполнении этой программы результат выглядит примерно так:
Create a ParentClass object and call its methods:
Hello, world!
Create a ChildClass object and call its methods:
Hello, world!
ParentClass objects don't have this method.
Create a GrandchildClass object and call its methods:
Hello, world!
ParentClass objects don't have this method.
Only GrandchildClass objects have this method.
An error:
Traceback (most recent call last):
File "inheritanceExample.py", line 35, in
parent.someNewMethod() # ParentClass objects don't have this method.
AttributeError: 'ParentClass' object has no attribute 'someNewMethod'