Книга: Изучаем Python: программирование игр, визуализация данных, веб-приложения. 3-е изд. дополненное и переработанное
Назад: 8. Функции
Дальше: 10. Файлы и исключения

9. Классы

19753.png

 

Объектно-ориентированное программирование (ООП) по праву считается одной из самых эффективных методологий создания программных продуктов. В ООП вы пишете классы, описыва­ющие реально существующие предметы и ситуации, а затем на основе этих описаний создаете объекты. При написании класса определяется общее поведение для целой категории объектов.

Когда вы создаете на базе классов конкретные объекты, каждый из них автоматически наделяется общим поведением; после этого вы можете прописать для каждого объекта уникальные особенности на свой вкус. Просто невероятно, насколько хорошо реальные ситуации моделируются в объектно-ориентированном программировании.

Создание объекта на основе класса называется созданием экземпляра; таким образом, вы работаете с экземплярами класса. В этой главе вы будете писать классы и создавать их экземпляры. Вы будете указывать, какая информация может храниться в экземплярах, и определять действия, которые могут выполняться с экземплярами. Кроме того, вы будете писать новые классы, расширяющие функциональность уже существующих; это позволяет организовать эффективное совместное использование кода похожими классами. Вы будете сохранять классы в модулях и импортировать классы, написанные другими программистами, в ваши программные файлы.

Имея хорошее понимание объектно-ориентированного программирования, вы взглянете на мир с точки зрения программиста. Вам будет проще понять свой код — увидеть не только то, что происходит в каждой его строке, но и более масштабные концепции, на которых он строится. Логика, заложенная в основу классов, научит вас мыслить логически, чтобы ваши программы эффективно решали практически любые задачи, с которыми вы можете столкнуться.

Кроме того, классы упрощают жизнь вам и другим программистам, с которыми вам придется работать совместно над более серьезными проектами. Когда вы и другие программисты пишете код, базирующийся на сходной логике, вам будет проще разобраться в коде, написанном другими людьми. Ваши программы будут понятны коллегам, и в результате все вы сможете добиться бо́льших результатов.

Создание и использование класса

Классы позволяют моделировать практически все, что угодно. Начнем с написания простого класса Dog, представляющего собаку — не какую-то конкретную, а собаку вообще. Что мы знаем о собаках? У них есть кличка и возраст, а еще большинство собак умеют садиться и перекатываться по команде. Эти два вида информации (кличка и возраст) и два вида поведения (сидеть и перекатываться) будут добавлены в класс Dog, поскольку являются общими для большинства собак. Класс сообщает Python, как создать объект, представляющий собаку. После того как класс будет написан, мы используем его для создания экземпляров, каждый из которых соответствует одной конкретной собаке.

Создание класса Dog

В каждом экземпляре, созданном на основе класса Dog, будут храниться данные о кличке (name) и возрасте (age); кроме того, в нем будут присутствовать функции sit() и roll_over():

dog.py

❶ class Dog:

    """Простая модель собаки."""

 

❷     def __init__(self, name, age):

        """Инициализирует атрибуты name и age."""

❸         self.name = name

        self.age = age

 

❹     def sit(self):

        """Имитирует, как собака садится по команде."""

          print(f"{self.name} is now sitting.")

 

    def roll_over(self):

        """Имитирует, как собака перекатывается по команде."""

        print(f"{self.name} rolled over!")

В этом коде есть много мест, заслуживающих вашего внимания, но не беспокойтесь. Эта структура неоднократно встретится вам в данной главе, и вы еще успеете к ней привыкнуть. Сначала мы определили класс Dog . Согласно общепринятым правилам, имена, начинающиеся с символа верхнего регистра, в Python обозначают классы. В определении класса нет круглых скобок, поскольку он создается с нуля. Ниже приведена строка документации с кратким описанием класса.

Метод __init__()

Функция, являющаяся частью класса, называется методом. Все, что вы узнали ранее о функциях, относится и к методам; единственное практическое различие — способ вызова методов. Метод __init__() — специальный метод, который автоматически выполняется при создании каждого нового экземпляра на базе класса Dog. Имя метода начинается и заканчивается двумя символами подчеркивания; эта схема предотвращает конфликты имен стандартных методов Python и методов ваших классов. Будьте внимательны: два символа подчеркивания должны стоять по обе стороны: __init__(). Если вы поставите только один символ подчеркивания с каждой стороны, то метод не будет вызываться автоматически при использовании класса, что может привести к появлению ошибок, которые сложно обнаружить.

Метод __init__() определяется с тремя параметрами: self, name и age. Параметр self обязателен; он должен предшествовать всем остальным параметрам. Его следует добавить в определение, поскольку при будущем вызове метода __init__() (для создания экземпляра Dog) Python автоматически передает аргумент self. При каждом вызове метода, связанного с классом, автоматически передается self — ссылка на экземпляр; она предоставляет конкретному экземпляру доступ к атрибутам и методам класса. Когда мы создаем экземпляр Dog, Python вызывает метод __init__() из класса Dog. Мы передаем Dog() кличку и возраст в аргументах; значение self передается автоматически — вам не нужно это делать. Каждый раз, когда вы захотите создать экземпляр на основе класса Dog, необходимо предоставить значения только двух последних аргументов: name и age.

