Ключевые вопросы параграфа
- Что такое класс и объект в Python и как они связаны между собой?
- Как задать свойства и поведение объектов с помощью атрибутов и методов?
- Как работает метод
__init__()
и зачем в методах используетсяself
? - Что такое инкапсуляция и как она помогает защитить внутреннее состояние объекта?
- Как реализовать полиморфизм в Python и в чём суть подхода «утиная типизация»?
Что такое класс и объект в Python и как они связаны между собой
Предлагаем начать не с теории, а с практики. Напишем программу, которая будет моделировать объекты класса «Автомобиль». При этом важно заранее определить, какие свойства и действия должны быть у этих объектов.
Пусть каждый автомобиль имеет собственный цвет. Его двигатель можно запустить, если в баке есть топливо, и заглушить в любой момент. Автомобиль должен проезжать определённое расстояние — но только при условии, что двигатель включён, а топлива хватает на заданный путь. После каждой поездки запас топлива уменьшается. Также автомобиль можно заправить до полного бака.
Выделим ключевые характеристики объектов: цвет, средний расход топлива, объём бака, запас топлива, пробег. А также — действия: запуск и остановка двигателя, поездка, заправка.
На текущем этапе мы можем описать такой объект с помощью словаря и управлять им через отдельные функции. Ниже — пример реализации:
1def create_car(color, consumption, tank_volume, mileage=0):
2 return {
3 "color": color,
4 "consumption": consumption,
5 "tank_volume": tank_volume,
6 "reserve": tank_volume,
7 "mileage": mileage,
8 "engine_on": False
9 }
10
11def start_engine(car):
12 if not car["engine_on"] and car["reserve"] > 0:
13 car["engine_on"] = True
14 return "Двигатель запущен."
15 return "Двигатель уже был запущен."
16
17def stop_engine(car):
18 if car["engine_on"]:
19 car["engine_on"] = False
20 return "Двигатель остановлен."
21 return "Двигатель уже был остановлен."
22
23def drive(car, distance):
24 if not car["engine_on"]:
25 return "Двигатель не запущен."
26 if car["reserve"] / car["consumption"] * 100 < distance:
27 return "Малый запас топлива."
28 car["mileage"] += distance
29 car["reserve"] -= distance / 100 * car["consumption"]
30 return f"Проехали {distance} км. Остаток топлива: {car['reserve']} л."
31
32def refuel(car):
33 car["reserve"] = car["tank_volume"]
34
35def get_mileage(car):
36 return f"Пробег {car['mileage']} км."
37
38def get_reserve(car):
39 return f"Запас топлива {car['reserve']} л."
40
41car_1 = create_car(color="black", consumption=10, tank_volume=55)
42
43print(start_engine(car_1))
44print(drive(car_1, 100))
45print(drive(car_1, 100))
46print(drive(car_1, 100))
47print(drive(car_1, 300))
48print(get_mileage(car_1))
49print(get_reserve(car_1))
50print(stop_engine(car_1))
51print(drive(car_1, 100))
Результат работы программы может выглядеть так:
Двигатель запущен. Проехали 100 км. Остаток топлива: 45.0 л. Проехали 100 км. Остаток топлива: 35.0 л. Проехали 100 км. Остаток топлива: 25.0 л. Малый запас топлива. Пробег 300 км. Запас топлива 25.0 л. Двигатель остановлен. Двигатель не запущен.
В этом примере все действия над объектом реализованы через отдельные функции, которые получают словарь с данными. Такой стиль называется процедурным программированием. Он был основным в ранней истории языков программирования и до сих пор подходит для простых задач.
Однако с ростом сложности проекта этот подход становится громоздким. Код начинает дублироваться, логика — расползаться по множеству функций, а структура программы — усложняться и становиться уязвимой к ошибкам.
Чтобы решить эти проблемы, в Python используют объектно-ориентированное программирование (ООП). Этот подход позволяет объединять данные и связанные с ними действия в единую структуру — класс.
Класс
— это шаблон (или описание), по которому создаются объекты. В классе задаются свойства (атрибуты) и действия (методы), общие для всех объектов этого типа.
Объект
— это конкретный экземпляр класса, обладающий собственными значениями атрибутов. Объект знает, к какому классу он принадлежит, и может выполнять описанные в нём действия.
Python построен на объектной модели. Это значит, что практически всё в нём — объекты, созданные на основе классов. Например, число 1
— не просто значение, а объект встроенного класса int
. Убедимся в этом:
1print(type(1))
2
3# Вывод программы:
4# <class 'int'>
Всё в Python — объекты: числа, строки, функции, списки и даже модули. А самое важное — вы можете создавать свои собственные классы для описания любых сущностей, от автомобилей до пользователей и заказов.
Дальше мы увидим, как именно создаются классы и как они позволяют сделать код более чистым, гибким и надёжным.
Как задать свойства и поведение объектов с помощью атрибутов и методов
Создание собственных классов — основа объектно-ориентированного программирования. С их помощью можно задать как структуру объекта
(его свойства), так и поведение
(набор доступных действий).
Синтаксис определения класса в Python выглядит так:
1class <ИмяКласса>:
2 <описание класса>
Согласно стандарту оформления PEP 8, имя класса записывается в стиле CapWords
— каждое слово начинается с заглавной буквы и не содержит подчёркиваний.
Например: Car
, ElectricVehicle
, UserProfile
.
Давайте перепишем наш пример с автомобилем, используя классы. Начнём с самого простого — создадим пустой класс Car
, в котором пока ничего не будет происходить. В этом случае используется инструкция-заглушка pass
:
1class Car:
2 pass
Теперь разберёмся, как задать свойства и поведение объектов, создаваемых на основе этого класса.
Атрибуты — свойства объекта
Атрибутами
называются переменные, которые хранятся внутри объекта. Они описывают состояние конкретного экземпляра класса — например, цвет, пробег, запас топлива и т. д.
Атрибут можно создать вручную после создания объекта:
1<имя_объекта>.<имя_атрибута> = <значение>
Пример:
car.color = "чёрный"
Методы: как описать действия объекта
Методы
— это функции, определённые внутри класса. Они описывают поведение объекта, например запуск двигателя, поездку или заправку.
Метод объявляется с помощью ключевого слова def
, а первым его параметром всегда идёт self
. Это ссылка на конкретный объект, для которого вызывается метод:
1def start_engine(self):
2 ...
С помощью self
метод может обращаться к атрибутам и другим методам того же объекта. Без self
объект не сможет «узнать» собственные свойства.
Как работает метод init() и зачем в методах используется self
Во всех классах Python можно определить специальный метод __init__()
. Он автоматически вызывается при создании нового объекта и отвечает за начальную инициализацию его атрибутов.
Вернёмся к примеру с автомобилем и добавим метод __init__()
в класс Car, чтобы задать значения свойств при создании:
1class Car:
2
3 def __init__(self, color, consumption, tank_volume, mileage=0):
4 self.color = color
5 self.consumption = consumption
6 self.tank_volume = tank_volume
7 self.reserve = tank_volume
8 self.mileage = mileage
9 self.engine_on = False
Здесь мы определили, что каждый объект класса Car
будет обладать следующими атрибутами: цветом (color
), средним расходом топлива (consumption
), объёмом бака (tank_volume
), текущим запасом топлива (reserve
), пробегом (mileage
) и состоянием двигателя (engine_on). Значения этих атрибутов задаются при создании объекта.
Для создания объекта на основе класса используют следующую запись:
1<имя_объекта> = <ИмяКласса>(<аргументы метода __init__()>)
Пример:
1car_1 = Car(color="black", consumption=10, tank_volume=55)
Обратите внимание: теперь всё, что относится к автомобилю, собрано внутри одного объекта. Такой подход делает код не только короче, но и понятнее, масштабируемее и легче в поддержке.
Что такое инкапсуляция и как она помогает защитить внутреннее состояние объекта
Теперь, когда мы описали свойства и поведение класса, наш код стало легче читать. Мы видим, что создаётся объект определённого типа (Car
), а не просто передаётся словарь в набор функций. Вся логика работы с автомобилем теперь сосредоточена внутри самого класса.
Опишем с помощью методов, какие действия может выполнять объект Car
. Напомним, что, согласно PEP 8, между методами класса нужно оставлять по одной пустой строке:
1class Car:
2
3 def __init__(self, color, consumption, tank_volume, mileage=0):
4 self.color = color
5 self.consumption = consumption
6 self.tank_volume = tank_volume
7 self.reserve = tank_volume
8 self.mileage = mileage
9 self.engine_on = False
10
11 def start_engine(self):
12 if not self.engine_on and self.reserve > 0:
13 self.engine_on = True
14 return "Двигатель запущен."
15 return "Двигатель уже был запущен."
16
17 def stop_engine(self):
18 if self.engine_on:
19 self.engine_on = False
20 return "Двигатель остановлен."
21 return "Двигатель уже был остановлен."
22
23 def drive(self, distance):
24 if not self.engine_on:
25 return "Двигатель не запущен."
26 if self.reserve / self.consumption * 100 < distance:
27 return "Малый запас топлива."
28 self.mileage += distance
29 self.reserve -= distance / 100 * self.consumption
30 return f"Проехали {distance} км. Остаток топлива: {self.reserve} л."
31
32 def refuel(self):
33 self.reserve = self.tank_volume
34
35 def get_mileage(self):
36 return self.mileage
37
38 def get_reserve(self):
39 return self.reserve
40
41
42car_1 = Car(color="black", consumption=10, tank_volume=55)
43print(car_1.start_engine())
44print(car_1.drive(100))
45print(car_1.drive(100))
46print(car_1.drive(100))
47print(car_1.drive(300))
48print(f"Пробег {car_1.get_mileage()} км.")
49print(f"Запас топлива {car_1.get_reserve()} л.")
50print(car_1.stop_engine())
51print(car_1.drive(100))
Вывод программы:
Двигатель запущен. Проехали 100 км. Остаток топлива: 45.0 л. Проехали 100 км. Остаток топлива: 35.0 л. Проехали 100 км. Остаток топлива: 25.0 л. Малый запас топлива. Пробег 300 км. Запас топлива 25.0 л. Двигатель остановлен. Двигатель не запущен.
Обратите внимание: все действия с объектом происходят через методы. Мы не изменяем значения атрибутов напрямую, а работаем с объектом через предусмотренный «интерфейс» — это и есть принцип инкапсуляции.
Инкапсуляция означает, что внутреннее состояние объекта скрыто от внешнего кода и доступно только через методы. Такой подход помогает:
- избежать случайных ошибок при прямом изменении данных;
- сохранить корректность логики внутри класса;
- упростить отладку и поддержку программы.
Посмотрим, что произойдёт, если проигнорировать этот принцип и напрямую изменить атрибут mileage
, не выполняя никаких проверок:
1car_1 = Car(color="black", consumption=10, tank_volume=55)
2car_1.mileage = 1000
3print(f"Пробег {car_1.get_mileage()} км.")
4print(f"Запас топлива {car_1.get_reserve()} л.")
5
6# Вывод программы:
7# Пробег 1000 км.
8# Запас топлива 55 л.
Как видите, объект «проехал» 1000 км, хотя двигатель не запускался и топливо не расходовалось. Это делает данные недостоверными и нарушает логику работы модели. Именно от таких ситуаций инкапсуляция и защищает.
Позже мы узнаем, как ещё сильнее ограничивать доступ к данным с помощью специальных соглашений и модификаторов доступа.
Как реализовать полиморфизм в Python и в чём суть подхода «утиная типизация»
В предыдущем примере мы описали класс Car
. Теперь давайте создадим ещё один класс — для электромобилей. Он будет отличаться от бензинового аналога тем, что вместо топливного бака у него есть аккумуляторная батарея:
1class ElectricCar:
2
3 def __init__(self, color, consumption, bat_capacity, mileage=0):
4 self.color = color
5 self.consumption = consumption
6 self.bat_capacity = bat_capacity
7 self.reserve = bat_capacity
8 self.mileage = mileage
9 self.engine_on = False
10
11 def start_engine(self):
12 if not self.engine_on and self.reserve > 0:
13 self.engine_on = True
14 return "Двигатель запущен."
15 return "Двигатель уже был запущен."
16
17 def stop_engine(self):
18 if self.engine_on:
19 self.engine_on = False
20 return "Двигатель остановлен."
21 return "Двигатель уже был остановлен."
22
23 def drive(self, distance):
24 if not self.engine_on:
25 return "Двигатель не запущен."
26 if self.reserve / self.consumption * 100 < distance:
27 return "Малый заряд батареи."
28 self.mileage += distance
29 self.reserve -= distance / 100 * self.consumption
30 return f"Проехали {distance} км. Остаток заряда: {self.reserve} кВт*ч."
31
32 def recharge(self):
33 self.reserve = self.bat_capacity
34
35 def get_mileage(self):
36 return self.mileage
37
38 def get_reserve(self):
39 return self.reserve
Допустим, мы хотим написать функцию range_reserve()
, которая рассчитывает запас хода автомобиля. Она должна одинаково работать и с Car
, и с ElectricCar
. Такое поведение называется полиморфизмом — когда один и тот же интерфейс (в нашем случае — функция) работает с объектами разных типов.
Чтобы это было возможно, в обоих классах должны быть методы с одинаковыми именами и сигнатурами. Для расчёта запаса хода нам понадобится ещё один метод — get_consumption()
:
1def range_reserve(car):
2 return car.get_reserve() / car.get_consumption() * 100
При этом Python не требует, чтобы оба объекта были наследниками одного и того же класса. Важно лишь, чтобы объект поддерживал нужные методы, — именно это поведение называется утиная типизация
.
Это поведение получило своё название от шутливого выражения:
«Если что-то выглядит как утка, плавает как утка и крякает как утка — значит, это утка»
Применительно к Python это значит: если объект умеет делать всё, что от него ожидается, мы можем с ним работать — даже не проверяя его тип с помощью isinstance
.
Вот полная программа с двумя классами и функцией range_reserve()
:
1class Car:
2
3 def __init__(self, color, consumption, tank_volume, mileage=0):
4 self.color = color
5 self.consumption = consumption
6 self.tank_volume = tank_volume
7 self.reserve = tank_volume
8 self.mileage = mileage
9 self.engine_on = False
10
11 def start_engine(self):
12 if not self.engine_on and self.reserve > 0:
13 self.engine_on = True
14 return "Двигатель запущен."
15 return "Двигатель уже был запущен."
16
17 def stop_engine(self):
18 if self.engine_on:
19 self.engine_on = False
20 return "Двигатель остановлен."
21 return "Двигатель уже был остановлен."
22
23 def drive(self, distance):
24 if not self.engine_on:
25 return "Двигатель не запущен."
26 if self.reserve / self.consumption * 100 < distance:
27 return "Малый запас топлива."
28 self.mileage += distance
29 self.reserve -= distance / 100 * self.consumption
30 return f"Проехали {distance} км. Остаток топлива: {self.reserve} л."
31
32 def refuel(self):
33 self.reserve = self.tank_volume
34
35 def get_mileage(self):
36 return self.mileage
37
38 def get_reserve(self):
39 return self.reserve
40
41 def get_consumption(self):
42 return self.consumption
43
44
45class ElectricCar:
46
47 def __init__(self, color, consumption, bat_capacity, mileage=0):
48 self.color = color
49 self.consumption = consumption
50 self.bat_capacity = bat_capacity
51 self.reserve = bat_capacity
52 self.mileage = mileage
53 self.engine_on = False
54
55 def start_engine(self):
56 if not self.engine_on and self.reserve > 0:
57 self.engine_on = True
58 return "Двигатель запущен."
59 return "Двигатель уже был запущен."
60
61 def stop_engine(self):
62 if self.engine_on:
63 self.engine_on = False
64 return "Двигатель остановлен."
65 return "Двигатель уже был остановлен."
66
67 def drive(self, distance):
68 if not self.engine_on:
69 return "Двигатель не запущен."
70 if self.reserve / self.consumption * 100 < distance:
71 return "Малый заряд батареи."
72 self.mileage += distance
73 self.reserve -= distance / 100 * self.consumption
74 return f"Проехали {distance} км. Остаток заряда: {self.reserve} кВт*ч."
75
76 def recharge(self):
77 self.reserve = self.bat_capacity
78
79 def get_mileage(self):
80 return self.mileage
81
82 def get_reserve(self):
83 return self.reserve
84
85 def get_consumption(self):
86 return self.consumption
87
88
89def range_reserve(car):
90 return car.get_reserve() / car.get_consumption() * 100
91
92
93car_1 = Car(color="black", consumption=10, tank_volume=55)
94car_2 = ElectricCar(color="white", consumption=15, bat_capacity=90)
95print(f"Запас хода: {range_reserve(car_1)} км.")
96print(f"Запас хода: {range_reserve(car_2)} км.")
Вывод программы будет таким:
Запас хода: 550.0 км. Запас хода: 600.0 км.
Благодаря тому, что у классов одинаковый интерфейс, мы легко применили одну и ту же функцию к объектам разной природы — без единой строчки лишней логики. Это и есть сила полиморфизма и утиных типов.
✅ У вас получилось разобраться с ООП?
Что дальше
Изучив параграф, вы научились создавать собственные классы и описывать поведение объектов с помощью методов. Вы поняли, как задаются свойства через атрибуты, как работает метод __init__()
и зачем в методах используется self
.
Вы узнали, что такое инкапсуляция и как она помогает защитить внутреннее состояние объектов от неконтролируемых изменений. А ещё — познакомились с полиморфизмом и подходом «утиная типизация», который делает Python особенно гибким.
Далее мы расширим эти знания: научимся использовать наследование, переопределять методы и работать со специальными («волшебными») методами классов. Всё это позволит строить более мощные и выразительные структуры в вашем коде.
А пока вы не ушли дальше — закрепите материал на практике:
- Отметьте, что урок прочитан, при помощи кнопки ниже.
- Пройдите мини-квиз, чтобы проверить, насколько хорошо вы усвоили тему.
- Перейдите к задачам этого параграфа и потренируйтесь.
- Перед этим загляните в короткий гайд о том, как работает система проверки.
Хотите обсудить, задать вопрос или не понимаете, почему код не работает? Мы всё предусмотрели — вступайте в сообщество Хендбука! Там студенты помогают друг другу разобраться.
Ключевые выводы параграфа
- Класс — это шаблон, по которому создаются объекты с заданными свойствами и поведением.
- Объект — экземпляр класса, который хранит собственные данные и умеет выполнять методы.
- Атрибуты описывают состояние объекта, а методы — его действия.
- Метод
__init__()
и параметрself
используются для инициализации и работы с объектом. - Инкапсуляция и полиморфизм помогают создавать надёжные и гибкие программы.