Изучаем Python Эрик Метиз. Crash course2 n d e d i t i o na h a n d s o n, p r o j e c t b a s e d i n t r o d u c t i o n t o p r o g r a m m i n g
Скачать 6.19 Mb.
|
182 Глава 9 • Классы ко всем атрибутам класса-родителя. Имя super происходит из распространенной терминологии: класс-родитель называется суперклассом, а класс-потомок — под- классом. Чтобы проверить, правильно ли сработало наследование, попробуем создать элек- тромобиль с такой же информацией, которая передается при создании обычного экземпляра Car . В точке мы создаем экземпляр класса ElectricCar и сохраняем его в my_tesla . Эта строка вызывает метод __init__() , определенный в ElectricCar , который, в свою очередь, приказывает Python вызвать метод __init__() , определен- ный в классе-родителе Car . При вызове передаются аргументы 'tesla' , 'model s' и 2019 Кроме __init__() , класс еще не содержит никаких атрибутов или методов, специ- фических для электромобилей. Пока мы просто убеждаемся в том, что класс электромобиля содержит все поведение, присущее классу автомобиля: 2019 Tesla Model S Экземпляр ElectricCar работает так же, как экземпляр Car ; можно переходить к определению атрибутов и методов, специфических для электромобилей. Определение атрибутов и методов класса-потомка После создания класса-потомка, наследующего от класса-родителя, можно пере- ходить к добавлению новых атрибутов и методов, необходимых для того, чтобы потомок отличался от родителя. Добавим атрибут, специфический для электромобилей (например, мощность аккумуляторa), и метод для вывода информации об этом атрибуте: class Car(): class ElectricCar(Car): """Представляет аспекты машины, специфические для электромобилей.""" def __init__(self, make, model, year): """ Инициализирует атрибуты класса-родителя. Затем инициализирует атрибуты, специфические для электромобиля. """ super().__init__(make, model, year) ❶ self.battery_size = 75 ❷ def describe_battery(self): """Выводит информацию о мощности аккумулятора.""" print(f"This car has a {self.battery_size}-kWh battery.") my_tesla = ElectricCar('tesla', 'model s', 2019) print(my_tesla.get_descriptive_name()) my_tesla.describe_battery() Наследование 183 В точке добавляется новый атрибут self.battery_size , которому присваивается исходное значение — скажем, 75. Этот атрибут будет присутствовать во всех экзем- плярах, созданных на основе класса ElectricCar (но не во всяком экземпляре Car ). Также добавляется метод с именем describe_battery() , который выводит инфор- мацию об аккумуляторе в точке . При вызове этого метода выводится описание, которое явно относится только к электромобилям: 2019 Tesla Model S This car has a 75-kWh battery. Возможности специализации класса ElectricCar беспредельны. Вы можете до- бавить сколько угодно атрибутов и методов, чтобы моделировать электромобиль с любой нужной точностью. Атрибуты или методы, которые могут принадлежать любой машине (а не только электромобилю), должны добавляться в класс Car вместо ElectricCar . Тогда эта информация будет доступна всем пользователям класса Car , а класс ElectricCar будет содержать только код информации и пове- дения, специфических для электромобилей. Переопределение методов класса-родителя Любой метод родительского класса, который в моделируемой ситуации делает не то, что нужно, можно переопределить. Для этого в классе-потомке определяется метод с тем же именем, что и у метода класса-родителя. Python игнорирует метод родителя и обращает внимание только на метод, определенный в потомке. Допустим, в классе Car имеется метод fill_gas_tank() . Для электромобилей за- правка бензином бессмысленна, поэтому этот метод логично переопределить. На- пример, это можно сделать так: class ElectricCar(Car): def fill_gas_tank(self): """У электромобилей нет бензобака.""" print("This car doesn't need a gas tank!") И если кто-то попытается вызвать метод fill_gas_tank() для электромобиля, Python проигнорирует метод fill_gas_tank() класса Car и выполнит вместо него этот код. С применением наследования потомок сохраняет те аспекты родителя, которые вам нужны, и переопределяет все ненужное. Экземпляры как атрибуты При моделировании явлений реального мира в программах классы нередко до- полняются все большим количеством подробностей. Списки атрибутов и мето- дов растут, и через какое-то время файлы становятся длинными и громоздкими. В такой ситуации часть одного класса нередко можно записать в виде отдельного 184 Глава 9 • Классы класса. Большой код разбивается на меньшие классы, которые работают во взаи- модействии друг с другом. Например, при дальнейшей доработке класса ElectricCar может оказаться, что в нем появилось слишком много атрибутов и методов, относящихся к аккумулято- ру. В таком случае можно остановиться и переместить все эти атрибуты и методы в отдельный класс с именем Battery . Затем экземпляр Battery становится атрибу- том класса ElectricCar : class Car(): ❶ class Battery(): """Простая модель аккумулятора электромобиля.""" ❷ def __init__(self, battery_size=75): """Инициализирует атрибуты аккумулятора.""" self.battery_size = battery_size ❸ def describe_battery(self): """Выводит информацию о мощности аккумулятора.""" print(f"This car has a {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', 2019) print(my_tesla.get_descriptive_name()) my_tesla.battery.describe_battery() В точке определяется новый класс с именем Battery , который не наследует ни один из других классов. Метод __init__() в точке получает один параметр battery_size , кроме self . Если значение не предоставлено, этот необязательный параметр задает battery_size значение 75. Метод describe_battery() также пере- мещен в этот класс . Затем в класс ElectricCar добавляется атрибут с именем self.battery . Эта стро- ка приказывает Python создать новый экземпляр Battery (со значением battery_ size по умолчанию, равным 75, потому что значение не задано) и сохранить его в атрибуте self.battery . Это будет происходить при каждом вызове __init__() ; теперь любой экземпляр ElectricCar будет иметь автоматически создаваемый экземпляр Battery Наследование 185 Программа создает экземпляр электромобиля и сохраняет его в переменной my_ tesla . Когда потребуется вывести описание аккумулятора, необходимо обратиться к атрибуту battery : my_tesla.battery.describe_battery() Эта строка приказывает Python обратиться к экземпляру my_tesla , найти его атрибут battery и вызвать метод describe_battery() , связанный с экземпляром Battery из атрибута. Результат выглядит так же, как и в предыдущей версии: 2019 Tesla Model S This car has a 75-kWh battery. Казалось бы, новый вариант требует большой дополнительной работы, но теперь аккумулятор можно моделировать с любой степенью детализации без загромож- дения класса ElectricCar . Добавим в Battery еще один метод, который выводит запас хода на основании мощности аккумулятора: class Car(): class Battery(): ❶ def get_range(self): """Выводит приблизительный запас хода для аккумулятора.""" if self.battery_size == 75: range = 260 elif self.battery_size == 100: range = 315 print(f"This car can go about {range} miles on a full charge.") class ElectricCar(Car): my_tesla = ElectricCar('tesla', 'model s', 2019) print(my_tesla.get_descriptive_name()) my_tesla.battery.describe_battery() ❷ my_tesla.battery.get_range() Новый метод get_range() в точке проводит простой анализ. Если мощность равна 75, то get_range() устанавливает запас хода 260 миль, а при мощности 100 кВт/ч запас хода равен 315 милям. Затем программа выводит это значение. Когда вы захотите использовать этот метод, его придется вызывать через атрибут battery в точке . Результат сообщает запас хода машины в зависимости от мощности аккумуля- тора: 186 Глава 9 • Классы 2019 Tesla Model S This car has a 75-kWh battery. This car can go approximately 260 miles on a full charge. Моделирование объектов реального мира Занявшись моделированием более сложных объектов, таких как электромоби- ли, вы столкнетесь с множеством интересных вопросов. Является ли запас хода электромобиля свойством аккумулятора или машины? Если вы описываете толь- ко одну машину, вероятно, можно связать метод get_range() с классом Battery Но если моделируется целая линейка машин от производителя, вероятно, метод get_range() правильнее будет переместить в класс ElectricCar . Метод get_range() по-прежнему будет проверять мощность аккумулятора перед определением за- паса хода, но он будет сообщать запас хода для той машины, с которой он связан. Также возможно связать метод get_range() с аккумулятором, но передавать ему параметр (например, car_model ). Метод get_range() будет определять запас хода на основании мощности аккумулятора и модели автомобиля. Если вы начнете ломать голову над такими вопросами, это означает, что вы мыслите на более высоком логическом уровне, не ограничиваясь уровнем синтаксиса. Вы думаете уже не о Python, а о том, как лучше представить реальный мир в своем коде. И достигнув этой точки, вы поймете, что однозначно правильного или неправиль- ного подхода к моделированию реальных ситуаций часто не существует. Некоторые методы эффективнее других, но для того, чтобы найти наиболее эффективную ре- ализацию, необходим практический опыт. Если ваш код работает именно так, как вы хотели, — значит, у вас все получается! Не огорчайтесь, если окажется, что вы по несколько раз переписываете свои классы для разных решений. На пути к написанию точного, эффективного кода все программисты проходят через этот процесс. УПРАЖНЕНИЯ 9.6. Киоск с мороженым: киоск с мороженым — особая разновидность ресторана. Напиши- те класс IceCreamStand, наследующий от класса Restaurant из упражнения 9.1 (с. 175) или упражнения 9.4 (с. 180). Подойдет любая версия класса; просто выберите ту, которая вам больше нравится. Добавьте атрибут с именем flavors для хранения списка сортов мороже- ного. Напишите метод, который выводит этот список. Создайте экземпляр IceCreamStand и вызовите этот метод. 9.7. Администратор: администратор — особая разновидность пользователя. Напишите класс с именем Admin, наследующий от класса User из упражнения 9.3 или упражнения 9.5 (с. 180). Добавьте атрибут privileges для хранения списка строк вида "разрешено добавлять сообщения" , "разрешено удалять пользователей" , "разрешено банить пользователей" и т. д. На- пишите метод show_privileges() для вывода набора привилегий администратора. Создай- те экземпляр Admin и вызовите свой метод. 9.8. Привилегии: напишите класс Privileges . Класс должен содержать всего один атри- бут privileges со списком строк из упражнения 9.7. Переместите метод show_privileges() в этот класс. Создайте экземпляр Privileges как атрибут класса Admin . Создайте новый эк- земпляр Admin и используйте свой метод для вывода списка привилегий. Импортирование классов 187 9.9. Обновление аккумулятора: используйте окончательную версию программы electric_ car .py из этого раздела. Добавьте в класс Battery метод с именем upgrade_battery() . Этот метод должен проверять размер аккумулятора и устанавливать мощность равной 100, если она имеет другое значение. Создайте экземпляр электромобиля с аккумулятором по умол- чанию, вызовите get_range() , а затем вызовите get_range() во второй раз после вызова upgrade_battery() . Убедитесь в том, что запас хода увеличился. Импортирование классов С добавлением новой функциональности в классы файлы могут стать слишком длинными, даже при правильном использовании наследования. В соответствии с общей философией Python файлы не должны загромождаться лишними подроб- ностями. Для этого Python позволяет хранить классы в модулях и импортировать нужные классы в основную программу. Импортирование одного класса Начнем с создания модуля, содержащего только класс Car . При этом возникает неочевидный конфликт имен: в этой главе уже был создан файл с именем car .py , но этот модуль тоже должен называться car .py , потому что в нем содержится код класса Car . Мы решим эту проблему, сохранив класс Car в модуле с именем car .py , заменяя им файл car .py , который использовался ранее. В дальнейшем любой про- грамме, использующей этот модуль, придется присвоить более конкретное имя файла — например, my_car .py . Ниже приведен файл car .py с кодом класса Car : 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 = f"{self.year} {self.manufacturer} {self.model}" return long_name.title() def read_odometer(self): """Выводит пробег машины в милях.""" print(f"This car has {self.odometer_reading} miles on it.") def update_odometer(self, mileage): 188 Глава 9 • Классы """ Устанавливает на одометре заданное значение. При попытке обратной подкрутки изменение отклоняется. """ 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 В точке включается строка документации уровня модуля с кратким описанием содержимого модуля. Пишите строки документации для каждого созданного вами модуля. Теперь мы создадим отдельный файл с именем my_car .py . Этот файл импортирует класс Car и создает экземпляр этого класса: my_car.py ❶ from car import Car my_new_car = Car('audi', 'a4', 2019) print(my_new_car.get_descriptive_name()) my_new_car.odometer_reading = 23 my_new_car.read_odometer() Команда import в точке приказывает Python открыть модуль car и импортиро- вать класс Car . Теперь мы можем использовать класс Car так, как если бы он был определен в этом файле. Результат остается тем же, что и в предыдущей версии: 2019 Audi A4 This car has 23 miles on it. Импортирование классов повышает эффективность программирования. Пред- ставьте, каким длинным получился бы файл этой программы, если бы в него был включен весь класс Car . Перемещая класс в модуль и импортируя этот модуль, вы получаете ту же функциональность, но основной файл программы при этом остается чистым и удобочитаемым. Большая часть логики также может храниться в отдельных файлах; когда ваши классы работают так, как положено, вы можете забыть об этих файлах и сосредоточиться на высокоуровневой логике основной программы. Хранение нескольких классов в модуле В одном модуле можно хранить сколько угодно классов, хотя все эти классы должны быть каким-то образом связаны друг с другом. Оба класса, Battery Импортирование классов 189 и ElectricCar , используются для представления автомобилей, поэтому мы до- бавим их в модуль car .py : car.py """Классы для представления машин с бензиновым и электродвигателем.""" class Car(): class Battery(): """Простая модель аккумулятора электромобиля.""" def __init__(self, battery_size=70): """Инициализация атрибутов аккумулятора.""" self.battery_size = battery_size def describe_battery(self): """Выводит информацию о мощности аккумулятора.""" print(f"This car has a {self.battery_size}-kWh battery.") def get_range(self): """Выводит приблизительный запас хода для аккумулятора.""" if self.battery_size == 75: range = 260 elif self.battery_size == 100: range = 315 print(f"This car can go about {range} miles on a full charge.") class ElectricCar(Car): """Представляет аспекты машины, специфические для электромобилей.""" def __init__(self, make, model, year): """ Инициализирует атрибуты класса-родителя. Затем инициализирует атрибуты, специфические для электромобиля. """ super().__init__(make, model, year) self.battery = Battery() Теперь вы можете создать новый файл с именем my_electric_car .py , импортировать класс ElectricCar и создать новый экземпляр электромобиля: my_electric_car.py from car import ElectricCar my_tesla = ElectricCar('tesla', 'model s', 2019) print(my_tesla.get_descriptive_name()) my_tesla.battery.describe_battery() my_tesla.battery.get_range() Программа выводит тот же результат, что и в предыдущем случае, хотя большая часть ее логики скрыта в модуле: 190 Глава 9 • Классы 2019 Tesla Model S This car has a 75-kWh battery. This car can go approximately 260 miles on a full charge. Импортирование нескольких классов из модуля В файл программы можно импортировать столько классов, сколько потребуется. Если вы захотите создать обычный автомобиль и электромобиль в одном файле, потребуется импортировать оба класса, Car и ElectricCar : my_cars.py ❶ from car import Car, ElectricCar ❷ my_beetle = Car('volkswagen', 'beetle', 2019) print(my_beetle.get_descriptive_name()) ❸ my_tesla = ElectricCar('tesla', 'roadster', 2019) print(my_tesla.get_descriptive_name()) Чтобы импортировать несколько классов из модуля, разделите их имена запя- тыми . После того как необходимые классы будут импортированы, вы можете создать столько экземпляров каждого класса, сколько вам потребуется. В этом примере создается обычный автомобиль Volkswagen Beetle и электро- мобиль Tesla Roadster : 2019 Volkswagen Beetle 2019 Tesla Roadster Импортирование всего модуля Также возможно импортировать весь модуль, а потом обращаться к нужным клас- сам с использованием точечной записи. Этот способ прост, а полученный код легко читается. Так как каждый вызов, создающий экземпляр класса, включает имя моду- ля, в программе не будет конфликтов с именами, используемыми в текущем файле. |