Каждая из двух переменных, определяемых в теле метода __init__(), снабжена префиксом self . Переменная с этим префиксом доступна для любого метода в классе, и вы также сможете обращаться к этим переменным в каждом экземпляре, созданном на основе класса. Конструкция self.name = name берет значение, хранящееся в параметре name, и сохраняет его в переменной name, которая затем связывается с создаваемым экземпляром. Процесс повторяется и с self.age = age. Переменные, к которым вы обращаетесь через экземпляры, также называются атрибутами.

Кроме того, в классе Dog определяются два метода: sit() и roll_over() . Им не нужна дополнительная информация (кличка или возраст), поэтому они определяются с единственным параметром self. Экземпляры, которые будут созданы позднее, смогут вызывать эти методы. Пока данные методы ограничиваются простым выводом сообщения о том, что собака садится или перекатывается. Тем не менее концепцию легко расширить для практического применения: если бы этот класс был частью компьютерной игры, то эти методы вполне могли бы содержать код, позволяющий создать анимацию садящейся или перекатывающейся собаки. А если бы класс был написан для манипулирования роботом, то методы могли бы управлять механизмами, заставляющими робота-собаку выполнить соответствующую команду.

Создание экземпляра класса

Считайте, что класс — это своего рода инструкция по созданию экземпляров. Соответственно, класс Dog — инструкция по созданию экземпляров, представляющих конкретных собак.

Создадим экземпляр для конкретной собаки:

class Dog:

    --пропуск--

 

❶ my_dog = Dog('Willie', 6)

 

❷ print(f"My dog's name is {my_dog.name}.")

❸ print(f"My dog is {my_dog.age} years old.")

Использованный в данном случае класс Dog был написан в предыдущем примере. Python получает указание — создать экземпляр собаки с кличкой 'Willie' и возрастом 6 . В процессе обработки этой строки Python вызывает метод __init__() класса Dog с аргументами 'Willie' и 6. Метод __init__() создает экземпляр, представляющий конкретную собаку, и присваивает его атрибутам name и age переданные значения. Затем Python возвращает экземпляр, представляющий собаку. Он сохраняется в переменной my_dog. Здесь нелишне вспомнить правила о записи имен: обычно считается, что имя, начинающееся с символа верхнего регистра (например, Dog), обозначает класс, а имя, записанное в нижнем регистре (например, my_dog), обозначает отдельный экземпляр, созданный на базе класса.

Обращение к атрибутам

Для обращения к атрибутам экземпляра используется точечная нотация. Мы обращаемся к значению атрибута name экземпляра my_dog:

my_dog.name

Точечная нотация часто используется в Python. Этот синтаксис показывает, как Python ищет значения атрибутов. В данном случае Python обращается к экземпляру my_dog и ищет атрибут name, связанный с экземпляром my_dog. Это тот же атрибут, который обозначался как self.name в классе Dog. Тот же прием используется для работы с атрибутом age .

Пример выводит известную информацию о my_dog:

My dog's name is Willie.

My dog is 6 years old.

Вызов методов

После создания экземпляра на основе класса Dog можно применять точечную нотацию для вызова любых методов, определенных в Dog:

class Dog:

    --пропуск--

 

my_dog = Dog('Willie', 6)

my_dog.sit()

my_dog.roll_over()

Чтобы вызвать метод, укажите экземпляр (в данном случае my_dog) и вызываемый метод, разделив их точкой. В ходе обработки my_dog.sit() Python ищет метод sit() в классе Dog и выполняет его код. Строка my_dog.roll_over() интерпретируется аналогичным образом.

Теперь экземпляр послушно выполняет полученные команды:

Willie is now sitting.

Willie rolled over!

Это очень полезный синтаксис. Если атрибутам и методам были присвоены описательные имена (например, name, age, sit() и roll_over()), то разработчик сможет легко понять, что делает блок кода, — даже если видит его впервые.

Создание нескольких экземпляров

На основе класса можно создать сколько угодно экземпляров. Создадим второй экземпляр Dog с именем your_dog:

class Dog:

    --пропуск--

 

my_dog = Dog('Willie', 6)

your_dog = Dog('Lucy', 3)

 

print(f"My dog's name is {my_dog.name}.")

print(f"My dog is {my_dog.age} years old.")

my_dog.sit()

 

print(f"\nYour dog's name is {your_dog.name}.")

print(f"Your dog is {your_dog.age} years old.")

your_dog.sit()

В этом примере создаются два экземпляра с именами Willie и Lucy. Каждый экземпляр обладает своим набором атрибутов и способен выполнять действия из общего набора:

My dog's name is Willie.

My dog is 6 years old.

Willie is now sitting.

 

Your dog's name is Lucy.

Your dog is 3 years old.

Lucy is now sitting.

Даже если второй собаке будут назначены те же имя и возраст, Python все равно создаст отдельный экземпляр класса Dog. Вы можете создать сколько угодно экземпляров одного класса при условии, что они хранятся в переменных с различными именами или занимают разные позиции в списке или словаре.

