Зачем объединять данные и функции вместе
До сих пор вы держали данные и код, который с ними работает, отдельно друг от друга. Например, для книги вы бы завели три переменные — title, author, pages — а печатать описание книги пришлось бы отдельной функцией, в которую эти три переменные каждый раз передаются заново. Если книг несколько, переменных становится в три раза больше, и легко перепутать, какой автор к какому названию относится.
Представьте письменный стол: на нём вперемешку лежат страницы из разных книг. Найти нужную страницу — мучение. Гораздо удобнее, когда каждая книга — это отдельный, скреплённый том: обложка, страницы и содержание держатся вместе, как единое целое.
Именно эту идею предлагает объектно-ориентированное программирование (сокращённо — ООП): данные (что объект знает о себе) и функции, которые с этими данными работают (что объект умеет делать), объединяются в одну сущность — объект. В официальной терминологии данные объекта называют атрибутами, а функции внутри объекта — методами.
Аналогия: книга в библиотеке
Атрибуты (что она знает о себе)
- название
- автор
- количество страниц
Методы (что с ней можно сделать)
- показать короткое описание
- отметить как прочитанную
Объект — это данные (атрибуты) плюс функции для работы с ними (методы), упакованные вместе. Раньше вы писали программы как последовательность действий — такой подход называется процедурным стилем. ООП — ещё один способ организовать код, а не замена всему, что вы уже знаете: функции, циклы и условия внутри объектов работают точно так же, как раньше.
Класс — чертёж, объект — изделие по чертежу
Прежде чем печатать книги, издательство делает макет: где заголовок, где имя автора, какой шрифт. По одному макету печатают тысячи разных книг — у каждой свой текст, но структура одна и та же. В программировании такой макет называют классом, а готовую книгу, напечатанную по этому макету, — экземпляром класса (или просто объектом).
Класс описывается ключевым словом class: имя класса, двоеточие и тело с отступом — так же, как вы описывали функцию через def.
class Book:
pass # пока пустой класс
| Часть кода | Что делает |
|---|---|
class | ключевое слово, с которого начинается описание класса |
Book | имя класса; по договорённости пишется с большой буквы, каждое новое слово тоже с большой — это называется CamelCase: Book, BankAccount |
: | как и у def или if, двоеточие открывает тело класса с отступом |
pass | заглушка: тело не должно быть пустым, а функционала пока нет |
Этот класс уже работает, но пока бесполезен — в нём нет ни данных, ни действий. Наполним его по шагам.
Соглашение об именах: классы называют в CamelCase (Car, UserProfile), а функции и переменные — в привычном вам snake_case (user_profile). Это не требование языка, а негласная договорённость всех Python-разработчиков — нарушите её, и код будет работать, но сбивать с толку других.
Конструктор __init__: как задать объекту начальные данные
Когда вы регистрируетесь в сервисе, вам сразу дают анкету: имя, email, пароль. Пока вы её не заполнили, аккаунта фактически нет — только пустая форма. У объектов то же самое: сразу после создания нужно заполнить его данными. Для этого в классе описывают специальную функцию, которая выполняется автоматически в момент создания объекта.
Такая функция называется конструктором, а в Python она всегда носит имя __init__ (от английского initialize — «инициализировать», то есть задать начальное состояние). Двойные подчёркивания с двух сторон — не опечатка: так в Python помечают служебные («магические», или dunder, от double underscore) методы — методы со особым, предопределённым самим языком поведением. __init__ — самый частый из них.
class Book:
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
| Строка | Что происходит |
|---|---|
def __init__(self, title, author, pages): | объявление конструктора; параметры title, author, pages — обычные параметры функции, ничем не отличаются от параметров def, который вы уже знаете |
self.title = title | значение параметра title сохраняется в объекте под тем же именем — о self подробно дальше |
self.author = author и следующая | то же самое для остальных данных — каждую строку конструктора можно читать так: «сохрани переданное значение внутри объекта» |
Пока класс сам по себе не выполняет этот код — __init__ сработает только тогда, когда вы создадите объект. Как именно — разберём в следующем разделе.
self — ссылка на «этот конкретный объект»
Представьте, что вы заполняете анкету не для себя, а сразу за всех сотрудников компании — сто одинаковых бланков. Чтобы не запутаться, в какой бланк что писать, вам нужно постоянно указывать: «вот в этот, текущий». В Python эту роль играет self: он показывает, с каким именно объектом сейчас работает код.
self — первый параметр каждого метода класса, и обозначает он тот объект, у которого этот метод вызвали. Слово self — тоже просто соглашение (можно назвать параметр иначе, язык не запретит), но менять его не стоит: все, кто читает ваш код, ожидают увидеть именно self.
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
def description(self):
return f"'{self.title}' by {self.author}"
book = Book("1984", "George Orwell")
print(book.description())
| Строка | Что происходит |
|---|---|
def description(self): | метод без дополнительных параметров — но self есть всё равно, он есть всегда |
return f"'{self.title}' by {self.author}" | внутри f-строки self.title и self.author берут данные именно из того объекта, для которого метод вызван |
book = Book("1984", "George Orwell") | создаём объект — подробно в следующем разделе |
book.description() | вызываем метод через точку; выведет '1984' by George Orwell |
Когда вы пишете book.description(), Python сам подставляет book как первый аргумент — то есть фактически выполняется Book.description(book). Именно поэтому self не передают руками при вызове: интерпретатор делает это за вас.
Запомните: self пишут в определении метода (в скобках после имени метода), но не пишут при вызове метода — там уже стоит имя объекта перед точкой.
Создание объекта и обращение к его данным
Класс — это только описание того, какими будут будущие объекты. Чтобы получить настоящий, «рабочий» объект, класс нужно вызвать как функцию — это называется созданием экземпляра.
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
book = Book("1984", "George Orwell")
print(book.title) # 1984
print(book.author) # George Orwell
| Строка | Что происходит |
|---|---|
Book("1984", "George Orwell") | Python создаёт новый пустой объект типа Book и сразу вызывает для него __init__, передавая туда "1984" и "George Orwell" — их получают параметры title и author (параметр self Python подставляет сам, вручную его не передают) |
book = ... | готовый объект сохраняется в переменную book, дальше с ним можно работать как с обычным значением |
book.title | обращение к атрибуту объекта через точку — вернёт "1984", то самое значение, которое в конструкторе сохранили как self.title |
Несколько объектов одного класса — у каждого свои данные
Один и тот же макет книги издательство использует, чтобы напечатать сотни разных экземпляров — у каждого свой текст, но обложка устроена одинаково. Так же и здесь: по одному классу можно создать сколько угодно объектов, и данные внутри них никак не пересекаются.
class Book:
def __init__(self, title, pages):
self.title = title
self.pages = pages
book1 = Book("1984", 328)
book2 = Book("Война и мир", 1225)
print(book1.title, book1.pages) # 1984 328
print(book2.title, book2.pages) # Война и мир 1225
book1.pages = 330 # меняем данные только у book1
print(book1.pages) # 330
print(book2.pages) # 1225 -- не изменилось
| Строка | Что происходит |
|---|---|
book1 = Book(...) и book2 = Book(...) | два независимых объекта, созданных по одному классу |
book1.pages = 330 | напрямую меняем атрибут конкретного объекта, как обычную переменную, только через точку |
book2.pages всё ещё 1225 | изменение book1 никак не задело book2 — у каждого объекта своя, отдельная копия атрибутов |
Атрибут класса: данные, общие для всех объектов
У всех книг, независимо от названия и автора, один и тот же материал — бумага. Незачем хранить это отдельно в каждом объекте: такую общую характеристику удобнее задать один раз на весь класс, а не повторять в конструкторе для каждого экземпляра.
Такие данные называют атрибутом класса — в отличие от атрибута экземпляра (который вы уже видели, он задаётся через self в __init__), атрибут класса пишут прямо в теле класса, без self.
class Book:
material = "бумага" # атрибут КЛАССА -- один на всех
def __init__(self, title):
self.title = title # атрибут ЭКЗЕМПЛЯРА -- свой у каждого
book1 = Book("1984")
book2 = Book("Дюна")
print(book1.material) # бумага
print(book2.material) # бумага -- общее значение
print(Book.material) # бумага -- можно взять и у самого класса
| Строка | Что происходит |
|---|---|
material = "бумага" | строка внутри тела класса, но вне __init__ и без self — это и есть атрибут класса |
book1.material, book2.material | оба объекта видят одно и то же значение, хотя ни один из них не задавал его сам |
Book.material | атрибут класса доступен и через сам класс, без создания объекта — ещё одно отличие от атрибута экземпляра |
Правило простое: если значение своё у каждого объекта — это атрибут экземпляра, задавайте через self в __init__. Если значение одно на всех — атрибут класса.
Методы с параметрами: объект, который умеет действовать
Пока что методы у вас только читали данные объекта. Но объект может ещё и изменять сам себя — например, счёт в банке умеет не только показывать баланс, но и пополняться, и списывать деньги. Такое поведение тоже описывают методом, только он принимает дополнительные параметры и меняет атрибуты через self.
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
if amount > self.balance:
return False
self.balance -= amount
return True
acc = BankAccount("Алиса", 1000)
acc.deposit(500)
print(acc.balance) # 1500
success = acc.withdraw(2000)
print(success, acc.balance) # False 1500 -- баланс не изменился
| Строка | Что происходит |
|---|---|
def __init__(self, owner, balance=0): | параметр balance со значением по умолчанию — работает так же, как у обычных функций: не передали — будет 0 |
def deposit(self, amount): | метод с дополнительным параметром amount — помимо обязательного self |
self.balance += amount | метод меняет атрибут того самого объекта, для которого его вызвали — именно поэтому нужен self, без него метод не знал бы, чей баланс менять |
if amount > self.balance: return False | обычная проверка через if, ровно такая же, как в функциях — классы не меняют то, что вы уже знаете про условия |
acc.deposit(500) | вызов метода: self Python подставляет сам (это acc), поэтому передаётся только amount=500 |
Частые ошибки
Забыли self в определении метода.
class Book:
def description(): # нет self
return "книга"
book = Book()
book.description()
# TypeError: description() takes 0 positional arguments but 1 was given
Python всё равно попытается подставить book первым аргументом при вызове — а свободного места под него в скобках нет. Первым параметром любого метода всегда должен идти self, даже если внутри метода он не используется.
Обратились к атрибуту без self внутри класса.
class Book:
def __init__(self, title):
title = title # не self.title!
book = Book("1984")
print(book.title)
# AttributeError: 'Book' object has no attribute 'title'
Строка title = title создаёт обычную локальную переменную внутри __init__, которая исчезает, как только функция заканчивается. Чтобы значение осталось в объекте, слева обязательно должно стоять self.title.
Перепутали атрибут класса с атрибутом экземпляра для изменяемых данных.
Если атрибут класса задать один общий список или словарь на всех, все объекты будут делить один и тот же объект-данные, и изменение у одного отразится на всех остальных. Как правило, менять со временем нужно только атрибуты экземпляра — их задают в __init__ через self, а не в теле класса.
ООП от азов до PRO
Этот урок — только начало. В отдельном курсе вы разберёте ООП полностью: наследование, полиморфизм, инкапсуляцию, магические методы и паттерны проектирования на реальных проектах.
Что важно запомнить
- Объект объединяет данные (атрибуты) и функции для работы с ними (методы)
- Класс — чертёж; экземпляр (объект) — конкретная вещь, созданная по этому чертежу
- Класс описывают через
class ИмяКласса:, имя — вCamelCase __init__— конструктор, вызывается автоматически при создании объекта и задаёт начальные атрибутыself— первый параметр каждого метода, ссылка на тот объект, для которого метод вызвали; при вызове его не передают- Атрибут экземпляра задаётся через
self.имяв__init__— свой у каждого объекта - Атрибут класса пишут в теле класса без
self— общий для всех объектов - Метод — функция внутри класса; вызывается как
объект.метод(аргументы) - Несколько объектов одного класса не мешают друг другу: изменение одного не затрагивает остальные
Проверьте себя
8 вопросов
Класс Book
Создайте класс Book с:
- атрибутом класса
material = "бумага"; - конструктором
__init__(self, title, author, pages), который сохраняет название, автора и количество страниц в атрибуты объекта; - методом
description(self), который возвращает строку вида'1984' by George Orwell, 328 pages(кавычки вокруг названия — одинарные, как в примере).