ПРИМЕЧАНИЕ
Подобные методы управляют обновлением внутренних значений экземпляров (таких, как показа-
ния одометра), однако любой пользователь, имеющий доступ к программному коду, сможет напря-
мую задать атрибуту любое значение . Эффективная схема безопасности должна уделять особое
внимание таким подробностям, не ограничиваясь простейшими проверками .
УПРАЖНЕНИЯ
9-4 . Посетители: начните с программы из упражнения 9-1 (с . 165) . Добавьте атрибут
number_served со значением по умолчанию 0; он представляет количество обслуженных
посетителей . Создайте экземпляр с именем restaurant . Выведите значение number_served,
потом измените и выведите снова .
Добавьте метод с именем set_number_served(), позволяющий задать количество обслужен-
ных посетителей . Вызовите метод с новым числом, снова выведите значение .
170 Глава 9 • Классы
Добавьте метод с именем increment_number_served(), который увеличивает количество
обслуженных посетителей на заданную величину . Вызовите этот метод с любым числом,
которое могло бы представлять количество обслуженных клиентов — скажем, за один день .
9-5 . Попытки входа: добавьте атрибут login_attempts в класс User из упражнения 9-3
(с . 165) . Напишите метод increment_login_attempts(), увеличивающий значение login_
attempts на 1 . Напишите другой метод с именем reset_login_attempts(), обнуляющий значе-
ние login_attempts .
Создайте экземпляр класса User и вызовите increment_login_attempts() несколько раз . Вы-
ведите значение login_attempts, чтобы убедиться в том, что значение было изменено пра-
вильно, а затем вызовите reset_login_attempts() . Снова выведите login_attempts и убеди-
тесь в том, что значение обнулилось .
Наследование
Работа над новым классом не обязана начинаться с нуля. Если класс, который вы
пишете, представляет собой специализированную версию ранее написанного клас-
са, вы можете воспользоваться наследованием. Один класс, наследующий от другого,
автоматически получает все атрибуты и методы первого класса. Исходный класс
называется родителем, а новый класс — потомком. Класс-потомок наследует
атрибуты и методы родителя, но при этом также может определять собственные
атрибуты и методы.
Метод __init__() класса-потомка
Первое, что делает Python при создании экземпляра класса-потомка, — присваивает
значения всем атрибутам класса-родителя. Для этого методу
__init__()
класса-по-
томка необходима помощь со стороны родителя.
Например, попробуем построить модель электромобиля. Электромобиль пред-
ставляет собой специализированную разновидность автомобиля, поэтому новый
класс
ElectricCar
можно создать на базе класса
Car
, написанного ранее. Тогда
нам останется добавить в него код атрибутов и поведения, относящегося только
к электромобилям.
Начнем с создания простой версии класса
ElectricCar
, который делает все, что
делает класс
Car
:
electric_car.py
class Car():
"""Простая модель автомобиля."""
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
self.odometer_reading = 0
def get_descriptive_name(self):
long_name = str(self.year) + ' ' + self.make + ' ' + self.model
return long_name.title()
Наследование 171
def read_odometer(self):
print("This car has " + str(self.odometer_reading) + " miles on it.")
def update_odometer(self, mileage):
if mileage >= self.odometer_reading:
self.odometer_reading = mileage
else:
print("You can't roll back an odometer!")
def increment_odometer(self, miles):
self.odometer_reading += miles
class ElectricCar(Car):
"""Представляет аспекты машины, специфические для электромобилей."""
def __init__(self, make, model, year):
"""Инициализирует атрибуты класса-родителя."""
super().__init__(make, model, year)
my_tesla = ElectricCar('tesla', 'model s', 2016)
print(my_tesla.get_descriptive_name())
В точке строится экземпляр
Car
. При создании класса-потомка класс-родитель
должен быть частью текущего файла, а его определение должно предшествовать
определению класса-потомка в файле. В точке определяется класс-потомок
ElectricCar
. В определении потомка имя класса-родителя заключается в круглые
скобки. Метод
__init__()
в точке получает информацию, необходимую для
создания экземпляра
Car
.
Функция
super()
в строке — специальная функция, которая помогает Python
связать потомка с родителем. Эта строка приказывает Python вызвать метод
__init__()
класса, являющегося родителем
ElectricCar
, в результате чего экзем-
пляр
ElectricCar
получает все атрибуты класса-родителя. Имя
super
происходит
из распространенной терминологии: класс-родитель называется суперклассом,
а класс-потомок — субклассом.
Чтобы проверить, правильно ли сработало наследование, попробуем создать
электромобиль с такой же информацией, которая передается при создании
обычного экземпляра
Car
. В точке мы создаем экземпляр класса
ElectricCar
и сохраняем его в
my_tesla
. Эта строка вызывает метод
__init__()
, определен-
ный в
ElectricCar
, который в свою очередь приказывает Python вызвать метод
__init__()
, определенный в классе-родителе
Car
. При вызове передаются аргу-
менты
'tesla'
,
'model
s'
и
2016
.
Кроме
__init__()
класс еще не содержит никаких атрибутов или методов, специ-
фических для электромобилей. Пока мы просто убеждаемся в том, что класс
электромобиля содержит все поведение, присущее классу автомобиля:
2016 Tesla Model S
Экземпляр
ElectricCar
работает так же, как экземпляр
Car
; можно переходить
к определению атрибутов и методов, специфических для электромобилей.
172 Глава 9 • Классы
Наследование в Python 2 .7
В Python 2.7 наследование реализовано немного иначе. Класс
ElectricCar
будет
выглядеть примерно так:
class Car(object):
def __init__(self, make, model, year):
...
class ElectricCar(Car):
def __init__(self, make, model, year):
super(ElectricCar, self).__init__(make, model, year)
...
Функция
super()
должна получать два аргумента: ссылку на класс-потомок и объ-
ект
self
. Эти аргументы необходимы для того, чтобы Python мог правильно связать
родителя с потомком. Если вы используете наследование в Python 2.7, убедитесь
в том, что родитель также определяется с синтаксисом
object
.
Определение атрибутов и методов класса-потомка
После создания класса-потомка, наследующего от класса-родителя, можно пере-
ходить к добавлению новых атрибутов и методов, необходимых для того, чтобы
потомок отличался от родителя.
Добавим атрибут, специфический для электромобилей (например, мощность ак-
кумулятора), и метод для вывода информации об этом атрибуте:
class Car():
...
class ElectricCar(Car):
"""Представляет аспекты машины, специфические для электромобилей."""
def __init__(self, make, model, year):
"""
Инициализирует атрибуты класса-родителя.
Затем инициализирует атрибуты, специфические для электромобиля.
"""
super().__init__(make, model, year)
self.battery_size = 70
def describe_battery(self):
"""Выводит информацию о мощности аккумулятора."""
print("This car has a " + str(self.battery_size) + "-kWh battery.")
my_tesla = ElectricCar('tesla', 'model s', 2016)
print(my_tesla.get_descriptive_name())
my_tesla.describe_battery()
В точке добавляется новый атрибут
self.battery_size
, которому присваивается
исходное значение — скажем, 70. Этот атрибут будет присутствовать во всех экзем-
плярах, созданных на основе класса
ElectricCar
(но не во всяком экземпляре
Car
).
Также добавляется метод с именем
describe_battery()
, который выводит инфор-
Наследование 173
мацию об аккумуляторе в точке . При вызове этого метода выводится описание,
которое явно относится только к электромобилям:
2016 Tesla Model S
This car has a 70-kWh battery.
Возможности специализации класса
ElectricCar
беспредельны. Вы можете до-
бавить сколько угодно атрибутов и методов, чтобы моделировать электромобиль
с любой нужной точностью. Атрибуты или методы, которые могут принадлежать
любой машине (а не только электромобилю), должны добавляться в класс
Car
вме-
сто
ElectricCar
. Тогда эта информация будет доступна всем пользователям класса
Car
, а класс
ElectricCar
будет содержать только код информации и поведения,
специфических для электромобилей.
Переопределение методов класса-родителя
Любой метод родительского класса, который в моделируемой ситуации делает
не то, что нужно, можно переопределить. Для этого в классе-потомке определяется
метод с тем же именем, что и у метода класса-родителя. Python игнорирует метод
родителя и обращает внимание только на метод, определенный в потомке.
Допустим, в классе
Car
имеется метод
fill_gas_tank()
. Для электромобилей за-
правка бензином бессмысленна, поэтому этот метод логично переопределить. На-
пример, это можно сделать так:
def ElectricCar(Car):
...
def fill_gas_tank():
"""У электромобилей нет бензобака."""
print("This car doesn't need a gas tank!")
И если кто-то попытается вызвать метод
fill_gas_tank()
для электромобиля,
Python игнорирует метод
fill_gas_tank()
класса
Car
и выполнит вместо него этот
код. С применением наследования потомок сохраняет те аспекты родителя, которые
вам нужны, и переопределяет все ненужное.
Экземпляры как атрибуты
При моделировании явлений реального мира в программах классы нередко до-
полняются все большим количеством подробностей. Списки атрибутов и мето-
дов растут, и через какое-то время файлы становятся длинными и громоздкими.
В такой ситуации часть одного класса нередко можно записать в виде отдельного
класса. Большой код разбивается на меньшие классы, которые работают во взаи-
модействии друг с другом.
Например, при дальнейшей доработке класса
ElectricCar
может оказаться, что
в нем появилось слишком много атрибутов и методов, относящихся к аккумулято-
ру. В таком случае можно остановиться и переместить все эти атрибуты и методы
в отдельный класс с именем
Battery
. Затем экземпляр
Battery
становится атрибу-
том класса
ElectricCar
:
174 Глава 9 • Классы
class Car():
...
class Battery():
"""Простая модель аккумулятора электромобиля."""
def __init__(self, battery_size=70):
"""Инициализирует атрибуты аккумулятора."""
self.battery_size = battery_size
def describe_battery(self):
"""Выводит информацию о мощности аккумулятора."""
print("This car has a " + str(self.battery_size) + "-kWh battery.")
class ElectricCar(Car):
"""Представляет аспекты машины, специфические для электромобилей."""
def __init__(self, make, model, year):
"""
Инициализирует атрибуты класса-родителя.
Затем инициализирует атрибуты, специфические для электромобиля.
"""
super().__init__(make, model, year)
self.battery = Battery()
my_tesla = ElectricCar('tesla', 'model s', 2016)
print(my_tesla.get_descriptive_name())
my_tesla.battery.describe_battery()
В точке определяется новый класс с именем
Battery
, который не наследует
ни от одного из других классов. Метод
__init__()
в точке получает один пара-
метр
battery_size
, кроме
self
. Если значение не предоставлено, этот необязатель-
ный параметр задает
battery_size
значение 70. Метод
describe_battery()
также
перемещен в этот класс .
Затем в класс
ElectricCar
добавляется атрибут с именем
self.battery
. Эта стро-
ка приказывает Python создать новый экземпляр
Battery
(со значением
battery_
size
по умолчанию, равным 70, потому что значение не задано) и сохранить его
в атрибуте
self.battery
. Это будет происходить при каждом вызове
__init__()
;
теперь любой экземпляр
ElectricCar
будет иметь автоматически создаваемый
экземпляр
Battery
.
Программа создает экземпляр электромобиля и сохраняет его в переменной
my_
tesla
. Когда потребуется вывести описание аккумулятора, необходимо обратиться
к атрибуту
battery
:
my_tesla.battery.describe_battery()
Эта строка приказывает Python обратиться к экземпляру
my_tesla
, найти его
атрибут
battery
и вызвать метод
describe_battery()
, связанный с экземпляром
Battery
из атрибута.
Результат выглядит так же, как и в предыдущей версии:
2016 Tesla Model S
This car has a 70-kWh battery.
Наследование 175
Казалось бы, новый вариант требует большой дополнительной работы, но теперь
аккумулятор можно моделировать с любой степенью детализации без загромож-
дения класса
ElectricCar
. Добавим в
Battery
еще один метод, который выводит
запас хода на основании мощности аккумулятора:
class Car():
...
class Battery():
...
def get_range(self):
"""Выводит приблизительный запас хода для аккумулятора."""
if self.battery_size == 70:
range = 240
elif self.battery_size == 85:
range = 270
message = "This car can go approximately " + str(range)
message += " miles on a full charge."
print(message)
class ElectricCar(Car):
...
my_tesla = ElectricCar('tesla', 'model s', 2016)
print(my_tesla.get_descriptive_name())
my_tesla.battery.describe_battery()
my_tesla.battery.get_range()
Новый метод
get_range()
в точке проводит простой анализ. Если мощность рав-
на 70, то
get_range()
устанавливает запас хода 240 миль, а при мощности 85 kWh
запас хода равен 270 милям. Затем программа выводит это значение. Когда вы
захотите использовать этот метод, его придется вызывать через атрибут
battery
в точке .
Результат сообщает запас хода машины в зависимости от мощности аккумулятора:
2016 Tesla Model S
This car has a 70-kWh battery.
This car can go approximately 240 miles on a full charge.
Моделирование объектов реального мира
Занявшись моделированием более сложных объектов — таких, как электромо-
били, — вы столкнетесь со множеством интересных вопросов. Является ли запас
хода электромобиля свойством аккумулятора или машины? Если вы описываете
только одну машину, вероятно, можно связать метод
get_range()
с классом
Battery
.
Но, если моделируется целая линейка машин от производителя, вероятно, метод
get_range()
правильнее будет переместить в класс
ElectricCar
. Метод
get_range()
по-прежнему будет проверять мощность аккумулятора перед определением за-
паса хода, но он будет сообщать запас хода для той машины, с которой он связан.
176 Глава 9 • Классы
Также возможно связать метод
get_range()
с аккумулятором, но передавать ему
параметр (например,
car_model
). Метод
get_range()
будет определять запас хода
на основании мощности аккумулятора и модели автомобиля.
Если вы начнете ломать голову над такими вопросами, это означает, что вы мыс-
лите на более высоком логическом уровне, не ограничиваясь уровнем синтаксиса.
Вы думаете уже не о Python, а о том, как представить реальный мир в коде. И, до-
стигнув этой точки, вы поймете, что однозначно правильного или неправильного
подхода к моделированию реальных ситуаций часто не существует. Некоторые
методы эффективнее других, но для того, чтобы найти наиболее эффективную
реализацию, необходим практический опыт. Если ваш код работает именно так,
как вы хотели, — значит, у вас все получается! Не огорчайтесь, если окажется, что
вы по несколько раз переписываете свои классы для разных решений. На пути
к написанию точного, эффективного кода все программисты проходят через этот
процесс.
Достарыңызбен бөлісу: |