Упражнения

9.1. Ресторан. Создайте класс Restaurant. Его метод __init__() должен содержать два атрибута: restaurant_name и cuisine_type. Создайте метод describe_restaurant(), который выводит два атрибута, и метод open_restaurant(), который выводит сообщение о том, что ресторан открыт.

Создайте на основе своего класса экземпляр restaurant. Выведите два атрибута по отдельности, затем вызовите оба метода.

9.2. Три ресторана. Начните с класса из упражнения 9.1. Создайте три разных экземпляра и вызовите для каждого экземпляра метод describe_restaurant().

9.3. Пользователи. Создайте класс User и два атрибута first_name и last_name, а затем еще несколько атрибутов, которые обычно хранятся в профиле пользователя. Напишите метод describe_user(), который выводит сводку с информацией о пользователе. Создайте еще один метод — greet_user(), позволяющий вывести персональное приветствие для пользователя.

Создайте несколько экземпляров, представляющих разных пользователей. Вызо­вите оба метода для каждого пользователя.

Работа с классами и экземплярами

Классы могут использоваться для моделирования многих реальных ситуаций. После того как класс будет написан, разработчик проводит бо́льшую часть времени за работой с экземплярами, созданными на основе этого класса. Одной из первых задач станет изменение атрибутов, связанных с конкретным экземпляром. Атрибуты экземпляра можно изменять напрямую или же написать методы, изменяющие их по особым правилам.

Класс Car

Напишем класс, который представляет автомобиль. Он будет содержать информацию о типе машины, а также метод для вывода краткого описания:

car.py

class Car:

    """Простая модель автомобиля."""

 

❶     def __init__(self, make, model, year):

        """Инициализирует атрибуты описания автомобиля."""

        self.make = make

        self.model = model

        self.year = year

 

❷     def get_descriptive_name(self):

        """Возвращает отформатированное описание."""

        long_name = f"{self.year} {self.make} {self.model}"

        return long_name.title()

 

❸ my_new_car = Car('audi', 'a4', 2024)

print(my_new_car.get_descriptive_name())

В классе 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(), чтобы увидеть, с какой машиной работает программа:

2024 Audi A4

Чтобы класс был более интересным, добавим атрибут, изменяющийся со временем, — в нем будут храниться данные о пробеге машины в милях.

Назначение атрибуту значения по умолчанию

При создании экземпляра можно определить атрибуты, не передавая их в качестве параметров. Это можно сделать в методе __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(f"This car has {self.odometer_reading} miles on it.")

 

my_new_car = Car('audi', 'a4', 2024)

print(my_new_car.get_descriptive_name())

my_new_car.read_odometer()

Когда Python вызывает метод __init__() для создания нового экземпляра, этот метод сохраняет данные о фирме-производителе, модели и годе выпуска в атрибутах, как и в предыдущем случае. Затем Python создает новый атрибут odometer_reading и присваивает ему исходное значение 0 . Кроме того, в класс добавляется новый метод read_odometer() , который упрощает чтение данных о пробеге машины в милях.

Сразу же после создания машины ее пробег равен 0:

2024 Audi A4

This car has 0 miles on it.

Впрочем, у продаваемых машин одометр редко показывает ровно 0, поэтому нам понадобится способ изменения значения этого атрибута.

Изменение значений атрибутов

Значение атрибута можно изменить одним из трех способов: изменить его прямо в экземпляре, задать значение с помощью метода или изменить его с приращением (то есть прибавлением определенной величины), используя метод. Рассмотрим все эти способы.

Прямое изменение значения атрибута

Чтобы изменить значение атрибута, проще всего обратиться к нему через экземпляр. В следующем примере на одометре напрямую выставляется значение 23:

class Car:

    --пропуск--

 

my_new_car = Car('audi', 'a4', 2024)

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:

2024 Audi A4

This car has 23 miles on it.

Иногда подобные прямые обращения к атрибутам допустимы, но чаще разработчик пишет вспомогательный метод, который изменяет значение за него.

Изменение значения атрибута с помощью метода

В класс можно добавить методы, которые изменяют определенные атрибуты за вас. Вместо того чтобы задавать атрибут напрямую, вы передаете новое значение методу, который берет обновление атрибута на себя.

В следующем примере в класс добавлен метод update_odometer():

class Car:

    --пропуск--

 

    def update_odometer(self, mileage):

        """Устанавливает заданное значение на одометре."""

        self.odometer_reading = mileage

 

my_new_car = Car('audi', 'a4', 2024)

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 в аргументе . Метод устанавливает на одометре значение 23, а метод read_odometer() выводит текущие показания:

2024 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, то показания одометра можно обновить с помощью нового значения . Если же новое значение меньше текущего, то вы получите предупреждение о недопустимости обратной подкрутки .

Изменение значения атрибута с приращением

Иногда значение атрибута требуется изменить с заданным приращением (вместо того чтобы присваивать атрибуту произвольное новое значение). Допустим, вы купили подержанную машину и проехали на ней 100 миль с момента покупки. Следующий метод получает величину приращения и прибавляет ее к текущим показаниям одометра:

