Книга Изучаем Python
Скачать 4.68 Mb.
|
166 Глава 9 • Классы В точке в классе Car определяется метод __init__() ; его список параметров на- чинается с self , как и в классе Dog . За ним следуют еще три параметра: make , model и year . Метод __init__() получает эти параметры и сохраняет их в атрибутах, ко- торые будут связаны с экземплярами, созданными на основе класса. При создании нового экземпляра Car необходимо указать фирму-производителя, модель и год выпуска для данного экземпляра. В точке определяется метод get_descriptive_name() , который объединяет год выпуска, фирму-производителя и модель в одну строку с описанием. Это избавит вас от необходимости выводить значение каждого атрибута по отдельности. Для работы со значениями атрибутов в этом методе используется синтаксис self.make , self.model и self.year В точке создается экземпляр класса Car , который сохраняется в переменной my_new_car . Затем вызов метода get_descriptive_name() показывает, с какой ма- шиной работает программа: 2016 Audi A4 Чтобы класс был более интересным, добавим атрибут, изменяющийся со време- нем, — в нем будет храниться пробег машины в милях. Назначение атрибуту значения по умолчанию Каждый атрибут класса должен иметь исходное значение, даже если оно равно 0 или пустой строке. В некоторых случаях (например, при задании значений по умолчанию) это исходное значение есть смысл задавать в теле метода __init__() ; в таком случае передавать параметр для этого атрибута при создании объекта не обязательно. Добавим атрибут с именем odometer_reading , исходное значение которого всегда равно 0. Также в класс будет включен метод read_odometer() для чтения текущих показаний одометра: 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): def read_odometer(self): """Выводит пробег машины в милях.""" print("This car has " + str(self.odometer_reading) + " miles on it.") my_new_car = Car('audi', 'a4', 2016) print(my_new_car.get_descriptive_name()) my_new_car.read_odometer() Работа с классами и экземплярами 167 Когда Python вызывает метод __init__() для создания нового экземпляра, этот метод сохраняет фирму-производителя, модель и год выпуска в атрибутах, как и в предыдущем случае. Затем Python создает новый атрибут с именем odometer_ reading и присваивает ему исходное значение 0 . Также в класс добавляется новый метод read_odometer() , который упрощает чтение пробега машины в милях. Сразу же после создания машины ее пробег равен 0: 2016 Audi A4 This car has 0 miles on it. Впрочем, у продаваемых машин одометр редко показывает ровно 0, поэтому нам понадобится способ изменения значения этого атрибута. Изменение значений атрибутов Значение атрибута можно изменить одним из трех способов: изменить его прямо в экземпляре, задать значение при помощи метода или изменить его с приращением (то есть прибавлением определенной величины) при помощи метода. Рассмотрим все эти способы. Прямое изменение значения атрибута Чтобы изменить значение атрибута, проще всего обратиться к нему прямо через эк- земпляр. В следующем примере на одометре напрямую выставляется значение 23: class Car(): my_new_car = Car('audi', 'a4', 2016) print(my_new_car.get_descriptive_name()) my_new_car.odometer_reading = 23 my_new_car.read_odometer() В точке точечная запись используется для обращения к атрибуту odometer_ reading экземпляра и прямого присваивания его значения. Эта строка приказывает Python взять экземпляр my_new_car , найти связанный с ним атрибут odometer_ reading и задать значение атрибута равным 23: 2016 Audi A4 This car has 23 miles on it. Иногда подобные прямые обращения к атрибутам допустимы, но чаще разработчик пишет вспомогательный метод, который изменяет значение за него. Изменение значения атрибута с использованием метода В класс можно включить методы, которые изменяют некоторые атрибуты за вас. Вместо того чтобы изменять атрибут напрямую, вы передаете новое значение ме- тоду, который берет обновление атрибута на себя. 168 Глава 9 • Классы В следующем примере в класс включается метод update_odometer() для изменения показаний одометра: class Car(): def update_odometer(self, mileage): """Устанавливает заданное значение на одометре.""" self.odometer_reading = mileage my_new_car = Car('audi', 'a4', 2016) print(my_new_car.get_descriptive_name()) my_new_car.update_odometer(23) my_new_car.read_odometer() Класс Car почти не изменился, в нем только добавился метод update_odometer() . Этот метод получает пробег в милях и сохраняет его в self.odometer_reading В точке мы вызываем метод update_odometer() и передаем ему значение 23 в аргументе (соответствующем параметру mileage в определении метода). Метод устанавливает на одометре значение 23, а метод read_odometer() выводит текущие показания: 2016 Audi A4 This car has 23 miles on it. Метод update_odometer() можно расширить так, чтобы при каждом изменении показаний одометра выполнялась некоторая дополнительная работа. Добавим проверку, которая гарантирует, что никто не будет пытаться сбрасывать показания одометра: class Car(): def update_odometer(self, mileage): """ Устанавливает на одометре заданное значение. При попытке обратной подкрутки изменение отклоняется. """ if mileage >= self.odometer_reading: self.odometer_reading = mileage else: print("You can't roll back an odometer!") Теперь update_odometer() проверяет новое значение перед изменением атрибута. Если новое значение mileage больше или равно текущего, self.odometer_reading , показания одометра можно обновить новым значением . Если же новое значение меньше текущего, вы получите предупреждение о недопустимости обратной под- крутки . Изменение значения атрибута с приращением Иногда значение атрибута требуется изменить с заданным приращением (вме- сто того чтобы присваивать атрибуту произвольное новое значение). Допустим, Работа с классами и экземплярами 169 вы купили подержанную машину и проехали на ней 100 миль. Следующий метод получает величину приращения и прибавляет ее к текущим показаниям одометра: class Car(): def update_odometer(self, mileage): --snip-- def increment_odometer(self, miles): """Увеличивает показания одометра с заданным приращением.""" self.odometer_reading += miles my_used_car = Car('subaru', 'outback', 2013) print(my_used_car.get_descriptive_name()) my_used_car.update_odometer(23500) my_used_car.read_odometer() my_used_car.increment_odometer(100) my_used_car.read_odometer() Новый метод increment_odometer() в точке получает расстояние в милях и прибавляет его к self.odometer_reading . В точке создается экземпляр my_used_car . Мы инициализируем показания его одометра значением 23 500; для этого вызывается метод update_odometer() , которому передается значение 23500 . В точке вызывается метод increment_odometer() , которому передает- ся значение 100, чтобы увеличить показания одометра на 100 миль, пройденные с момента покупки: 2013 Subaru Outback This car has 23500 miles on it. This car has 23600 miles on it. При желании можно легко усовершенствовать этот метод, чтобы он отклонял отрицательные приращения; тем самым вы предотвратите обратную подкрутку одометра. ПРИМЕЧАНИЕ Подобные методы управляют обновлением внутренних значений экземпляров (таких, как показа- ния одометра), однако любой пользователь, имеющий доступ к программному коду, сможет напря- мую задать атрибуту любое значение . Эффективная схема безопасности должна уделять особое внимание таким подробностям, не ограничиваясь простейшими проверками . УПРАЖНЕНИЯ 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. |