5.1. Объектная модель Python. Классы, поля и методы

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

Ключевые вопросы параграфа

  • Что такое класс и объект в 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 используются для инициализации и работы с объектом.
  • Инкапсуляция и полиморфизм помогают создавать надёжные и гибкие программы.

Отмечайте параграфы как прочитанные, чтобы видеть свой прогресс обучения

Вступайте в сообщество хендбука

Здесь можно найти единомышленников, экспертов и просто интересных собеседников. А ещё — получить помощь или поделиться знаниями.
Вступить
Сообщить об ошибке
Предыдущий параграф4.4. Чему вы научились
Следующий параграф5.2. Волшебные методы, переопределение методов. Наследование

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