class Car:

    --пропуск--

 

    def update_odometer(self, mileage):

        --пропуск--

 

    def increment_odometer(self, miles):

        """Увеличивает показания одометра с заданным приращением."""

        self.odometer_reading += miles

 

❶ my_used_car = Car('subaru', 'outback', 2019)

print(my_used_car.get_descriptive_name())

 

❷ my_used_car.update_odometer(23_500)

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(), которому передается значение 23_500 . Наконец, вызывается метод increment_odometer(), которому передается значение 100, чтобы увеличить показания одометра на 100 миль, которые автомобиль проехал с момента покупки:

2019 Subaru Outback

This car has 23500 miles on it.

This car has 23600 miles on it.

При желании можно легко усовершенствовать этот метод, чтобы он отклонял отрицательные приращения; тем самым вы предотвратите обратную подкрутку одометра.

ПРИМЕЧАНИЕ

Подобные методы управляют обновлением внутренних значений экземпляров (таких как показания одометра), однако любой пользователь, имеющий доступ к программному коду, сможет напрямую задать атрибуту любое значение. Создавая эффективную схему безопасности, нужно уделять особое внимание таким подробностям, не ограничиваясь простейшими проверками.

Упражнения

9.4. Посетители. Начните с программы из упражнения 9.1. Добавьте атрибут number_served со значением по умолчанию 0; он представляет количество обслуженных посетителей. Создайте экземпляр restaurant. Выведите значение number_served, потом измените и выведите снова.

Добавьте метод set_number_served(), позволяющий задать количество обслуженных посетителей. Вызовите его с новым числом, снова выведите значение.

Добавьте метод increment_number_served(), который увеличивает количество обслуженных посетителей на заданную величину. Вызовите его с любым числом, которое могло бы представлять количество обслуженных клиентов — скажем, за один день.

9.5. Попытки входа. Добавьте атрибут login_attempts в класс User из упражнения 9.3. Напишите метод increment_login_attempts(), увеличивающий значение login_attempts на 1. Напишите еще один метод, reset_login_attempts(), обнуляющий значение login_attempts.

Создайте экземпляр класса User и вызовите increment_login_attempts() несколько раз. Выведите значение login_attempts, чтобы убедиться в том, что значение было изменено правильно, а затем вызовите reset_login_attempts(). Снова выведите login_attempts и убедитесь в том, что значение обнулилось.

Наследование

Работа над новым классом не обязана начинаться с нуля. Если класс, который вы пишете, представляет собой специализированную версию ранее написанного класса, то вы можете воспользоваться наследованием (inheritance). Один класс, наследующий от другого, автоматически получает все атрибуты и методы первого класса. Исходный класс называется родителем (parent), а новый класс — потомком (child class). Класс-потомок наследует атрибуты и методы класса-родителя, но при этом может определять и собственные атрибуты и методы.

Метод __init__() класса-потомка

При написании нового класса на базе существующего часто приходится вызывать метод __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 = f"{self.year} {self.make} {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):

        """Устанавливает на одометре заданное значение."""

        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_leaf = ElectricCar('nissan', 'leaf', 2024)

print(my_leaf.get_descriptive_name())

Сначала создается класс Car . При создании класса-потомка класс-родитель должен быть частью текущего файла, а его определение должно предшествовать определению класса-потомка в файле. Затем определяется класс-потомок ElectricCar . В определении потомка имя класса-родителя заключается в круглые скобки. Метод __init__() получает информацию, необходимую для создания экземпляра Car .

Функция super() — специальная; она позволяет вызвать метод родительского класса. Благодаря этой строке Python получает указание вызвать метод __init__() класса Car, в результате чего экземпляр ElectricCar имеет доступ ко всем атрибутам класса-родителя. Имя super происходит из общепринятой терминологии: класс-родитель называется суперклассом, а класс-потомок — подклассом.

Чтобы проверить, правильно ли сработало наследование, попробуем создать электромобиль с такой же информацией, которая передается при создании обычного экземпляра Car. Мы создаем экземпляр класса ElectricCar и сохраняем его в my_leaf . Эта строка вызывает метод __init__(), определенный в ElectricCar, который, в свою очередь, дает Python указание вызвать метод __init__(), определенный в классе-родителе Car. При вызове передаются аргументы 'nissan', 'leaf' и 2024.

Кроме __init__(), класс еще не содержит никаких атрибутов или методов, специ­фических для электромобилей. Пока мы просто убеждаемся в том, что класс электромобиля содержит все поведение, присущее классу Car:

2024 Nissan Leaf

Экземпляр ElectricCar работает так же, как экземпляр Car. Теперь можно пе­реходить к определению атрибутов и методов, специфических для электромобилей.

Определение атрибутов и методов класса-потомка

После создания класса-потомка, наследующего от класса-родителя, можно переходить к добавлению новых атрибутов и методов, необходимых для того, чтобы потомок отличался от родителя.

Добавим атрибут, специфический для электромобилей (например, мощность аккумулятора), и метод для вывода информации об этом атрибуте:

class Car:

    --пропуск--

 

