Классы в Python
Классы в Python
| |
Тематические порталы
|
Объектно-ориентированное программирование является одним из наиболее эффективных подходов для создания программного обеспечения. В объектно-ориентированном программировании создаются классы, которые отражают реальные вещи и ситуации, и соответственно создаются объекты на основе этих классов. Когда вы пишете класс, вы определяете общее поведение, которое может иметь целая категория объектов. Когда вы создаете отдельные объекты из класса, каждый объект автоматически обладает общим поведением или шаблоном; затем вы можете дать каждому объекту любые уникальные характеристики, которые вы пожелаете. Вы будете удивлены, насколько хорошо реальные ситуации могут быть смоделированы с помощью объектно-ориентированного программирования.
Создание объекта из класса называется созданием экземпляра, и вы работаете с экземплярами класса. В этой статье мы создадим классы и экземпляры этих классов, затем укажем тип информации, которая может храниться в экземплярах, и определим действия, которые можно выполнять с этими экземплярами. Далее мы создадим классы, которые расширяют функциональность существующих классов, чтобы схожие классы могли эффективно взаимодействовать с кодом. Потом научимся хранить свои классы в модулях и импортировать классы, написанные другими программистами, в свои собственные программные файлы.
Понимание объектно-ориентированного программирования помогает глубже понять процесс создания программ, игр, приложений. Знание логики поможет писать программы, которые эффективно решают практически любые проблемы, с которыми вы сталкиваетесь. Классы также облегчают жизнь вам и другим программистам, с которыми вам придется работать, когда вы решаете все более сложные задачи. Когда вы и другие программисты пишете код, основанный на одной и той же логике, вы сможете понять работу друг друга.
Создание и использование классов
При помощи классов можно моделировать почти все. Давайте начнем с написания простого класса Dog
, который представит объект или шаблон собаки. Используя этот класс, шаблон можно организовать создание других объектов "собаки" на основе такого шаблона. Прежде чем создать класс, нужно понять, какими параметрами или свойствами обладают собаки? Так, у них всех есть имя и возраст. Мы также знаем, что большинство собак могут исполнять команды "сидеть" или "переворачиваться". Эти части данных (имя и возраст) и эти два поведения ("сидеть" и "переворачиваться") войдут в наш класс Dog
, потому что они являются общими для большинства собак. После написания нашего класса мы будем использовать его для создания отдельных экземпляров, каждый из которых представляет одну конкретную собаку.
Пример создания простого класса
Каждый экземпляр, созданный из класса Dog
, будет хранить имя и возраст, и мы дадим каждой собаке выполнять действия sit(
) и roll_over()
:
class Dog:
"""A simple attempt to model a dog."""
def __init__(self, name, age):
"""Initialise name and age attributes."""
self.name = name.title()
self.age = age
def sit(self):
"""Simulate a dog sitting in response to a command."""
print(self.name + " is now sitting.")
def roll_over(self):
"""Simulate rolling over in response to a command."""
print(self.name + " rolled over!")
Код сначала может казаться непонятным, но мы сейчас всё поясним. Такая стандартная структура будет использоваться достаточно часто, поэтому нужно привыкнуть к ней. И так, в самом начале мы определяем класс с именем Dog
. Так принято в Python, что все классы начинаются с заглавной буквы. Затем в тройных кавычках можно комментировать, что означает последующий код, так как это информация будет включена в документацию к программе для тех, кто может работать с кодом в последующем.
Метод __init__()
Функция, которая является частью класса, называется методом. Все, что вы узнали о функциях, относится и к методам; единственное практическое отличие на данный момент - это то, как мы будем вызывать методы. __init __()
в коде - это специальный метод, который Python запускает автоматически всякий раз, когда мы создаем новый экземпляр на основе класса Dog
. Этот метод выделяется двойными подчёркиваниями.
Мы определяем метод __init __(
) для трех параметров: "self", "name" и "age". Параметр "self" является обязательным в определении метода и должен предшествовать другим параметрам. Он должен быть включен в определение, потому что когда Python позже вызывает этот метод __init __()
(для создания экземпляра Dog
), вызов метода автоматически передаст аргумент "self". Каждый вызов метода, связанный с классом, автоматически передает себя, что является ссылкой на сам экземпляр; он дает экземпляру доступ к атрибутам и методам в классе. Когда мы создаем экземпляр Dog
, Python вызывает метод __init __()
из класса Dog
. Мы передадим классу Dog()
имя и возраст в качестве аргументов; "self" передается автоматически, поэтому нам не нужно его передавать. Всякий раз, когда мы хотим создать экземпляр из класса Dog
, мы предоставим значения для двух параметров - имени и возраста.
Две переменные, написанные в коде, имеют префикс "self". Любая переменная с префиксом "self" доступна каждому методу в классе, и мы также сможем получить доступ к этим переменным через любой экземпляр, созданный из класса. self.name = name.title()
принимает значение, сохраненное в имени параметра, и сохраняет его в имени переменной, которая затем присоединяется к создаваемому экземпляру. При этом мы используем функцию title()
, которая выводит все значения в переменной с заглавной буквы. Тот же процесс происходит с self.age = age
. Переменные, доступные через такие экземпляры, называются атрибутами.
Класс Dog
имеет два других метода в нашем примере: sit()
и roll_over()
. Так как эти методы не нуждаются в дополнительной информации, такие как имя или возраст, мы просто определяем их как один параметр - self. Экземпляры, которые мы создадим позже, будут иметь доступ к этим методам. Другими словами, мы присваиваем объекту действия для собаки: садиться и переворачиваться. В данном примере методы sit()
и roll_over()
просто выводят сообщение о том, что собака села или переворачивается. Имейте в виду, что концепция может быть расширена до реалистичных ситуаций: если бы этот класс был частью реальной компьютерной игры, эти методы содержали бы код, заставляющий анимированную собаку садиться и переворачиваться.
Создание экземпляра из класса
Думайте о классе как о наборе инструкций о том, как создать экземпляр. Класс Dog
- это набор инструкций, который сообщает Python, как создавать отдельные экземпляры, представляющие конкретных собак Давайте сделаем пример, представляющий конкретную собаку. После кода определяющий класс Dog
вносим следующий код:
my_dog = Dog('willie', 6)
print("My dog's name is " + my_dog.name + ".")
print("My dog is " + str(my_dog.age) + " years old.")
Класс Dog
, который мы здесь используем, - это тот, который мы только что написали в предыдущем примере. Сначала мы сообщаем Python создать собаку с именем "willie" и возрастом 6 лет. Когда Python читает эту строку, он вызывает метод __init __()
в Dog
с аргументами "willie" и "6". Метод __init __()
создает экземпляр, представляющий эту конкретную собаку и устанавливающий атрибуты имени и возраста с использованием предоставленных нами значений. Метод __init __ ()
не имеет явного оператора return, но Python автоматически возвращает экземпляр, представляющий эту собаку. Мы храним этот экземпляр в переменной my_dog
. В нашем примере название заглавное Dog
, относится к классу, а строчное имя, например, my_dog
, относится к одному экземпляру, созданному из класса.
Доступ к атрибутам
Для доступа к атрибутам экземпляра используется точечная запись. Так в коде мы получаем доступ к значению имени атрибута my_dog
, написав:
my_dog.name
Точечная нотация часто используется в Python. Этот синтаксис демонстрирует, как Python находит значение атрибута. Здесь Python использует экземпляр my_dog
и затем находит атрибут "name", связанное с my_dog
. Это тот же атрибут, который называется self.name
в классе Dog
. Затем в коде мы используем такой же подход для работы с атрибутом age
(возраст). Во втором выводе значение атрибута my_dog.age
преобразует значение в строку. Вывод представляет собой сводку того, что мы знаем о my_dog
:
My dog's name is Willie. My dog is 6 years old.
Вызов методов
После того, как мы создаем экземпляр из класса Dog
, мы можем использовать точечную запись для вызова любого метода, созданного в этом классе. Давайте заставим условно нашу собаку сесть и перевернуться:
my_dog = Dog('willie', 6)
my_dog.sit()
my_dog.roll_over()
Чтобы вызвать метод, нужно задать названия экземпляра (в данном случае my_dog
и метод, который вы хотите вызвать, через точку. Когда Python читает my_dog.sit()
, он ищет метод sit()
в классе Dog
и запускает этот код. Python интерпретирует строку my_dog.roll_over()
таким же образом. Теперь наша виртуальная собака делает то, что мы сообщаем ей:
Willie is now sitting. Willie rolled over!
Такой синтаксис довольно полезен. Когда атрибутам и методам присвоены соответствующие описательные имена, такие как name
, age
, sit()
и roll_over()
, мы можем легко определить, что должен делать блок кода.
Создание нескольких экземпляров
Вы можете создать столько экземпляров из класса, сколько нужно. Давайте создадим вторую собаку с именем your_dog
:
my_dog = Dog('willie', 6)
your_dog = Dog('lucy', 3)
print("My dog's name is " + my_dog.name.title() + ".")
print("My dog is " + str(my_dog.age) + " years old.")
print("\nYour dog's name is " + your_dog.name.title() + ".")
print("Your dog is " + str(your_dog.age) + " years old.")
your_dog.sit()
my_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
. Можно создать столько экземпляров из одного класса, сколько нужно, при условии, что даете каждому экземпляру уникальное имя переменной или оно занимает уникальное место в списке или словаре.
Работа с классами и экземплярами
Можно использовать классы для моделирования многих реальных ситуаций. После написания класса вы будете проводить большую часть времени, работая с экземплярами, созданными из этого класса. Одна из первых задач, которую нужно будет выполнить, - изменить атрибуты, связанные с конкретным экземпляром. Можно изменить атрибуты экземпляра напрямую или написать методы, которые обновляют атрибуты определенным образом.
Класс автомобиля
Давайте напишем новый класс, представляющий автомобиль. Наш класс будет хранить информацию о типе автомобиля, с которым мы работаем, и у него будет метод, который сообщает эту информацию:
class Car:
def __init__(self, make, model, year):
"""Initialize attributes to describe a car."""
self.make = make
self.model = model
self.year = year
def get_descriptive_name(self):
"""Return a neatly formatted descriptive name."""
long_name = str(self.year) + ' ' + self.make + ' ' + self.model
return long_name.title()
my_new_car = Car('audi', 'a4', 2016)
print(my_new_car.get_descriptive_name())
В начале класса "Car" мы сначала определяем метод __init __()
с параметром self
, как мы делали это раньше с нашим классом c. Затем этому классу задаём три других параметра: марка, модель и год автомобиля. Метод __init __(
) принимает эти параметры и сохраняет их в атрибутах, которые будут связаны с экземплярами, созданными из этого класса. Когда мы создаем новый экземпляр "Car", нам нужно указать марку, модель и год для нашего автомобиля.
Затем мы определяем метод get_descriptive_name(
), который помещает год, марку и модель автомобиля в одну строку, описывающий автомобиль. Это избавит нас от необходимости печатать значение каждого атрибута отдельно. Для работы со значениями атрибутов в этом методе мы используем self.make
, self.model
и self.yea
r. Потом создаём экземпляр из класса 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):
"""Initialize attributes to describe a car."""
self.make = make
self.model = model
self.year = year
self.odometer_reading = 0
def get_descriptive_name(self):
--snip--
def read_odometer(self):
"""Print a statement showing the car's mileage."""
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()
На этот раз, когда Python вызывает метод __init __(
) для создания нового экземпляра, он сохраняет значения make
, model
и year
в виде таких атрибутов, как это было в предыдущем примере. Затем Python создает новый атрибут с именем odometer_reading
и устанавливает его начальное значение равным "0". У нас также есть новый метод read_odometer()
, который облегчает считывание пробега автомобиля. Так пробег нашего виртуального автомобиля равняется нулю:
2016 Audi A4 This car has 0 miles on it.
Но не все автомобили продаются с нулевым пробегом, поэтому нам нужен способ изменить значение этого атрибута.
Изменение значений атрибутов
Можно изменить значение атрибута тремя способами: вы можете изменить значение непосредственно через экземпляр, установить значение с помощью метода или увеличить значение (добавить к нему определенное число) с помощью метода. Давайте посмотрим на каждый из этих подходов.
Прямое изменение значения атрибута
Самый простой способ изменить значение атрибута - получить доступ к атрибуту напрямую через экземпляр. Здесь мы устанавливаем показания счётчика на 23:
class Car():
--snip--
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.
Иногда необходимо получить доступ к атрибутам напрямую, но иногда нужно написать метод, который обновит это значение.
Изменение значения атрибута с помощью метода
Иногда нужно увеличить значение атрибута на определенную величину, а не устанавливать совершенно новое значение. Предположим, мы покупаем подержанный автомобиль c пробегаем 100 километров между моментом покупки и моментом регистрации. Есть метод, который позволяет нам передать это значение и добавить его к показаниям счётчика:
class Car():
--snip--
def update_odometer(self, mileage):
"""Set the odometer reading to the given value."""
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" в качестве аргумента (соответствует параметру пробега в определении метода). Он устанавливает показание счётчика на 23, и получаем результат:
2016 Audi A4 This car has 23 miles on it.
Можно расширить метод update_odometer()
, чтобы не выполнять дополнительную процесс каждый раз, когда изменяется показание счётчика. Давайте добавим немного логики, чтобы убедиться, чтобы никто не смог откатить показания счётчика:
class Car():
--snip--
def update_odometer(self, mileage):
"""
Set the odometer reading to the given value.
Reject the change if it attempts to roll the odometer back.
"""
if mileage >= self.odometer_reading:
self.odometer_reading = mileage
else:
print("You can't roll back an odometer!")
Теперь метод update_odometer() проверяет, имеет ли новое значение новое значение, прежде чем изменять этот атрибут. Если новое значение больше или равно существующему значению "self.odometer_reading", то можно обновить показания счётчика до нового показания. Если новое значение меньше существующего, то получите предупреждение о том, что нельзя откатить показания счётчика.
Увеличение значения атрибута с помощью метода
Иногда необходимо увеличить значение атрибута на определенную величину, а не устанавливать новое значение для него. Скажем, мы покупаем подержанный автомобиль и проезжаем 100 километров. Вот метод, который позволяет нам передать это значение и добавляет его к показанию счётчика:
class Car:
-- snip --
def update_odometer(self, mileage):
-- snip --
def increment_odometer(self, miles):
"""Add the given amount to the odometer reading."""
self.odometer_reading += miles
my_new_car = Car('audi', 'a4', 2016)
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()
и передаём значение "100". добавить 100 миль. И соответственно получаем на выходе:
2013 Subaru Outback This car has 23500 miles on it. This car has 23600 miles on it.
Можно легко изменить этот метод, чтобы не принимать отрицательные значения, чтобы никто не мог использовать эту функцию для отката счётчика. Теперь Вы знаете, как использовать подобные методы для обновления значений в той или иной программе, игре, приложении.
Унаследование
При создании классов в Python не всегда нужно их делать заново, когда есть похожие параметры объекта, то можно использовать так называемое унаследование.
Когда один класс наследуется от другого, он автоматически принимает все атрибуты и методы первого класса. Исходный класс называется родительским классом, а новый класс - дочерним. Дочерний класс наследует все атрибуты и методы своего родительского класса, но также может определять новые собственные атрибуты и методы.
Метод __init __ () для дочернего класса
Первая задача при создании экземпляра из дочернего класса - присвоить значения всем атрибутам в родительском классе. Для этого методу __init __ ()
дочернего класса требуется помощь его родительского класса.
В качестве примера давайте смоделируем электромобиль. Электромобиль - это просто особый вид автомобиля, поэтому мы можем добавить наш новый класс ElectricCar
на базе класса Car
, который мы написали ранее. Тогда нам останется только написать код для атрибутов и поведения, характерных для электромобилей.
Давайте начнем с создания простой версии класса ElectricCar
, который делает все, что делает класс Car
:
class Car:
def __init__(self, make, model, year):
"""Initialize attributes to describe a car."""
self.make = make
self.model = model
self.year = year
self.odometer_reading = 0 # Preset parameter
def get_descriptive_name(self):
"""Return a neatly formatted descriptive name."""
long_name = str(self.year) + ' ' + self.make + ' ' + self.model
return long_name.title()
def read_odometer(self):
"""Print a statement showing the car's mileage."""
print("This car has " + str(self.odometer_reading) + " miles on it.")
def update_odometer(self, mileage):
"""Set the odometer reading to the given value.
Reject the change if it attempts to roll the odometer back."""
if mileage >= self.odometer_reading:
self.odometer_reading = mileage
else:
print("You can't roll back an odometer!")
def increment_odometer(self, miles):
"""Add the given amount to the odometer reading."""
if self.odometer_reading - miles >= self.odometer_reading:
print('You cannot use negative values')
else:
self.odometer_reading += miles
class ElectricCar(Car):
"""Represent aspects of a car, specific to electric vehicles."""
def __init__(self, make, model, year):
"""Initialize attributes of the parent class."""
super().__init__(make, model, year)
my_tesla = ElectricCar('tesla', 'model s', 2016)
print(my_tesla.get_descriptive_name())
На 33-й строке мы начинаем с класса Car
. Когда вы создаете дочерний класс, он должен быть частью родительского класса и необходимо его указывать после кода дочернего класса. На 35-й строке мы задаём дочерний класс ElectricCar
. Имя родительского класса должно быть заключено в круглые скобки в определении дочернего класса. Метод __init __()
принимает информацию, необходимую для создания экземпляра Car
.
Функция super()
на 37-й строке - это специальная функция, которая помогает Python устанавливать связи между родительским и дочерним классами. Эта строка указывает вызывать метод __init __()
из родительского класса ElectricCar
, который передает экземпляру ElectricCar
все атрибуты его родительского класса. Название super происходит от соглашения о назывании родительского класса суперклассом, а дочернего класса - подклассом.
Далее мы проверяем, правильно ли срабатывает унаследование, создавая класс электромобиля с теме же данными, которые мы задавали при создании класса обычного автомобиля. Потом мы создаем экземпляр класса ElectricCar и сохраняем его в переменной my_tesla
(39-я строка). Эта строка вызывает метод __init __()
, заданный в ElectricCar, определенный в родительском классе Car
. И как результат мы добавляем аргументы «Tesla», «Model S» и год выпуска автомобиля "2016".
Помимо __init __(
), пока не заданы атрибуты или методы, специально для электромобиля. На данный момент мы просто убеждаемся, что электромобиль имеет соответствующее поведение класса автомобиля:
2016 Tesla Model S
Экземпляр ElectricCar работает так же, как и экземпляр Car, поэтому теперь можно приступить к определению атрибутов и методов, специфичных для электромобилей.
Определение атрибутов и методов для дочернего класса
Когда есть дочерний класс, который наследуется от родительского класса, можно добавить любые новые атрибуты и методы отличные от дочернего родительского класса.
Давайте добавим атрибут, который присущ электромобилям (например, аккумулятор), и метод вывода по этому атрибуту. Мы сохраним размер батареи и напишем метод, выводящий описание батареи:
class Car():
--snip--
class ElectricCar(Car):
"""Represent aspects of a car, specific to electric vehicles."""
def __init__(self, make, model, year):
"""
Initialize attributes of the parent class.
Then initialize attributes specific to an electric car.
"""
super().__init__(make, model, year)
self.battery_size = 70
def describe_battery(self):
"""Print a statement describing the battery size."""
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()
На 13 строчке мы добавляем новую переменную self.battery_size
, содержащую значение заряда батареи. Этот атрибут будет связан со всеми экземплярами, созданными из класса "ElectricCar", но не будет связан ни с одним экземпляром класса Car
. Мы также добавим метод description_battery()
- строка 15, который выводит информацию о батарее. Запустив программу, мы получим следующий результат:
2016 Tesla Model S This car has a 70-kWh battery.
Нет предела тому, сколько и как можно редактировать класс ElectricCar
, добавляя неограниченное количество атрибутов классу Car
(не ограничиваясь только электромобилем). Таким образом используя данный класс у разработчика есть доступ ко всему функционалу данного класса.
Переопределение методов из родительского класса
Вы можете переписать любой метод из родительского класса в дочернем классе, заменив его новым аналогичным методом. Это называется переопределением. При создании экземпляра дочернего класса Python автоматически использует переопределенный метод из дочернего класса, а не метод из родительского класса.
Давайте переопределим метод describe_battery()
в классе ElectricCar
для получения более подробного описания:
class ElectricCar(Car): --snip--
my_tesla = ElectricCar('tesla', 'model s', 2016) my_tesla.describe_battery()
В переопределенном методе describe_battery() мы сначала выводим информацию о размере батареи, как и раньше. Затем добавлена новая логика для определения примерного запаса хода автомобиля на одном заряде в зависимости от размера батареи. При запуске кода с новым методом describe_battery() будет выведено:
This car has a 70-kWh battery pack. This car can go approximately 240 miles on a full charge.
Таким образом, переопределение методов позволяет заменить поведение родительского класса в дочернем классе более специализированной версией.