На этой странице
- В этом уроке
- Глава 1: Зачем нужно ООП — процедурный код и объекты
- Глава 2: Класс и объект — определяем шаблон и создаём по нему объекты
- Глава 3:
__init__иself— объект, который рождается полноценным - Глава 4: Методы — действия объекта
- Глава 5: Общие данные — атрибут экземпляра и атрибут класса
- Глава 6: Что становится классом — от словесного требования к классам
- Глава 7: Назад в банк — всё вместе
- Практика
- Мини-проект: Маленькая библиотека
Классы и объекты: от проблемы к полноценному классу
- Распознавать проблемы процедурного кода по мере роста системы и объяснять, как их решает ООП
- Определять класс (Class) и создавать из него несколько объектов, отличать класс от объекта (Object)
- Писать
__init__, который инициализирует полноценный объект и проверяет значения, и объяснять, что такоеself - Добавлять методы — геттеры и сеттеры — и
__str__, который отображает объект - Различать атрибут экземпляра (Instance Attribute) и атрибут класса (Class Attribute) и использовать общий счётчик
- Превращать словесное описание в структуру классов — что является классом, что атрибутом, а что методом
- Построить полноценный класс, который связывает все идеи, — в мини-проекте
В этом уроке
Урок построен как одна история, в том же порядке, что и в презентации. Мы начинаем с маленькой банковской системы на словарях и функциях, видим, где она ломается, и в каждой главе добавляем один инструмент, который решает одну проблему:
- Глава 1 — зачем нужно ООП: проблемы процедурного кода
- Глава 2 — класс и объект: шаблон и экземпляры
- Глава 3 —
__init__иself: объект, который рождается полноценным - Глава 4 — методы: объект не только знает, но и делает
- Глава 5 — общие данные: атрибуты класса
- Глава 6 — что становится классом: от словесного требования к классам
- Глава 7 — назад в банк: всё вместе
В конце урока — практика и мини-проект, который связывает все главы.
Глава 1: Зачем нужно ООП — процедурный код и объекты
Что мы изучим?
Вся эта часть курса посвящена ООП — объектно-ориентированному программированию (Object-Oriented Programming). Прежде чем написать хоть одну строку кода в стиле ООП, ответим на самый важный вопрос: зачем?
В этой главе:
- Напишем маленькую систему без ООП — тем способом, который уже знаем.
- Увидим, где она начинает становиться проблемной.
- Поймём идею ООП как решения — пока без синтаксиса.
Зачем это нужно?
Всё, что мы изучили до сих пор, работает. Любую программу можно написать с переменными, словарями, списками и функциями. Это называется процедурным программированием (Procedural Programming): данные с одной стороны, функции, которые над ними действуют, — с другой.
Но когда система растёт до десятков видов «вещей» и сотен функций, начинаются проблемы. ООП — способ, которым строятся почти все большие системы. И инструменты, которыми вы будете пользоваться (FastAPI, библиотеки Python), тоже написаны в стиле ООП. Чтобы их читать и использовать, нужно его понимать.
Объяснение с основ
Проблема: система банковских счетов — без ООП
Построим маленькую систему управления банковскими счетами тем, что уже знаем:
account1 = {"owner": "Dana", "balance": 1000}
account2 = {"owner": "Avi", "balance": 500}
def deposit(account, amount):
if amount <= 0:
raise ValueError("amount must be positive")
account["balance"] = account["balance"] + amount
def withdraw(account, amount):
if amount > account["balance"]:
raise ValueError("insufficient funds")
account["balance"] = account["balance"] - amount
deposit(account1, 200)
withdraw(account2, 100)
print(account1, account2){'owner': 'Dana', 'balance': 1200} {'owner': 'Avi', 'balance': 400}Работает! Так в чём проблема? Давайте увеличим систему.
Проблема 1: кто угодно может «трогать» данные
account1["balance"] = -999999
account2["balnce"] = 100- Первая строка обходит все проверки, которые мы написали в
withdraw. Отрицательный баланс без всякой проверки. - Во второй строке опечатка. Создан новый ключ
"balnce", и никакой ошибки нет (мы видели это в теме структур данных и функций).
Правила (не снимать больше, чем есть, положительная сумма) существуют, только если все помнят, что нужно пользоваться функциями. Ничто их к этому не принуждает.
Проблема 2: данные и действия разбросаны
Каждое действие со счётом — отдельная функция, которая получает словарь: deposit(account, ...), withdraw(account, ...), transfer(...), monthly_fee(...), print_statement(...). В настоящей системе это десятки функций в нескольких файлах. Тот, кто получил словарь счёта, не знает, какие действия с ним существуют и где они.
Проблема 3: нет определённой «структуры»
Как выглядит счёт? Что обязано в нём быть? Возможно, в одном месте кода кто-то создал {"owner": ..., "balance": ...}, а в другом — {"name": ..., "money": ...}. Нет одного места, которое определяет: «это банковский счёт, вот его поля, а вот его действия».
Проблема 4: похожие, но разные виды
Добавили «сберегательный счёт»: как обычный, но с процентами и без возможности снять ниже 1000. Теперь withdraw должна проверять: if account["type"] == "savings": ... elif .... И с каждым новым видом счёта все функции наполняются if.
Идея ООП
ООП организует код вокруг «вещей» (объектов), а не вокруг действий. Каждая «вещь» содержит вместе:
- Свои данные: атрибуты (Attributes). Для счёта: владелец, баланс.
- Действия, которые с ней можно делать: методы (Methods). Для счёта: внести, снять.
PROCEDURAL OBJECT-ORIENTED
data: account dict ┌──────────────────────────┐
functions: deposit(account, x) │ BankAccount │
withdraw(account, x) │ data: owner, balance│
print_statement(account) │ actions: deposit() │
│ withdraw() │
"functions act ON data" │ statement() │
└──────────────────────────┘
"the object knows its own data
and its own actions"И как это решает проблемы?
| Проблема | Решение ООП | Изучим в… |
|---|---|---|
| Кто угодно трогает данные | Объект защищает свои данные, и изменения проходят через его действия (инкапсуляция, Encapsulation) | Следующем уроке |
| Данные и действия разбросаны | Всё внутри класса. account. + Tab в VS Code показывает всё, что можно сделать | Сегодня |
| Нет определённой структуры | Один класс точно определяет, какие данные есть у каждого счёта | Сегодня |
| Похожие виды | Наследование и полиморфизм: сберегательный счёт «наследует» обычный и меняет только то, что отличается | Уроках о наследовании и полиморфизме |
Шаблон и конкретная вещь
Самая базовая идея ООП уже существует в реальном мире:
| Шаблон (Class) | Конкретные вещи, созданные по нему (Objects) |
|---|---|
| Проект квартиры | Квартира Даны на 3-м этаже, квартира Ави на 5-м этаже |
| Анкета «данные студента» | Анкета, которую заполнила Дана, анкета, которую заполнил Ави |
| Шаблон производства машин на конвейере | Красная машина с номером 12-345-67 |
| Понятие «банковский счёт» | Счёт Даны с 1200 шекелей, счёт Ави с 400 шекелей |
- У всех квартир, построенных по одному проекту, одинаковая структура (комнаты, кухня), но разные значения (цвет стен, кто живёт).
- Изменение квартиры Даны не меняет квартиру Ави.
- Сам проект не является квартирой. В проекте жить нельзя.
Класс (Class) = шаблон для производства машин. Он определяет, что у каждой машины есть цвет, модель и номер (атрибуты) и что каждая машина умеет ехать, тормозить и сигналить (методы). Объект (Object) = одна конкретная машина, произведённая по шаблону: красная, модель 2024, номер 12-345-67. По нему можно произвести тысячи машин, у каждой свой цвет и номер.
Синтаксис
Класс мы пока не пишем. В следующей главе изучим полный синтаксис. Но вот «вкус», чтобы увидеть, куда мы идём. Та же банковская система в стиле ООП:
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("amount must be positive")
self.balance = self.balance + amount
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("insufficient funds")
self.balance = self.balance - amount
account1 = BankAccount("Dana", 1000)
account1.deposit(200)
print(account1.balance)Не пытайтесь сейчас понять каждое слово (class, __init__, self) — каждому из них дальше в уроке посвящена целая глава. Обратите внимание только на одно: данные (owner, balance) и действия (deposit, withdraw) находятся в одном месте, и вызывают их через объект: account1.deposit(200).
Первый пример кода
Вы всё время пользовались объектами, сами того не зная! В Python каждое значение — объект со своими данными и методами:
name = "dana cohen"
print(name.title())
print(name.upper())
print(type(name))
numbers = [3, 1, 2]
numbers.append(5)
numbers.sort()
print(numbers)
print(type(numbers))Вывод:
Dana Cohen
DANA COHEN
<class 'str'>
[1, 2, 3, 5]
<class 'list'>Разбор кода построчно
| Код | Что это в ООП |
|---|---|
name = "dana cohen" | Создан объект типа str. Его данные — символы. |
name.title() | Вызов метода title объекта. Он «умеет» работать со своим текстом. |
type(name) → <class 'str'> | Класс (шаблон) объекта — str. |
numbers.append(5) | Метод объекта list, который меняет его собственные данные. |
<class 'list'> | list — тоже класс. Каждый созданный вами список — объект класса list. |
Ключевой момент: str и list — классы, которые Python написал для нас. В этой части мы научимся писать собственные классы: BankAccount, Student, Product, подходящие для нашей задачи.
Ещё один пример
Ещё одна процедурная проблема: система студентов. Возьмите следующий код и попробуйте сами найти проблемы.
student1 = {"name": "Dana", "grades": [90, 85]}
student2 = {"name": "Avi", "grades": [70]}
student3 = {"nmae": "Noa", "grades": []}
def add_grade(student, grade):
student["grades"].append(grade)
def average(student):
return sum(student["grades"]) / len(student["grades"])
add_grade(student1, 100)
student2["grades"].append(500)
print(average(student1))
print(average(student2))
print(average(student3))91.66666666666667
285.0
Traceback (most recent call last):
...
ZeroDivisionError: division by zeroПроблемы:
student3создан с опечаткой ("nmae"). Нет места, которое определяет, что «должно» быть у студента.student2["grades"].append(500)обошёлadd_grade. Даже если быadd_gradeпроверяла, что оценка от 0 до 100, проверка бы не сработала. Среднее 285!averageпадает на студенте без оценок. И тот, кто пользуется словарём студента, должен знать, что эта функция существует, и не забыть обработать этот случай.
В ООП Student будет одним классом с определённой структурой (имя и оценки, всегда), с методом add_grade, который будет единственным способом добавить оценку (и проверит её), и с методом average, который умеет обращаться с пустым списком. Правила находятся рядом с данными, которые они защищают.
Итоги
- Процедурное программирование: данные (словари) и функции отдельно. Работает, но по мере роста системы данные открыты, действия разбросаны и нет обязательной структуры.
- ООП: организуйте код вокруг вещей. Каждый объект хранит данные (атрибуты) и действия (методы) вместе.
- Класс (Class) = шаблон (проект здания, шаблон производства). Объект (Object) = конкретная вещь, созданная по нему.
- В Python каждое значение — объект:
strиlist— классы, а"hi".upper()— вызов метода. - ООП — способ мышления: какие вещи существуют, что они знают и что делают. Не всё обязано быть классом.
Официальная документация
Глава 2: Класс и объект — определяем шаблон и создаём по нему объекты
Что мы изучим?
- Что такое класс (Class) и что такое объект (Object, его также называют экземпляром — Instance).
- Что означает слово
class. - Синтаксис определения класса.
- Как создать объект из класса.
- Почему каждый объект независим.
Зачем это нужно?
В предыдущей главе мы увидели, что проблема «словарь + функции» в том, что нет определённой структуры и нет одного места, которое собирает данные и действия. Класс — инструмент, который создаёт эту структуру. Всё остальное (__init__, self, методы) строится на нём.
Объяснение с основ
Что такое класс?
Класс (Class) — шаблон (Blueprint), который определяет новый вид вещей: какая информация будет у каждой вещи этого вида и что она будет уметь делать.
- Класс не содержит данных конкретной вещи. Это только определение.
- Из одного класса можно создать сколько угодно вещей.
- Определяя класс, вы фактически создаёте новый тип в Python, точно как
strилиlist.
Что такое объект (экземпляр)?
Объект (Object) — конкретная вещь, созданная из класса. У него есть собственные данные.
Экземпляр (Instance) — синоним: «объект класса Student» = «экземпляр Student». Действие создания объекта из класса называется созданием экземпляра (Instantiation).
| Класс | Объект / экземпляр | |
|---|---|---|
| Что это | Шаблон, определение | Конкретная вещь |
| Сколько | Один на каждый вид | Сколько угодно |
| Данные | Определяет, какие поля будут | Хранит конкретные значения |
| Пример | Student | Дана, Ави, Ноа |
| Аналогия | Шаблон производства машин | Красная машина с номером 12-345-67 |
Слово class
class — зарезервированное слово (Keyword) в Python, как def, if и for. Оно говорит Python: «здесь начинается определение нового вида». После него идёт имя класса, а затем — тело с отступом.
Имена классов
По PEP 8 имена классов пишутся в PascalCase: каждое слово начинается с заглавной буквы, без подчёркиваний. Student, BankAccount, ShoppingCart. Это отличается от функций и переменных (snake_case), и так сразу видно, что перед вами класс.
Синтаксис
class ClassName:
# class body - indented
pass
my_object = ClassName()| Часть | Объяснение |
|---|---|
class | Зарезервированное слово: «начинается определение класса». |
ClassName | Имя класса в PascalCase. |
: | Обязательно, как после def или if. |
| Тело с отступом | То, что включает шаблон (в следующих главах: __init__ и методы). |
pass | «Ничего не делать». Тело класса (или функции) не может быть пустым, поэтому pass — заполнитель. |
ClassName() | Создание объекта: класс вызывают со скобками, как функцию. Получается новый объект. |
Первый пример кода
week-3/first_class.py:
class Person:
pass
dana = Person()
avi = Person()
print(dana)
print(avi)
print(type(dana))
print(dana is avi)
print(isinstance(dana, Person))Вывод:
<__main__.Person object at 0x000001F4A3B2C1D0>
<__main__.Person object at 0x000001F4A3B2C2E0>
<class '__main__.Person'>
False
TrueРазбор кода построчно
| Строка | Объяснение |
|---|---|
class Person: | Определяет новый вид с именем Person. Ни один человек ещё не создан. |
pass | Шаблон пока пуст. |
dana = Person() | Создание экземпляра: создан новый объект типа Person, и имя dana указывает на него. |
avi = Person() | Ещё один объект, полностью отдельный. |
print(dana) | У объекта нет «содержимого» для показа, поэтому Python печатает описание по умолчанию: тип и адрес в памяти (0x...). У вас адрес будет другим. |
| Два разных адреса | Два отдельных объекта в RAM. |
type(dana) | <class '__main__.Person'>. Класс объекта — Person, определённый в главном файле (__main__). |
dana is avi | False: это не один и тот же объект (вспомните is!). |
isinstance(dana, Person) | True: «является ли dana экземпляром Person?» |
Ещё один пример
Добавляем данные объекту — и видим, почему так делать не стоит. К объекту можно прикрепить атрибут снаружи, через точку:
class Product:
pass
laptop = Product()
laptop.name = "Laptop"
laptop.price = 3500
mouse = Product()
mouse.name = "Mouse"
mouse.price = 80
print(laptop.name, laptop.price)
print(mouse.name, mouse.price)
mouse.price = 70
print("After update:", mouse.price, "| laptop unchanged:", laptop.price)Вывод:
Laptop 3500
Mouse 80
After update: 70 | laptop unchanged: 3500Разбор:
laptop.name = "Laptop"— создаёт атрибут с именемnameу объектаlaptop. Точка означает «у»:laptop.name= «имя ноутбука».- У каждого объекта свои атрибуты. Изменение
mouse.priceне трогаетlaptop. - Это уже лучше словаря:
laptop.priceчитается лучше, чемlaptop["price"].
Но здесь большая проблема. Что гарантирует, что у каждого Product есть name и price?
keyboard = Product()
keyboard.nmae = "Keyboard"
print(keyboard.name)AttributeError: 'Product' object has no attribute 'name'- Опечатка (
nmae) создала другой атрибут. - Мы забыли определить
price, и предупреждения нет, пока кто-нибудь не попытается его использовать. - Это ровно проблема словаря из предыдущей главы! Пустой класс всё ещё не определяет структуру.
Решение: сам класс должен определять, какие данные обязаны быть у каждого объекта, и заполнять их в момент создания. Это роль __init__ — тема следующей главы.
Итоги
- Класс (Class) = шаблон, определяющий новый вид. Объект / экземпляр (Object / Instance) = конкретная вещь, созданная по нему.
- Синтаксис:
class Name:+ тело с отступом (илиpass). Имена в PascalCase. - Создание объекта:
obj = Name(), со скобками. Без скобок — это сам класс. - Каждый объект независим:
a is b→False. Атрибуты одного не влияют на другой. type(obj)возвращает класс.isinstance(obj, Name)проверяет принадлежность.- Добавление атрибутов «снаружи» работает, но не определяет структуру. Поэтому нужен
__init__.
Официальная документация
Глава 3: __init__ и self — объект, который рождается полноценным
Что мы изучим?
- Что такое
__init__— функция, которая выполняется автоматически при создании объекта. - Что такое конструктор (Constructor).
- Что такое
self— слово, которое встречается в ООП повсюду. - Атрибуты экземпляра (Instance Attributes): данные каждого объекта.
Зачем это нужно?
В предыдущей главе мы добавляли атрибуты «снаружи»: laptop.name = "Laptop". Мы увидели, что это опасно: можно забыть поле, сделать опечатку, и нет обязательной структуры. Мы даже написали вспомогательную функцию make_book, чтобы гарантировать, что у каждой книги есть title и pages.
__init__ — это именно тот make_book, только внутри класса. Он гарантирует, что каждый объект создаётся инициализированным: со всеми полями и со значениями — с самого первого момента.
Объяснение с основ
__init__ — метод инициализации
__init__ (произносится «дандер инит», double underscore init) — специальная функция, определённая внутри класса. Она:
- Вызывается автоматически в момент создания нового объекта:
Student(...). - Инициализирует объект, то есть кладёт в него начальные данные.
- Ничего не возвращает (
returnне нужен).
Имя с двумя подчёркиваниями с каждой стороны обозначает специальный метод Python (его называют Dunder Method или Magic Method). Python ищет именно это имя и вызывает его сам. Вы никогда не пишете obj.__init__() сами.
Конструктор
Конструктор (Constructor) = код, который выполняется при построении нового объекта. В Python эту роль играет __init__, поэтому его обычно называют «конструктором». (Если быть технически точным: само построение, выделение объекта в памяти, происходит на шаг раньше, а __init__ «лишь» заполняет его. Для целей курса __init__ = конструктор.)
self — «этот объект»
self — первый параметр каждого метода в классе, включая __init__. Он указывает на конкретный объект, с которым метод работает прямо сейчас.
Зачем он нужен? Класс — один шаблон, но из него создаётся много объектов. Когда код в __init__ говорит «сохрани имя», ему нужно знать, в каком объекте сохранить. self — ответ: «в объекте, который создаётся сейчас».
self.name = nameЧитайте так: «в этом объекте (self) создай атрибут с именем name и положи в него значение, полученное в параметре name».
self.name— атрибут объекта. Он остаётся, пока существует объект.name(отдельно) — параметр__init__: локальная переменная (Local, вспомните область видимости!), которая исчезает в конце__init__.
Откуда берётся self? Python передаёт его автоматически! Когда вы пишете Student("Dana", 24), вы передаёте 2 аргумента, а __init__ получает 3: Python добавляет новый объект как self в начало.
Класс — это пустая анкета с полями: «Имя: ____ Возраст: ____». __init__ — сотрудник регистратуры, который заполняет новую анкету для каждого пришедшего студента. self — «анкета, которая сейчас лежит на столе»: сотрудник пишет «Имя: Дана» на этой анкете, а не на всех анкетах в шкафу. Завтра придёт Ави, будет заполнена другая анкета, но сотрудник снова назовёт её «анкетой на столе» (self).
Что происходит в строке dana = Student("Dana", 24)
- Python создаёт новый пустой объект типа
Studentв памяти. - Python вызывает
__init__и передаёт ему:self= новый объект,name = "Dana",age = 24. __init__выполняетself.name = name, то естьnameнового объекта ="Dana". И то же самое дляage.__init__завершается. Локальные параметры исчезают, а атрибуты остаются на объекте.- Python возвращает объект, и имя
danaуказывает на него.
Атрибуты экземпляра
Атрибуты экземпляра (Instance Attributes) = данные, которые принадлежат конкретному объекту. У каждого объекта свои значения. Их определяют в __init__ через self.something = ....
Синтаксис
class ClassName:
def __init__(self, param1, param2):
self.attribute1 = param1
self.attribute2 = param2
self.attribute3 = "default value" # attributes do not have to come from parameters
obj = ClassName(arg1, arg2) # Python calls __init__(obj, arg1, arg2) for you
obj.attribute1 # read an instance attribute__init__пишется внутри класса, с отступом.self— всегда первый в списке параметров.- При вызове
selfне передают. - Параметрам можно давать значения по умолчанию, как в любой функции:
def __init__(self, name, age=18):.
Первый пример кода
week-3/student.py:
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
self.grades = []
dana = Student("Dana", 24)
avi = Student("Avi", 31)
print(dana.name, dana.age, dana.grades)
print(avi.name, avi.age, avi.grades)
dana.grades.append(95)
print("Dana's grades:", dana.grades)
print("Avi's grades:", avi.grades)Вывод:
Dana 24 []
Avi 31 []
Dana's grades: [95]
Avi's grades: []Разбор кода построчно
| Строка | Объяснение |
|---|---|
class Student: | Определение шаблона. |
def __init__(self, name, age): | Конструктор. self = создаваемый объект. name, age = то, что передают при создании. |
self.name = name | Атрибут name у объекта со значением из параметра. |
self.age = age | То же для возраста. |
self.grades = [] | Атрибут, который не приходит из параметра: каждый студент начинает с собственного пустого списка оценок. |
dana = Student("Dana", 24) | Python: создаёт объект, вызывает __init__(new_obj, "Dana", 24) и возвращает его. |
avi = Student("Avi", 31) | Второй объект. __init__ выполняется снова, с self = второй объект. |
dana.grades.append(95) | Обращение к списку Даны и добавление в него. |
avi.grades остаётся [] | Каждый __init__ создал новый список, поэтому общего нет. (Сравните с багом def f(cart=[]) из урока о функциях!) |
Теперь структура гарантирована: нельзя создать Student без имени и возраста, и у каждого студента всегда есть grades.
Ещё один пример
Product со значениями по умолчанию, вычисляемым атрибутом и первой проверкой. week-3/product.py:
class Product:
def __init__(self, name, price, quantity=0):
if price < 0:
raise ValueError("price cannot be negative, got " + str(price))
self.name = name
self.price = price
self.quantity = quantity
self.in_stock = quantity > 0
laptop = Product("Laptop", 3500, 4)
cable = Product("USB Cable", 25)
print(laptop.name, laptop.price, laptop.quantity, laptop.in_stock)
print(cable.name, cable.price, cable.quantity, cable.in_stock)
try:
broken = Product("Ghost", -10)
except ValueError as error:
print("Could not create product:", error)Вывод:
Laptop 3500 4 True
USB Cable 25 0 False
Could not create product: price cannot be negative, got -10Разбор:
quantity=0— значение по умолчанию.Product("USB Cable", 25)получает количество 0.self.in_stock = quantity > 0— атрибут, вычисляемый из данных:TrueилиFalse.- Проверка в конструкторе:
__init__отказывается создавать товар с отрицательной ценой и выбрасываетValueError(вспомнитеraise!). Недопустимый товар не может существовать в системе. Это ровно «держать правила рядом с данными», о чём мы говорили в первой главе. - Когда
__init__выбрасывает ошибку, объект не создаётся, иbrokenне получает значения.
Person — третий пример, для закрепления
class Person:
def __init__(self, first_name, last_name, city="Unknown"):
self.first_name = first_name
self.last_name = last_name
self.city = city
self.full_name = first_name + " " + last_name
noa = Person("Noa", "Levi", "Haifa")
tom = Person("Tom", "Cohen")
print(noa.full_name, "-", noa.city)
print(tom.full_name, "-", tom.city)Noa Levi - Haifa
Tom Cohen - UnknownИтоги
__init__= конструктор: вызывается автоматически при создании объекта и инициализирует его данные.self= конкретный объект, с которым метод работает прямо сейчас. Всегда первый параметр, и Python передаёт его сам.self.x = x:self.x= атрибут, сохранённый на объекте.x= локальный параметр, который исчезает.obj = Class(a, b)→ Python создаёт объект, вызывает__init__(obj, a, b)и возвращает объект.- В
__init__могут быть: значения по умолчанию, вычисляемые атрибуты и проверка сraise, чтобы недопустимый объект вообще не создавался.
Официальная документация
Глава 4: Методы — действия объекта
Что мы изучим?
- Что такое метод (Method).
- Как его определить и как вызвать.
- Три вида методов по тому, что они делают: вычисляют, меняют состояние, печатают.
- Метод, который вызывает другой метод.
__str__: как объект «представляет себя».
Зачем это нужно?
До сих пор у наших объектов были только данные. Но идея ООП — данные + действия вместе. В первой главе мы увидели, что правила (например, «нельзя снять больше баланса») должны находиться рядом с данными. Методы — это место, где живут эти правила.
И есть открытая проблема из предыдущей главы: description машины вычислялось один раз в __init__ и устарело, когда цвет изменился. Метод вычисляет заново каждый раз.
Объяснение с основ
Что такое метод?
Метод (Method) = функция, определённая внутри класса, которая действует на конкретный объект этого класса.
| Функция (обычная) | Метод | |
|---|---|---|
| Где определена | На верхнем уровне файла | Внутри класса |
| Как вызывать | function_name(args) | obj.method_name(args) |
| С чем работает | С тем, что ей передали | Со своим объектом (self) и с тем, что ему передали |
| Первый параметр | Любой | self |
Методами вы уже пользовались: "hi".upper(), numbers.append(5). Сегодня пишем свои.
self в методах
Точно как в __init__: когда вы вызываете dana.add_grade(90), Python автоматически передаёт dana как self. Внутри метода self.grades = оценки Даны. Если вызвать avi.add_grade(80), выполнится тот же самый метод, но self будет Ави.
dana.add_grade(90) → Student.add_grade(dana, 90)
↑ ↑
self gradeЛевая строка — то, что пишем мы. Правая — то, что на самом деле делает Python.
Три вида методов (по тому, что они делают)
| Вид | Что делает | Пример |
|---|---|---|
| Геттер (Getter) | Вычисляет и возвращает значение из данных, ничего не меняя | average(), is_passing() |
| Сеттер (Setter) | Меняет состояние объекта | add_grade(90), deposit(100) |
| Отображение (Display) | Печатает / возвращает текст для показа | print_report(), __str__() |
Полезное различие из чистого кода: хороший метод обычно либо геттер, либо сеттер, но не оба сразу (SRP!).
Состояние
Состояние (State) = значения всех атрибутов объекта в определённый момент. Счёт с балансом 1000 находится в определённом состоянии. После deposit(200) — в новом состоянии. Методы-сеттеры — упорядоченный способ менять состояние.
__str__ — как объект выглядит при печати
Помните, что print(dana) печатал <__main__.Student object at 0x...>? Если определить в классе специальный метод с именем __str__ (снова Dunder!), который возвращает строку, print и str() будут использовать его для показа объекта.
Синтаксис
class ClassName:
def __init__(self, ...):
self.data = ...
def getter_method(self): # returns something, changes nothing
return ... self.data ...
def setter_method(self, value): # changes the object's state
self.data = ...
def uses_other_method(self):
return self.getter_method() * 2 # call another method on the same object
def __str__(self): # used by print() and str()
return "readable text about " + str(self.data)
obj = ClassName(...)
obj.setter_method(5) # no self in the call
result = obj.getter_method()
print(obj) # calls __str__ automatically- Каждый метод с отступом внутри класса, и
self— первый. - Между методами — одна пустая строка (PEP 8).
- Внутри метода к данным и другим методам обращаются через
self..
Первый пример кода
week-3/student_methods.py:
PASSING_GRADE = 60
class Student:
def __init__(self, name):
self.name = name
self.grades = []
def add_grade(self, grade):
if grade < 0 or grade > 100:
raise ValueError("grade must be between 0 and 100, got " + str(grade))
self.grades.append(grade)
def average(self):
if not self.grades:
return 0
return sum(self.grades) / len(self.grades)
def is_passing(self):
return self.average() >= PASSING_GRADE
def __str__(self):
return self.name + " (average " + str(round(self.average(), 1)) + ")"
dana = Student("Dana")
dana.add_grade(90)
dana.add_grade(75)
avi = Student("Avi")
avi.add_grade(50)
print(dana)
print(avi)
print("Dana passing?", dana.is_passing())
print("Avi passing?", avi.is_passing())Вывод:
Dana (average 82.5)
Avi (average 50.0)
Dana passing? True
Avi passing? FalseРазбор кода построчно
| Код | Вид | Объяснение |
|---|---|---|
def add_grade(self, grade): | Сеттер | Меняет состояние: добавляет в self.grades. И сначала проверяет правило (0–100). |
def average(self): | Геттер | Вычисляет по данным объекта и возвращает. Ничего не меняет. Обрабатывает пустой список (баг из первой главы!). |
def is_passing(self): | Геттер | Вызывает другой метод того же объекта: self.average(). |
def __str__(self): | Отображение | Возвращает строку. print(dana) вызывает его автоматически. |
dana.add_grade(90) | Python: Student.add_grade(dana, 90). self = dana. | |
print(avi) | Python: print(avi.__str__()), то есть "Avi (average 50.0)". |
Важный момент: коду, который использует класс (dana.add_grade(...)), не нужно знать, как хранятся оценки (в списке), и не нужно самому проверять диапазон. За это отвечает класс.
Ещё один пример
Банковский счёт — проблема из первой главы решена. week-3/bank_account.py:
class BankAccount:
def __init__(self, owner, balance=0):
if balance < 0:
raise ValueError("initial balance cannot be negative")
self.owner = owner
self.balance = balance
self.transactions = []
def deposit(self, amount):
if amount <= 0:
raise ValueError("deposit must be positive, got " + str(amount))
self.balance = self.balance + amount
self.transactions.append("deposit " + str(amount))
def withdraw(self, amount):
if amount <= 0:
raise ValueError("withdrawal must be positive, got " + str(amount))
if amount > self.balance:
raise ValueError("insufficient funds: balance " + str(self.balance) + ", requested " + str(amount))
self.balance = self.balance - amount
self.transactions.append("withdraw " + str(amount))
def transfer_to(self, other_account, amount):
self.withdraw(amount)
other_account.deposit(amount)
def statement(self):
print("--- Statement for", self.owner, "---")
for transaction in self.transactions:
print(" ", transaction)
print(" Balance:", self.balance)
def __str__(self):
return "Account of " + self.owner + ": " + str(self.balance) + " NIS"
dana = BankAccount("Dana", 1000)
avi = BankAccount("Avi")
dana.deposit(200)
dana.transfer_to(avi, 300)
try:
avi.withdraw(5000)
except ValueError as error:
print("Rejected:", error)
print(dana)
print(avi)
dana.statement()Вывод:
Rejected: insufficient funds: balance 300, requested 5000
Account of Dana: 900 NIS
Account of Avi: 300 NIS
--- Statement for Dana ---
deposit 200
withdraw 300
Balance: 900Разбор:
transfer_to(self, other_account, amount)— метод, который получает другой объект как параметр. Он не меняет балансы напрямую. Он используетself.withdrawиother_account.deposit, и так все проверки выполняются автоматически. DRY!- Если
withdrawне удался (не хватает денег), он выбрасывает ошибку доdeposit, поэтому деньги «из воздуха» не появляются. self.transactions— внутренняя история, которую обновляет каждое действие. Тому, кто пользуется счётом, не нужно помнить о записи операции.statement()— метод отображения.__str__даёт короткое описание дляprint.- Сравните с процедурной версией из первой главы: те же действия, но теперь есть одно место, которое определяет, что такое счёт и что с ним разрешено делать.
Итоги
- Метод = функция внутри класса. Определяйте его с
selfпервым, а вызывайтеobj.method(args)безself. obj.method(x)равносильноClass.method(obj, x). Такself«знает», с каким объектом работать.- Внутри метода —
self.attributeиself.other_method(). - Геттер возвращает и не меняет. Сеттер меняет состояние (и проверяет правила).
__str__возвращает строку, иprint(obj)использует её.- Производную информацию лучше вычислять в методе, а не хранить как атрибут, который может устареть.
Официальная документация
Глава 5: Общие данные — атрибут экземпляра и атрибут класса
Что мы изучим?
- Атрибут и метод: как решить, что есть что.
- Атрибут экземпляра (Instance Attribute): данные каждого объекта отдельно (то, что мы делали до сих пор).
- Атрибут класса (Class Attribute): данные класса, общие для всех объектов.
- Когда использовать каждый и какие есть ловушки.
Зачем это нужно?
Допустим, есть класс Product, и все товары облагаются одинаковым НДС: 17%. Где хранить НДС?
- Как атрибут экземпляра (
self.vat = 0.17)? Тогда будет тысяча копий одного и того же числа, и если НДС изменится, придётся обновлять тысячу объектов. - Как глобальную константу в начале файла? Можно, но она «далеко» от класса, к которому относится.
А если хочется знать, сколько всего товаров было создано? Ни один товар этого «не знает». Это информация о виде в целом. Атрибуты класса решают оба случая.
Объяснение с основ
Атрибут или метод?
| Атрибут | Метод | |
|---|---|---|
| Что это | Данные, что объект знает / хранит | Действие, что объект делает / вычисляет |
| Часть речи | Существительное / прилагательное | Глагол |
| Примеры | name, price, balance, is_active | deposit(), add_grade(), total() |
| Доступ | obj.price | obj.total() со скобками |
Вопросы, которые помогают решить:
- Это данные, которые хранятся, или что-то, что вычисляется? Хранимые данные → атрибут. Вычисляемое из других данных → обычно метод (мы это видели:
description, которое устарело). - Меняет ли это объект? Каждое упорядоченное изменение → метод (команда).
- Нужны ли правила при изменении? Если да, изменение идёт через метод (и мы углубимся в это в уроке об инкапсуляции).
Атрибут экземпляра
Определяется через self.x = ..., обычно в __init__. У каждого объекта своя копия со своим значением.
Атрибут класса
Определяется прямо в теле класса, вне любого метода, без self:
class Product:
VAT_RATE = 0.17 # class attribute - one shared value
count = 0 # class attribute
def __init__(self, name):
self.name = name # instance attribute - per object- Есть только одна копия, которая принадлежит самому классу.
- Все объекты её видят.
- Обращаются к нему через класс (
Product.VAT_RATE, рекомендуемый способ) или через объект (laptop.VAT_RATE, работает для чтения).
Атрибуты экземпляра = то, что каждый ученик хранит у себя: имя, оценки, тетрадь. Атрибуты класса = то, что висит на классной доске и общее для всех: имя классного руководителя, номер кабинета и сколько учеников в классе. Когда меняется учитель, доску обновляют один раз, а не в тетради каждого ученика.
Как Python находит атрибут
Когда вы пишете obj.x, Python ищет:
- Сначала на объекте (атрибуты экземпляра).
- Если не найдено — на классе (атрибуты класса и методы).
- Не найдено →
AttributeError.
(Обратите внимание: это похоже на LEGB из урока об области видимости — поиск по слоям.) Отсюда и ловушка в типичной ошибке.
Синтаксис
class ClassName:
SHARED_CONSTANT = 100 # class attribute (constant style: UPPER_CASE)
counter = 0 # class attribute (changes, shared)
def __init__(self, value):
self.value = value # instance attribute
ClassName.counter = ClassName.counter + 1 # update the class attribute through the CLASS
def uses_constant(self):
return self.value * ClassName.SHARED_CONSTANT
ClassName.SHARED_CONSTANT # read through the class
ClassName.counterПервый пример кода
week-3/products_shared.py:
class Product:
VAT_RATE = 0.17
created_count = 0
def __init__(self, name, price):
self.name = name
self.price = price
Product.created_count = Product.created_count + 1
def price_with_vat(self):
return round(self.price * (1 + Product.VAT_RATE), 2)
laptop = Product("Laptop", 1000)
mouse = Product("Mouse", 50)
print(laptop.name, laptop.price_with_vat())
print(mouse.name, mouse.price_with_vat())
print("Products created:", Product.created_count)
Product.VAT_RATE = 0.18
print("After VAT change:", laptop.price_with_vat(), mouse.price_with_vat())Вывод:
Laptop 1170.0
Mouse 58.5
Products created: 2
After VAT change: 1180.0 59.0Разбор кода построчно
| Код | Вид | Объяснение |
|---|---|---|
VAT_RATE = 0.17 | Атрибут класса | Одно значение для всех товаров. Заглавные буквы = «константа» (соглашение). |
created_count = 0 | Атрибут класса | Общий счётчик. Начинается с 0, когда класс определяется. |
self.name, self.price | Атрибуты экземпляра | Разные у каждого товара. |
Product.created_count = Product.created_count + 1 | Каждый __init__ обновляет счётчик класса, поэтому он считает все объекты. | |
Product.VAT_RATE в методе | Чтение атрибута класса. | |
Product.VAT_RATE = 0.18 | Одно изменение, и все товары затронуты сразу, потому что все читают из одного места. |
Ещё один пример
Класс с ограничением мест и автоматическим уникальным идентификатором. week-3/students_ids.py:
class Student:
SCHOOL_NAME = "Kodkod 6"
MAX_STUDENTS = 3
next_id = 1
all_students = []
def __init__(self, name):
if len(Student.all_students) >= Student.MAX_STUDENTS:
raise ValueError("class is full - cannot add " + name)
self.name = name
self.student_id = Student.next_id
Student.next_id = Student.next_id + 1
Student.all_students.append(self)
def __str__(self):
return "#" + str(self.student_id) + " " + self.name + " (" + Student.SCHOOL_NAME + ")"
for name in ["Dana", "Avi", "Noa", "Tom"]:
try:
Student(name)
except ValueError as error:
print("Error:", error)
for student in Student.all_students:
print(student)
print("Next id will be:", Student.next_id)Вывод:
Error: class is full - cannot add Tom
#1 Dana (Kodkod 6)
#2 Avi (Kodkod 6)
#3 Noa (Kodkod 6)
Next id will be: 4Разбор:
next_id— счётчик, который выдаёт уникальный идентификатор каждому студенту. Каждый объект хранит свой номер в атрибуте экземпляраstudent_id, а общий счётчик продвигается вперёд.all_students— общий список, в котором класс «регистрирует» каждый созданный объект.append(self)= «добавь этот объект в список».MAX_STUDENTS— правило, действующее для всего класса. Проверка выполняется до присвоения ID, поэтому Том не «сжёг» номер.Student(name)в цикле без сохранения в переменную: объект не теряется, потому что зарегистрирован вall_students.
Атрибуты класса отлично подходят для констант (VAT_RATE, MAX_STUDENTS) и счётчиков. Общий список «всех объектов» — хороший инструмент для демонстрации, но в настоящих проектах коллекцию обычно лучше вести в отдельном классе (например, School, который хранит список студентов). Это мы изучим в уроках о проектировании ООП (композиция, Composition).
Итоги
- Атрибут = что объект знает (существительное). Метод = что он делает (глагол, со скобками). Вычисляемая информация → метод.
- Атрибут экземпляра (
self.xв__init__) = у каждого объекта своё значение. - Атрибут класса (в теле класса, без
self) = одна общая копия. Подходит для констант и счётчиков. - Python ищет
obj.xсначала на объекте, а затем на классе. - Меняйте атрибут класса только через имя класса.
obj.X = ...создаёт новый атрибут экземпляра. - Список/словарь «для каждого объекта» → в
__init__, а не в теле класса.
Официальная документация
Глава 6: Что становится классом — от словесного требования к классам
Что мы изучим?
До сих пор мы говорили вам, какие классы писать. Теперь переходим на другую сторону: решаете вы.
- Как находить классы внутри словесного описания системы.
- Четыре вопроса, которые решают: класс или нет?
- Когда что-то — атрибут, а когда — отдельный класс.
- Какому классу принадлежит каждое действие.
Зачем это нужно?
Программа курса подчёркивает: цель — понимать ООП, а не только уметь скопировать класс. На работе никто не скажет вам «напиши класс с именем Order с такими полями». Вы получите требование: «нужна система, в которой клиенты заказывают товары…», — и вам придётся самим превратить его в структуру классов.
Хорошее проектирование в начале экономит недели исправлений. Плохое приводит к одному огромному классу или к десяткам пустых классов, которые ничего не делают.
Объяснение с основ
Инструмент 1: анализ существительных и глаголов
Простая и эффективная техника: прочитайте требование и отметьте:
| В тексте | Кандидат в |
|---|---|
| Важные существительные («клиент», «заказ», «товар») | Классы или атрибуты |
| Прилагательные и свойства («цена», «дата», «активный») | Атрибуты |
| Глаголы («заказывает», «платит», «отменяет») | Методы |
| Правила («нельзя…», «не более…») | Проверки / инварианты (Validation / Invariants) |
Это отправная точка, а не ответ. Не каждое существительное становится классом.
Инструмент 2: четыре вопроса — это класс?
| # | Вопрос | Если ответ «да» |
|---|---|---|
| 1 | Есть ли у него идентичность и собственные данные? (у каждого разные значения) | Склоняется к классу |
| 2 | Есть ли у него поведение или правила? (действия, проверки) | Сильно склоняется к классу |
| 3 | Есть ли несколько его экземпляров? (много клиентов, много заказов) | Склоняется к классу |
| 4 | Это всего лишь одно значение, без всякого поведения? («цвет», «имя») | Атрибут, а не класс |
«Машина» — у неё есть свой номер (идентичность), она едет и тормозит (поведение), и машин много. ✓ Класс. «Цвет машины» — это всего лишь значение («красный») без собственных действий. Атрибут машины. «Двигатель» — зависит! В простой игре — атрибут (engine_size = 1.6). В гараже, который ремонтирует двигатели, — класс (у него есть серийный номер, история обслуживания и состояние). Одно и то же понятие — разное решение в зависимости от того, что нужно системе.
Инструмент 3: простой атрибут или отдельный класс?
Решающий вопрос: нужно ли хранить об этой вещи больше одного куска информации или применять к ней правила?
| Понятие | Простой атрибут, когда… | Отдельный класс, когда… |
|---|---|---|
| Адрес | Достаточно одного текста: "Herzl 10, Haifa" | Нужны отдельно город, улица и индекс, фильтрация по городу и проверка индекса |
| Цена | Одно число | Нужны валюта, конвертации и округление по валюте |
| Оценка | Число | Нужны ещё дата, название экзамена, вес и апелляция |
Эмпирическое правило: начинайте просто (с атрибута). Когда значение «разрастается» и получает больше полей и правил, превращайте его в класс.
Инструмент 4: какому классу принадлежит метод?
Золотое правило: метод идёт в класс, который хранит нужные ему данные. (Это называется «Information Expert».)
- «Посчитать сумму заказа» → нужны позиции заказа →
Order.total(). - «Есть ли товар в наличии?» → нужно количество товара →
Product.is_in_stock(). - «Сколько клиент потратил в этом году?» → нужны заказы клиента →
Customer(если он хранит свои заказы) или класс, который управляет всеми заказами.
Тревожный сигнал: метод в классе A, который в основном состоит из other.x, other.y, other.z, → вероятно, принадлежит классу other.
Синтаксис
Нового синтаксиса нет. Результат этого этапа — проект, который можно записать как «скелет» кода, с pass и docstring:
class Order:
"""A customer's order: items, status and total."""
def __init__(self, customer):
self.customer = customer # has-a: the order holds its customer
self.items = []
self.status = "new"
def add_item(self, product, quantity):
pass
def total(self):
passТакой скелет позволяет думать о структуре до написания логики.
Первый пример кода
Требование: «Система для кофейни. Клиенты заказывают напитки из меню. У каждого напитка есть название, размер (small/large) и цена. Большой напиток стоит на 3 шекеля дороже. Заказ может включать несколько напитков, и у него есть сумма. Клиент может отменить заказ, пока он не приготовлен.»
Шаг А — разметка:
- Существительные: клиент, напиток, меню, заказ, название, размер, цена, сумма.
- Глаголы: заказывать, включать, отменять, приготовлен.
- Правила: большой = +3. Отмена только до приготовления.
Шаг Б — четыре вопроса:
| Кандидат | Идентичность/данные? | Поведение? | Несколько экземпляров? | Решение |
|---|---|---|---|---|
| Напиток | ✓ название, размер, цена | ✓ расчёт цены по размеру | ✓ | Класс Drink |
| Заказ | ✓ позиции, статус | ✓ сумма, отмена, приготовление | ✓ | Класс Order |
| Клиент | ✓ имя | Немного | ✓ | Здесь: атрибут (имя) у Order. В требовании нет для него поведения. |
| Меню | Список напитков | Возможно, поиск | Одно | Здесь: простой dict. Если добавятся правила, станет классом. |
| Размер | Значение («small»/«large») | — | — | Атрибут Drink |
| Цена | Число | — | — | Атрибут + вычисляющий метод |
| Сумма | Производная | — | — | Метод Order.total() |
Шаг В — методы («кто хранит данные?»):
price()→ Drink, потому что Drink знает базовую цену и размер.total()→ Order, потому что Order хранит напитки.cancel(),mark_prepared()→ Order, потому что Order хранит статус.
Шаг Г — код. week-3/coffee_shop.py:
LARGE_EXTRA = 3
class Drink:
def __init__(self, name, base_price, size="small"):
if size not in ("small", "large"):
raise ValueError("size must be small or large, got " + size)
self.name = name
self.base_price = base_price
self.size = size
def price(self):
if self.size == "large":
return self.base_price + LARGE_EXTRA
return self.base_price
class Order:
def __init__(self, customer_name):
self.customer_name = customer_name
self.drinks = []
self.status = "open"
def add(self, drink):
if self.status != "open":
raise ValueError("cannot add drinks to a " + self.status + " order")
self.drinks.append(drink)
def total(self):
return sum(drink.price() for drink in self.drinks)
def mark_prepared(self):
self.status = "prepared"
def cancel(self):
if self.status == "prepared":
raise ValueError("cannot cancel - the order was already prepared")
self.status = "cancelled"
MENU = {"espresso": 9, "latte": 14, "tea": 8}
order = Order("Dana")
order.add(Drink("latte", MENU["latte"], "large"))
order.add(Drink("espresso", MENU["espresso"]))
print(order.customer_name, "pays", order.total())
order.mark_prepared()
try:
order.cancel()
except ValueError as error:
print("Rejected:", error)Вывод:
Dana pays 26
Rejected: cannot cancel - the order was already preparedРазбор кода построчно
| Проектное решение | Как это выглядит в коде |
|---|---|
| Напиток = класс | class Drink с данными (название, цена, размер) и поведением (price). |
| Размер = атрибут с правилом | self.size с проверкой в __init__. |
| «Большой = +3» принадлежит напитку | Drink.price(), потому что напиток знает свой размер. |
| Заказ = класс | class Order с позициями и статусом. |
| Клиент = атрибут (пока) | self.customer_name, строка. |
| «Отмена только до приготовления» | Проверка в Order.cancel(). |
| Меню = dict | MENU. У него нет поведения, поэтому класс не нужен. |
| Сумма = метод | total() вычисляет по drinks и не хранится как данные, которые могут устареть. |
Ещё один пример
Когда требование меняется — клиент становится классом. Добавилось требование: «У клиентов есть клубная карта: каждая покупка приносит балл за каждые 10 шекелей, а при 50 баллах полагается бесплатный напиток.»
Теперь у клиента есть данные (баллы), поведение (накопление, списание) и правила (50 баллов). По четырём вопросам это уже класс:
POINTS_PER_SHEKEL = 0.1
FREE_DRINK_POINTS = 50
class Customer:
def __init__(self, name):
self.name = name
self.points = 0
def earn_points(self, amount_paid):
self.points = self.points + int(amount_paid * POINTS_PER_SHEKEL)
def has_free_drink(self):
return self.points >= FREE_DRINK_POINTS
def redeem_free_drink(self):
if not self.has_free_drink():
raise ValueError(self.name + " has only " + str(self.points) + " points")
self.points = self.points - FREE_DRINK_POINTS
dana = Customer("Dana")
for payment in [120, 260, 140]:
dana.earn_points(payment)
print(dana.name, "has", dana.points, "points. Free drink?", dana.has_free_drink())
dana.redeem_free_drink()
print("After redeeming:", dana.points)Вывод:
Dana has 52 points. Free drink? True
After redeeming: 2И в структуре: Order теперь хранит self.customer (объект Customer) вместо customer_name, а при оплате заказа вызывает self.customer.earn_points(self.total()). Одно и то же понятие, «клиент», начиналось как атрибут и стало классом, когда требования выросли. Это естественно, и это правильный рабочий процесс.
Итоги
- Существительные → кандидаты в классы/атрибуты. Глаголы → методы. Правила → проверки.
- Класс — когда есть идентичность + данные, поведение или правила и несколько экземпляров. Одно значение без поведения → атрибут.
- Одно и то же понятие может быть атрибутом в одной системе и классом в другой. Начинайте просто и повышайте, когда требования растут.
- Метод принадлежит классу, который хранит нужные ему данные (Information Expert).
- Иногда «действие» (кредит, заказ) на самом деле класс, когда у него есть собственные данные и логика.
- Не класс для каждого существительного и не «божественный класс» (God Class), который делает всё.
Официальная документация
Глава 7: Назад в банк — всё вместе
Что мы изучим?
Мы возвращаемся к банковской системе из начала урока и строим её заново — на этот раз со всем, что изучили в предыдущих главах.
Зачем это нужно?
Каждая глава добавляла один инструмент и решала одну проблему. Здесь видно, как все они работают вместе в одном классе, — и именно это вы сделаете самостоятельно в мини-проекте.
Банк — со всем, что мы изучили
class BankAccount:
accounts_opened = 0 # shared by all accounts
def __init__(self, owner, balance=0):
if balance < 0:
raise ValueError("balance cannot be negative")
self.owner = owner # per account
self.balance = balance
BankAccount.accounts_opened = BankAccount.accounts_opened + 1
def deposit(self, amount): # setter - changes the state
if amount <= 0:
raise ValueError("amount must be positive")
self.balance = self.balance + amount
def __str__(self):
return self.owner + ": " + str(self.balance)
dana = BankAccount("Dana", 1000)
dana.deposit(200)
print(dana) # Dana: 1200| Часть кода | Из какой главы |
|---|---|
class BankAccount — один шаблон для всех счетов | Глава 2 |
__init__ с проверкой — каждый счёт рождается полноценным и допустимым | Глава 3 |
deposit — сеттер, который защищает правила | Глава 4 |
__str__ — как счёт выглядит при печати | Глава 4 |
accounts_opened — счётчик, общий для всех счетов | Глава 5 |
Четыре проблемы из первой главы решены: нельзя создать счёт с отрицательным балансом, данные и действия находятся вместе, у структуры одно определение, и сам класс следит за соблюдением правил.
Итоги
- Класс — шаблон, а объект — экземпляр, построенный по нему, — с собственными данными.
__init__выполняется при создании и гарантирует, что объект рождается полноценным;self— текущий объект.- Методы — это действия: геттер возвращает информацию, сеттер меняет состояние и проверяет правила, а
__str__решает, что показываетprint. - Атрибут класса — одно значение, общее для всех объектов, — например, счётчик.
Официальная документация
Практика
Упражнения опираются только на то, что пройдено в этом уроке и в предыдущих. Сначала попробуйте сами, и только потом открывайте решение.
Что вы отрабатываете на этой странице: зачем нужно ООП, создание класса с __init__ и проверкой значений, методы (геттер и сеттер) и __str__, данные, общие для всего класса, и проектирование — что является классом, что атрибутом, а что методом.
К концу страницы вы будете уметь:
- Распознавать, когда данные и действия лучше объединить в один объект
- Писать полноценный класс:
__init__, методы и__str__ - Использовать атрибут класса как счётчик, общий для всех объектов
- Превращать словесное описание в список классов, атрибутов и методов
Упражнение 1: Найдите проблемы
Что вы делаете: читаете существующий код и находите в нём проблемы — без написания кода.
Код:
student1 = {"name": "Dana", "grades": [90, 85]}
student2 = {"name": "Avi", "grades": [70]}
student3 = {"nmae": "Noa", "grades": []}
def average(student):
return sum(student["grades"]) / len(student["grades"])Задание: найдите три проблемы, которые возникают из-за того, что данные (словари) и действия (функция) разделены. Для каждой проблемы напишите одно предложение: в чём проблема и в какой строке она проявляется.
Что сдать: три проблемы, по одному предложению на каждую.
Показать решение
- Опечатка создаёт новый ключ — у
student3написано"nmae"вместо"name". Нет определённой структуры, которая бы это поймала, и проблема обнаружится, только когда кто-то попробуетstudent3["name"]. - Нет правил — любой код может положить в
grades-50или"abc", и никто не проверит. - Данные и действия разбросаны —
average— отдельная функция, которая получает словарь и не знает, студент ли это вообще. А с пустым списком (student3) она упадёт сZeroDivisionError.
В ООП Student один раз определяет, какие данные у него есть и какие действия он умеет выполнять, — всё вместе.
Упражнение 2: Проверка при создании
Что вы строите: товар в магазине, который нельзя создать с отрицательной ценой.
Что написать:
- Класс с именем
Product. __init__, который получает название, цену и количество. Если количество не передано — оно равно 0.- Если цена отрицательная: выбросить
ValueErrorс сообщением, которое есть в выводе, и ничего не сохранять. - Иначе: сохранить три значения на объекте через
self.
Проверочный код:
pen = Product("Pen", 5)
print(pen.name, pen.price, pen.quantity)
try:
Product("Bad", -10)
except ValueError as error:
print("error:", error)Желаемый вывод:
Pen 5 0
error: price cannot be negativeПоказать решение
class Product:
def __init__(self, name, price, quantity=0):
if price < 0:
raise ValueError("price cannot be negative")
self.name = name
self.price = price
self.quantity = quantity
pen = Product("Pen", 5)
print(pen.name, pen.price, pen.quantity)
try:
Product("Bad", -10)
except ValueError as error:
print("error:", error)Pen 5 0
error: price cannot be negativeПроверка в __init__ гарантирует, что объекта Product с недопустимой ценой не будет никогда.
Упражнение 3: Student с методами и str
Что вы строите: студента, который сам управляет своими оценками.
Что написать:
- Класс с именем
Student.__init__получает имя и создаёт пустой список оценок. add_grade— получает оценку и добавляет её в список. Это сеттер: он меняет состояние.average— возвращает среднее или 0, если оценок ещё нет. Это геттер: он только возвращает.__str__— возвращает имя и среднее точно как в последней строке вывода.
Проверочный код:
dana = Student("Dana")
print(dana.average())
dana.add_grade(90)
dana.add_grade(75)
print(dana.average())
print(dana)Желаемый вывод:
0
82.5
Dana (average 82.5)Показать решение
class Student:
def __init__(self, name):
self.name = name
self.grades = []
def add_grade(self, grade):
self.grades.append(grade)
def average(self):
if len(self.grades) == 0:
return 0
return sum(self.grades) / len(self.grades)
def __str__(self):
return self.name + " (average " + str(self.average()) + ")"
dana = Student("Dana")
print(dana.average())
dana.add_grade(90)
dana.add_grade(75)
print(dana.average())
print(dana)0
82.5
Dana (average 82.5)add_grade — сеттер, он меняет состояние. average — геттер, он возвращает информацию, ничего не меняя. print вызывает __str__ автоматически.
Упражнение 4: Общий счётчик
Что вы строите: товар, который знает свою цену с НДС, — и класс, который считает, сколько товаров создано.
Что написать:
- Класс с именем
Productс__init__, который получает название и цену. - Два атрибута класса — данные, принадлежащие всему классу, а не одному объекту:
VAT_RATEсо значением 0.17 иcreated_count, который начинается с 0. - Каждый раз, когда создаётся товар, увеличивайте
created_countна 1. price_with_vat— возвращает цену с НДС.
Проверочный код:
laptop = Product("Laptop", 1000)
mouse = Product("Mouse", 50)
print(Product.created_count)
print(laptop.price_with_vat())Желаемый вывод:
2
1170.0Показать решение
class Product:
VAT_RATE = 0.17
created_count = 0
def __init__(self, name, price):
self.name = name
self.price = price
Product.created_count = Product.created_count + 1
def price_with_vat(self):
return round(self.price * (1 + Product.VAT_RATE), 2)
laptop = Product("Laptop", 1000)
mouse = Product("Mouse", 50)
print(Product.created_count)
print(laptop.price_with_vat())2
1170.0nameиpriceпринадлежат каждому товару отдельно — атрибуты экземпляра.VAT_RATEиcreated_countобщие для всех товаров — атрибуты класса.- Счётчик обновляется через
Product.created_count, а не черезself.
Упражнение 5: Существительные и глаголы
Что вы делаете: только проектирование, без кода — превращаете словесное описание в список классов, атрибутов и методов.
Требование:
«В кофейне клиенты заказывают напитки разных размеров. У каждого напитка есть базовая цена, а большой напиток стоит на 3 шекеля дороже. Клиент накапливает баллы за каждый заказ и может их списать.»
Задание: постройте таблицу из трёх столбцов: кандидат, что это (класс, атрибут или метод) и почему.
Подсказка: существительные — кандидаты в классы или атрибуты. Глаголы — кандидаты в методы.
Что сдать: таблицу со всеми кандидатами из требования.
Показать решение
| Кандидат | Что это | Почему |
|---|---|---|
Клиент (Customer) | Класс | Существительное с данными (имя, баллы) и поведением (накапливает, списывает) |
Напиток (Drink) | Класс | Существительное с данными (базовая цена, размер) и правилом (большой стоит дороже) |
| Базовая цена, размер | Атрибуты Drink | Простые значения, которые описывают напиток |
| Баллы | Атрибут Customer | Простое число |
| Накапливает баллы, списывает | Методы Customer | Глаголы — действия с данными клиента |
| Итоговая цена | Метод Drink | Вычисляется из цены и размера |
Мини-проект: Маленькая библиотека
Какую систему вы строите: маленькую систему для библиотеки. В библиотеке есть книги, и у каждой книги несколько экземпляров. Читатели берут экземпляры и возвращают их, и в каждый момент система знает, сколько свободных экземпляров каждой книги — и сколько всего книг в библиотеке.
Чему вы научитесь в проекте:
- Строить полноценный класс с нуля:
__init__, методы и__str__ - Проверять значения уже при создании и выбрасывать ошибку, когда они недопустимы
- Различать сеттер, который меняет состояние, и геттер, который только возвращает информацию
- Использовать атрибут класса как счётчик, общий для всех объектов
Что построить — шаг за шагом:
Шаг 1 — класс и общий счётчик. Напишите класс с именем Book с атрибутом класса total_books, который начинается с 0. Он будет считать, сколько книг создано.
Шаг 2 — создание книги. __init__ получает название, автора и число экземпляров и сохраняет их. В начале все экземпляры свободны — сохраните их также в available. Каждая новая книга увеличивает total_books на 1. Если число экземпляров меньше 1 — выбросьте ValueError и не создавайте книгу.
Шаг 3 — выдача и возврат (сеттеры).
borrow— забирает один свободный экземпляр. Если свободных экземпляров нет:ValueError.return_book— возвращает один экземпляр. Если все экземпляры уже в библиотеке:ValueError.
Шаг 4 — проверка (геттер). is_available возвращает True, если есть хотя бы один свободный экземпляр, иначе False.
Шаг 5 — отображение. __str__ возвращает название, автора и сколько экземпляров свободно из скольких, как в выводе.
Сообщения об ошибках — именно такие:
copies must be at least 1
no copies available
all copies are already inПроверочный код:
books = [
Book("Clean Code", "Martin", 3),
Book("Python Basics", "Cohen", 1),
]
books[0].borrow()
books[1].borrow()
try:
books[1].borrow()
except ValueError as error:
print("Python Basics ->", error)
try:
Book("Empty", "Nobody", 0)
except ValueError as error:
print("Empty ->", error)
for book in books:
print(book, "| available:", book.is_available())
print("Books in the library:", Book.total_books)Желаемый вывод:
Python Basics -> no copies available
Empty -> copies must be at least 1
Clean Code by Martin (2/3 available) | available: True
Python Basics by Cohen (0/1 available) | available: False
Books in the library: 2К концу проекта вы будете уметь:
- Переводить описание «вещи» из мира — книги — в класс с данными и действиями
- Держать правила внутри объекта, чтобы он не мог попасть в недопустимое состояние
- Понимать, почему книга Empty не посчитана счётчиком: ошибка была выброшена до увеличения
Бонус: добавьте класс с именем Library, который хранит список книг, с add_book, который добавляет книгу, и available_books, который возвращает только книги, которые можно взять прямо сейчас.
Показать решение
class Book:
total_books = 0
def __init__(self, title, author, copies):
if copies < 1:
raise ValueError("copies must be at least 1")
self.title = title
self.author = author
self.copies = copies
self.available = copies
Book.total_books = Book.total_books + 1
def borrow(self):
if self.available == 0:
raise ValueError("no copies available")
self.available = self.available - 1
def return_book(self):
if self.available == self.copies:
raise ValueError("all copies are already in")
self.available = self.available + 1
def is_available(self):
return self.available > 0
def __str__(self):
return (self.title + " by " + self.author + " ("
+ str(self.available) + "/" + str(self.copies) + " available)")
books = [
Book("Clean Code", "Martin", 3),
Book("Python Basics", "Cohen", 1),
]
books[0].borrow()
books[1].borrow()
try:
books[1].borrow()
except ValueError as error:
print("Python Basics ->", error)
try:
Book("Empty", "Nobody", 0)
except ValueError as error:
print("Empty ->", error)
for book in books:
print(book, "| available:", book.is_available())
print("Books in the library:", Book.total_books)Python Basics -> no copies available
Empty -> copies must be at least 1
Clean Code by Martin (2/3 available) | available: True
Python Basics by Cohen (0/1 available) | available: False
Books in the library: 2total_books— атрибут класса: один счётчик для всех книг. КнигаEmptyне была создана (ошибка выброшена до увеличения), поэтому счётчик равен 2.borrowиreturn_book— сеттеры: они меняют состояние и проверяют правила до изменения.is_available— геттер: возвращает информацию, ничего не меняя.
Бонус:
class Library:
def __init__(self):
self.books = []
def add_book(self, book):
self.books.append(book)
def available_books(self):
result = []
for book in self.books:
if book.is_available():
result.append(book)
return result
library = Library()
for book in books:
library.add_book(book)
for book in library.available_books():
print(book)Clean Code by Martin (2/3 available)