class ElectricCar(Car):

    """Представляет аспекты машины, специфические для электромобилей."""

    def __init__(self, make, model, year):

        """

        Инициализирует атрибуты класса-родителя.

        Затем инициализирует атрибуты, специфические для электромобиля.

        """

        super().__init__(make, model, year)

❶         self.battery_size = 40

 

❷     def describe_battery(self):

        """Выводит информацию о мощности аккумулятора."""

        print(f"This car has a {self.battery_size}-kWh battery.")

 

my_leaf = ElectricCar('nissan', 'leaf', 2024)

print(my_leaf.get_descriptive_name())

my_leaf.describe_battery()

Сначала добавляется новый атрибут self.battery_size, которому присваивается исходное значение — скажем, 40 . Он будет присутствовать во всех экземплярах, созданных на основе класса ElectricCar (но не во всяком экземпляре Car). Затем добавляется метод describe_battery(), который выводит информацию об аккумуляторе . При вызове этого метода выдается описание, которое явно относится только к электромобилям:

2024 Nissan Leaf

This car has a 40-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 have a gas tank!")

И если кто-то попытается вызвать метод fill_gas_tank() для электромобиля, то Python игнорирует метод fill_gas_tank() класса Car и выполнит вместо него этот код. Когда вы используете наследование, потомок сохраняет те аспекты родителя, которые вам нужны, и переопределяет все ненужное.

Экземпляры как атрибуты

При моделировании явлений реального мира в программах классы нередко дополняются все бо́льшим количеством подробностей. Списки атрибутов и методов растут, и через какое-то время файлы становятся чрезмерно длинными. В такой ситуации часть одного класса нередко можно записать в виде отдельного класса. Большой код разбивается на меньшие классы, которые работают во взаимодействии друг с другом. Такой подход называется композицией (composition).

Например, при дальнейшей доработке класса ElectricCar может оказаться, что в нем появилось слишком много атрибутов и методов, относящихся к аккумулятору. В таком случае можно остановиться и переместить все эти атрибуты и методы в отдельный класс Battery. Затем экземпляр Battery становится атрибутом класса ElectricCar:

class Car:

    --пропуск--

 

class Battery:

    """Простая модель аккумулятора электромобиля."""

 

❶     def __init__(self, battery_size=40):

        """Инициализирует атрибуты аккумулятора."""

        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_leaf = ElectricCar('nissan', 'leaf', 2024)

print(my_leaf.get_descriptive_name())

my_leaf.battery.describe_battery()

Сначала определяется новый класс Battery, который не наследует ни от одного из других классов. Метод __init__() получает один параметр battery_size, кроме self. Если значение не предоставлено, то этот необязательный параметр задает battery_size значение 40. Метод describe_battery() также перемещен в этот класс .

Затем в класс ElectricCar добавляется атрибут self.battery . Эта строка дает Python указание создать новый экземпляр Battery (со значением battery_size по умолчанию, равным 40, поскольку значение не задано) и сохранить его в атрибуте self.battery. Это будет происходить при каждом вызове __init__(); теперь любой экземпляр ElectricCar будет иметь автоматически создаваемый экземпляр Battery.

Программа создает экземпляр электромобиля и сохраняет его в переменной my_leaf. Когда потребуется вывести описание аккумулятора, необходимо обратиться к атрибуту battery:

my_leaf.battery.describe_battery()

Благодаря этой строке Python получает указание обратиться к экземпляру my_leaf, найти его атрибут battery и вызвать метод describe_battery(), связанный с экземпляром Battery из атрибута.

Результат выглядит так же, как и в предыдущей версии:

2024 Nissan Leaf

This car has a 40-kWh battery.

Казалось бы, новый вариант требует большой дополнительной работы, но теперь аккумулятор можно моделировать, задавая любую степень детализации, при этом не усложняя класс ElectricCar. Добавим в Battery еще один метод, который выводит данные о запасе хода на основании мощности аккумулятора:

class Car:

    --пропуск--

 

class Battery:

    --пропуск--

 

    def get_range(self):

        """Выводит данные о приблизительном запасе хода для аккумулятора."""

        if self.battery_size == 40:

            range = 150

        elif self.battery_size == 65:

            range = 225

 

        print(f"This car can go about {range} miles on a full charge.")

 

class ElectricCar(Car):

    --пропуск--

 

my_leaf = ElectricCar('nissan', 'leaf', 2024)

print(my_leaf.get_descriptive_name())

my_leaf.battery.describe_battery()

❶ my_leaf.battery.get_range()

Новый метод get_range() проводит простой анализ. Если мощность равна 40 кВт/ч, то get_range() устанавливает запас хода 150 миль, а при мощности 65 кВт/ч запас равен 225 милям. Затем программа выводит это значение. Когда вы захотите использовать этот метод, его придется вызывать через атрибут battery .

Результат сообщает данные о запасе хода машины в зависимости от мощности аккумулятора:

2024 Nissan Leaf

This car has a 40-kWh battery.

This car can go approximately 150 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 или 9.4. Подойдет любая версия класса; просто выберите ту, которая вам больше нравится. Добавьте атрибут flavors для хранения списка сортов мороженого. Напишите метод, который выводит этот список. Создайте экземпляр IceCreamStand и вызовите данный метод.

9.7. Администратор. Администратор — особая разновидность пользователя. Напишите класс Admin, наследуемый от класса User из упражнения 9.3 или 9.5. Добавьте атрибут privileges для хранения списка строк вида "разрешено добавлять сообщения", "разрешено удалять пользователей", "разрешено банить пользователей" и т.д. Напишите метод show_privileges() для вывода набора привилегий администратора. Создайте экземпляр Admin и вызовите свой метод.

9.8. Привилегии. Напишите класс Privileges. Класс должен содержать всего один атрибут privileges со списком строк из упражнения 9.7. Переместите метод show_privileges() в этот класс. Создайте экземпляр Privileges как атрибут класса Admin. Создайте новый экземпляр Admin и используйте свой метод для вывода списка привилегий.

9.9. Обновление аккумулятора. Используйте окончательную версию программы electric_car.py из этого раздела. Добавьте в класс Battery метод upgrade_battery(). Он должен проверять размер аккумулятора и устанавливать мощность равной 65, если она имеет другое значение. Создайте экземпляр электромобиля с аккумулятором по умолчанию, вызовите 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):

        """

        Устанавливает на одометре заданное значение.

        При попытке обратной подкрутки изменение отклоняется.

        """

        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', 2024)

print(my_new_car.get_descriptive_name())

 

my_new_car.odometer_reading = 23

my_new_car.read_odometer()

Оператор import дает Python указание открыть модуль car и импортировать класс Car. Теперь мы можем использовать этот класс, как если бы он был определен в данном файле. Результат остается тем же, что и в предыдущей версии:

2024 Audi A4

This car has 23 miles on it.

Импортирование классов повышает эффективность программирования. Представьте, каким длинным получился бы файл этой программы, если бы в него был включен весь класс Car. Перемещая класс в модуль и импортируя этот модуль, вы получаете ту же функциональность, но основной файл программы при этом остается чистым и удобочитаемым. Кроме того, бо́льшая часть логики может храниться в отдельных файлах; когда ваши классы работают как положено, вы можете забыть об этих файлах и сосредоточиться на высокоуровневой логике основной программы.

Хранение нескольких классов в модуле

В одном модуле можно хранить сколько угодно классов, хотя все они должны быть как-то связаны друг с другом. Оба класса, Battery и ElectricCar, используются для представления автомобилей, поэтому мы добавим их в модуль car.py:

car.py

"""Классы для представления машин с бензиновым и электродвигателем."""

 

class Car:

    --пропуск--

 

class Battery:

    """Простая модель аккумулятора электромобиля."""

 

    def __init__(self, battery_size=40):

        """Инициализирует атрибуты аккумулятора."""

        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 == 40:

            range = 150

        elif self.battery_size == 65:

            range = 225

 

        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_leaf = ElectricCar('nissan', 'leaf', 2024)

print(my_leaf.get_descriptive_name())

my_leaf.battery.describe_battery()

my_leaf.battery.get_range()

Программа выводит тот же результат, что и в предыдущем случае, хотя бо́льшая часть ее логики скрыта в модуле:

2024 Nissan Leaf

This car has a 40-kWh battery.

This car can go approximately 150 miles on a full charge.

Импортирование нескольких классов из модуля

В файл программы можно импортировать столько классов, сколько понадобится. Если вы захотите создать обычный автомобиль и электромобиль в одном файле, то потребуется импортировать оба класса, Car и ElectricCar:

my_cars.py

❶ from car import Car, ElectricCar

 

❷ my_mustang = Car('ford', 'mustang', 2024)

print(my_mustang.get_descriptive_name())

❸ my_leaf = ElectricCar('nissan', 'leaf', 2024)

print(my_leaf.get_descriptive_name())

Чтобы импортировать несколько классов из модуля, разделите их имена запятыми . После того как необходимые классы будут импортированы, вы можете создать столько экземпляров каждого класса, сколько потребуется.

В этом примере создается обычный автомобиль Ford Mustang и электромобиль Nissan Leaf :

2024 Ford Mustang

2024 Nissan Leaf

Импортирование модуля целиком

Можно импортировать весь модуль, а потом обращаться к нужным классам, используя точечную запись. Этот способ прост, а полученный код легко читается. Каждый вызов, создающий экземпляр класса, содержит имя модуля, поэтому в программе не будет конфликтов с именами, используемыми в текущем файле.

Вот как выглядят импорт всего модуля car, а затем создание обычного автомобиля и электромобиля:

my_cars.py

❶ import car

 

❷ my_mustang = car.Car('ford', 'mustang', 2024)

print(my_mustang.get_descriptive_name())

 

❸ my_leaf = car.ElectricCar('nissan', 'leaf', 2024)

print(my_leaf.get_descriptive_name())

Сначала импортируется весь модуль car , после чего программа обращается к нужным классам, используя синтаксис имя_модуля.ИмяКласса. Затем снова создаются экземпляр Ford Mustang и экземпляр Nissan Leaf .

Импортирование всех классов из модуля

Для импортирования всех классов из модуля используется следующий синтаксис:

from имя_модуля import *

Применять этот способ не рекомендуется по двум причинам. Прежде всего бывает полезно прочитать операторы import в начале файла и получить четкое представление о том, какие классы используются в программе. А этот способ не позволяет понять, какие классы из модуля нужны программе. Кроме того, возможны конфликты с именами в файле. Если вы случайно импортируете класс с именем, уже присутствующим в файле, то в программе могут возникнуть ошибки, которые трудно выявить. Почему я привожу описание этого способа? Хотя использовать его не рекомендуется, скорее всего, вы встретите его в коде других разработчиков.

Итак, если вам нужно импортировать большое количество классов из модуля, то лучше импортировать весь модуль и воспользоваться синтаксисом имя_модуля.ИмяКласса. Вы не видите перечень всех используемых классов в начале файла, но по крайней мере понятно, где модуль используется в программе. К тому же предотвращаются потенциальные конфликты имен, которые могут возникнуть при импортировании каждого класса в модуле.

Импортирование модуля в модуль

Иногда классы приходится распределять по нескольким модулям, чтобы избежать чрезмерного разрастания одного файла и хранения несвязанных классов в одном модуле. При хранении классов в нескольких модулях может оказаться, что класс из одного модуля зависит от класса из другого модуля. В таких случаях необходимый класс можно импортировать в первый модуль.

Допустим, класс Car хранится в одном модуле, а классы ElectricCar и Battery — в другом. Мы создадим новый модуль electric_car.py (он заменит файл electric_car.py, созданный ранее) и скопируем в него только классы Battery и ElectricCar:

electric_car.py

"""Набор классов для представления электромобилей."""

 

from car import Car

 

class Battery:

    --пропуск--

 

class ElectricCar(Car):

    --пропуск--

Классу ElectricCar необходим доступ к классу-родителю Car, поэтому класс Car импортируется прямо в модуль. Если вы забудете вставить эту команду, то при попытке создания экземпляра ElectricCar произойдет ошибка. Кроме того, необходимо обновить модуль Car, чтобы он содержал только класс Car:

car.py

"""Простая модель автомобиля."""

 

class Car:

    --пропуск--

Теперь вы можете импортировать классы из каждого модуля по отдельности и создать ту разновидность машины, которая вам нужна:

my_cars.py

from car import Car

from electric_car import ElectricCar

 

my_mustang = Car('ford', 'mustang', 2024)

print(my_mustang.get_descriptive_name())

 

my_leaf = ElectricCar('nissan', 'leaf', 2024)

print(my_leaf.get_descriptive_name())

Сначала класс Car импортируется из своего модуля, а класс ElectricCar — из своего. После этого создаются экземпляры обоих разновидностей. Вывод показывает, что экземпляры были созданы правильно:

2024 Ford Mustang

2024 Nissan Leaf

Использование псевдонимов

Как было показано в главе 8, псевдонимы весьма полезны при использовании модулей для организации кода проектов. Кроме того, они позволят импортировать классы.

Для примера возьмем программу, которая должна создать группу экземпляров электрических машин. Многократно вводить (и читать) имя ElectricCar будет очень утомительно. Имени ElectricCar можно назначить псевдоним в операторе import:

from electric_car import ElectricCar as EC

С этого момента вы сможете использовать этот псевдоним каждый раз, когда вам потребуется создать экземпляр ElectricCar:

my_leaf = EC('nissan', 'leaf', 2024)

Вы также можете присвоить псевдоним модулю. Ниже показано, как импортировать модуль electric_car целиком, используя псевдоним:

import electric_car as ec

Теперь вы можете использовать псевдоним этого модуля с полным именем класса:

my_leaf = ec.ElectricCar('nissan', 'leaf', 2024)

Выработка рабочего процесса

Как видите, Python предоставляет много возможностей структурирования кода в крупных проектах. Вы должны знать все эти возможности, чтобы найти эффективные способы организации своих проектов, а также лучше понимать код других разработчиков.

На первых порах постарайтесь поддерживать простую структуру своего кода. Попробуйте поместить весь код в один файл, и только когда все заработает, переместите классы в отдельные модули. Если вам нравится схема взаимодействия между модулями и файлами, то попробуйте сохранить классы в модулях в начале работы над проектом. Найдите подход, при котором у вас получается работоспособный код, и продолжайте работу.

Упражнения

9.10. Импортирование класса Restaurant. Возьмите последнюю версию класса Restaurant и сохраните ее в модуле. Создайте отдельный файл, импортирующий этот класс. Создайте экземпляр Restaurant и вызовите один из методов Restaurant, чтобы показать, что оператор import работает правильно.

9.11. Импортирование класса Admin. Начните с версии класса из упражнения 9.8. Сохраните классы User, Privileges и Admin в одном модуле. Создайте отдельный файл, затем экземпляр Admin и вызовите метод show_privileges(), чтобы показать, что все работает правильно.

9.12. Множественные модули. Сохраните класс User в одном модуле, а классы Privileges и Admin — в другом. В отдельном файле создайте экземпляр Admin и вызовите метод show_privileges(), чтобы показать, что все работает правильно.

Стандартная библиотека Python

Стандартная библиотека Python (Python standard library) представляет собой набор модулей, добавляемых в каждую установленную копию Python. Сейчас вы уже примерно понимаете, как работают классы, и можете начать использовать модули, написанные другими программистами. Чтобы использовать любую функцию или класс из стандартной библиотеки, достаточно добавить простой оператор import в начало файла. Для примера рассмотрим модуль random, который может пригодиться для моделирования многих реальных ситуаций.

В частности, модуль random содержит интересную функцию randint(). Она получает два целочисленных аргумента и возвращает случайно выбранное целое число в диапазоне, определяемом этими двумя числами (включительно).

В следующем примере генерируется случайное число в диапазоне от 1 до 6:

>>> from random import randint

>>> randint(1, 6)

3

Другая полезная функция choice() получает список или кортеж и возвращает случайно выбранный элемент:

>>> from random import choice

>>> players = ['charles', 'martina', 'michael', 'florence', 'eli']

>>> first_up = choice(players)

>>> first_up

'florence'

Модуль random не должен использоваться при создании приложений, связанных с безопасностью, но его возможностей достаточно для применения во многих интересных и увлекательных проектах.

ПРИМЕЧАНИЕ

Модули можно скачивать из внешних источников. Соответствующие примеры встретятся вам в части II, в которой мы будем использовать внешние модули для завершения работы над проектами.

Упражнения

9.13. Игра в кости. Создайте класс Die с одним атрибутом sides, который имеет значение по умолчанию 6. Напишите метод roll_die() для вывода случайного числа от 1 до количества граней на кубике. Создайте экземпляр, представляющий шестигранный кубик, и смоделируйте десять бросков.

Создайте экземпляры, представляющие 10- и 20-гранный кубик. Смоделируйте десять бросков каждого кубика.

9.14. Лотерея. Создайте список или кортеж, содержащий серию из десяти чисел и пяти букв. Случайным образом выберите четыре числа или буквы из списка. Выведите сообщение о том, что билет, содержащий эту комбинацию из четырех цифр или букв, является выигрышным.

9.15. Анализ лотереи. Напишите цикл, который проверяет, насколько сложно выиграть в смоделированной вами лотерее. Создайте список или кортеж my_ticket. Напишите цикл, который продолжает генерировать комбинации до тех пор, пока не выпадет выигрышная комбинация. Выведите сообщение с информацией о том, сколько выполнений цикла понадобилось для получения выигрышной комбинации.

9.16. Модуль недели. Для знакомства со стандартной библиотекой Python отлично подойдет сайт под названием Python Module of the Week. Откройте сайт http://pymotw.com/ и просмотрите оглавление. Найдите модуль, который покажется вам интересным; прочитайте его описание или изучите документацию по модулю random.

Оформление классов

В форматировании классов есть несколько моментов, о которых стоит упомянуть отдельно, — особенно учитывая тот факт, что ваши программы будут усложняться.

Имена классов должны записываться в верблюжьем регистре (CamelCase): первая буква каждого слова записывается в верхнем регистре, слова не разделяются пробелами. Имена экземпляров и модулей записываются в нижнем регистре, слова разделяются символами подчеркивания.

Каждый класс должен иметь строку документации, следующую сразу же за определением класса. В ней вы должны представить краткое описание того, что делает класс, соблюдая те же правила форматирования, которые вы использовали при написании строк документации в функциях. Каждый модуль тоже должен содержать строку документации, в которой описывается возможное применение классов в модуле.

Пустые строки можно использовать для структурирования кода, но злоупотреблять ими не стоит. В классах можно разделять методы одной пустой строкой, а в модулях для разделения классов можно использовать две пустые строки.

Если вам потребуется импортировать модуль из стандартной библиотеки и модуль из библиотеки, написанной вами, то начните с оператора import для модуля стандартной библиотеки. Затем добавьте пустую строку и оператор import для модуля, написанного вами. В программах с несколькими операторами import выполнение этого правила поможет понять, откуда берутся разные модули, использованные в программе.

Резюме

В этой главе вы узнали, как написать собственные классы. Вы научились хранить информацию в классе с помощью атрибутов и наделять свои классы требуемым поведением. Вы узнали, как написать методы __init__(), позволяющие создавать экземпляры ваших классов с нужными значениями атрибутов, и как изменять атрибуты экземпляров напрямую и через методы. Кроме того, вы увидели, что наследование может упростить создание логически связанных классов и экземпляры одного класса могут использоваться как атрибуты другого класса в целях упрощения кода классов.

Вы узнали, что хранение классов в модулях и импортирование необходимых классов в файлы, где они будут использоваться, улучшает организацию проектов. Вы познакомились со стандартной библиотекой Python и рассмотрели пример, основанный на модуле random. Наконец, вы научились оформлять свои классы в соответствии с общепринятыми правилами Python.

В главе 10 вы научитесь работать с файлами и сохранять результаты работы, выполненной в программе. Вдобавок вы познакомитесь с исключениями — экземплярами специального класса Python, предназначенного для передачи информации о возникающих ошибках.

Назад: 8. Функции
Дальше: 10. Файлы и исключения