Модуль 8 · Урок 8.1
Классы
Свой тип данных: экземпляры и self, атрибуты, свойства, наследование, композиция и dataclass.
Дополнительный урок — для продвинутых. Он для тех, кто хочет писать программы больше экзаменационных: в темах ЕГЭ классы не встречаются. Задачи основных уроков решаются без него.
Ты читаешь несколько книг и хочешь вести учёт: название, число страниц, текущая страница. Словарь на книгу хранит данные, но правило «продвинуться, не перескочив последнюю страницу» живёт где-то отдельно, и его легко нарушить. Класс описывает новый тип объектов: какие данные хранит каждый объект и какие действия с ними разрешены.
Класс и экземпляр
Класс — описание нового типа, экземпляр — конкретный объект этого типа. Опишем класс прогресса чтения и создадим две книги:
class ReadingProgress: def __init__(self, title, total_pages): self.title = title self.total_pages = total_pages self.current_page = 0 def advance(self, pages): self.current_page = min(self.total_pages, self.current_page + pages) book = ReadingProgress("Python", 200)another_book = ReadingProgress("Алгоритмы", 300)book.advance(30)print(book.title, book.current_page)print(another_book.title, another_book.current_page)class описывает тип. Вызов ReadingProgress("Python", 200) создаёт новый объект — экземпляр — и сразу вызывает для него __init__: этот специальный метод задаёт начальные данные. Метод — функция внутри класса, как advance. У каждого экземпляра свои данные, а методы общие:
advance изменил страницу только у book: при вызове Python передаёт сам объект первым аргументом.Так устроено объектно-ориентированное программирование (ООП): программа описывает сущности со своим состоянием и действиями. Для суммы чисел класс не нужен — он полезен, когда сущностей много и у них есть правила.
self и атрибуты
self — экземпляр, для которого выполняется метод. При book.advance(30) Python подставляет book в первый параметр: вызов означает ReadingProgress.advance(book, 30). Имя self — соглашение, но его пишут всегда.
Атрибут — значение, привязанное к объекту: self.title, self.current_page. Он живёт, пока жив объект. Параметр pages — обычная локальная переменная: после вызова её нет.
Теперь второй шаг: класс сам охраняет свои правила. Число страниц должно быть положительным, читать назад нельзя, а доля прочитанного — новый метод:
class ReadingProgress: def __init__(self, title, total_pages): if total_pages <= 0: raise ValueError("Страниц должно быть больше нуля") self.title = title self.total_pages = total_pages self.current_page = 0 def advance(self, pages): if pages < 0: raise ValueError("Нельзя читать назад") self.current_page = min(self.total_pages, self.current_page + pages) def fraction(self): return self.current_page / self.total_pages book = ReadingProgress("Python", 200)book.advance(30)print(book.fraction())book.advance(-5)Проверки стоят внутри методов, поэтому их не обойти: любая книга создаётся через __init__ и продвигается через advance. Ошибки — тот же raise ValueError, что в уроке об исключениях.
Атрибут класса
Атрибут экземпляра у каждого объекта свой, атрибут класса — один на всех. Атрибут экземпляра создают в __init__ через self. Переменная в теле класса вне методов — атрибут класса: его видят все экземпляры. Для общей константы это удобно, а для списка — ловушка. Сравни:
class Notebook: def __init__(self): self.notes = [] first = Notebook()second = Notebook()first.notes.append("Циклы")print(second.notes)__init__ создал свой список: заметка попала только к first.class Notebook: notes = [] first = Notebook()second = Notebook()first.notes.append("Циклы")print(second.notes)first.notes и second.notes — два имени одного объекта, как b = a в уроке о списках.Инкапсуляция и property
Класс держит правила изменения данных рядом с самими данными. Это и есть инкапсуляция: остальная программа меняет страницу через advance, а не присваиванием в разных местах. Имя с подчёркиванием, например _current_page, по соглашению значит «внутренняя деталь, снаружи не трогать»; запретом это не является.
Тонкости:два подчёркивания
Имя с двумя начальными подчёркиваниями, например __page, Python переименовывает внутри класса, чтобы оно случайно не совпало с именем в классе-потомке. Защитой секретных данных это тоже не является.
@property позволяет читать вычисляемое свойство как атрибут:
class Rectangle: def __init__(self, width, height): self.width = width self.height = height @property def area(self): return self.width * self.height rectangle = Rectangle(3, 5)print(rectangle.area)area читается без скобок, но каждый раз вычисляется заново из ширины и высоты. Присвоить rectangle.area = 10 нельзя: свойство только для чтения, Python ответит AttributeError.
Наследование и композиция
Наследование уточняет поведение, композиция собирает объект из других объектов. Наследование создаёт класс на основе другого: задача — это именованный предмет, у которого есть ещё отметка о выполнении. Наследование уместно, если новый объект можно использовать везде, где ждут базовый.
class NamedItem: def __init__(self, name): self.name = name def label(self): return self.name class Task(NamedItem): def __init__(self, name, done=False): super().__init__(name) self.done = done def label(self): mark = "✓" if self.done else "○" return f"{mark} {self.name}" for item in [NamedItem("Книга"), Task("Прочитать", True)]: print(item.label())class Task(NamedItem) — наследование: Task получает всё от NamedItem. super().__init__(name) вызывает __init__ базового класса, чтобы не повторять его код.
Тонкости:super при нескольких родителях
Класс может наследовать сразу от нескольких. Тогда super() ведёт не обязательно к прямому родителю, а к следующему классу в порядке поиска методов.
label переопределён: одинаковый вызов на разных объектах может выполнять разную реализацию. Это пример полиморфизма — работы через общую форму действия. В Python для него не обязательно общее наследование: достаточно, чтобы объект поддерживал нужную операцию.
Композиция означает, что объект содержит другие объекты. Например, учебный курс содержит список уроков. Для отношения «содержит» обычно естественна композиция, а не наследование «курс является уроком».
Специальные методы и dataclass
Специальные методы связывают класс с обычными средствами языка, а dataclass пишет типовой код за нас. __init__ — первый из них. Ещё: __repr__ задаёт представление для разработчика, __str__ — текстовое представление, __len__ — поведение len, __eq__ — сравнение через ==. Свои имена с двумя подчёркиваниями с двух сторон не придумывают: Python знает только свой список.
Если класс в основном хранит запись, модуль dataclasses сокращает однотипный код:
from dataclasses import dataclass @dataclassclass Point: x: float y: float p = Point(1.0, 2.0)q = Point(1.0, 2.0)print(p)print(p == q)Декоратор сам пишет __init__, __repr__ и __eq__ по списку полей. Запись x: float объявляет поле и ожидаемый тип; подробнее о таких записях — в уроке об аннотациях. Число в float она не превращает.
Для поля-списка нужна фабрика, иначе список окажется общим, как атрибут класса выше:
from dataclasses import dataclass, field @dataclassclass Course: name: str lessons: list = field(default_factory=list) python = Course("Python")excel = Course("Excel")python.lessons.append("Циклы")print(python)print(excel.lessons)default_factory=list вызывает list() для каждого нового экземпляра: у каждого курса свой список уроков.
Итог
Класс собирает данные и правила их изменения в один тип; экземпляров может быть сколько угодно. Вот что понадобится дальше.
- Класс и экземпляр
classописывает тип; вызов класса создаёт экземпляр,__init__задаёт начальные данные. selfЭкземпляр, для которого вызван метод;self.x— его атрибут.- Атрибут классаОдин на всех: список в теле класса общий, свои данные создают в
__init__. @propertyВычисляемое свойство без скобок; наследование уточняет класс, композиция — «содержит».@dataclass__init__,__repr__,__eq__по полям; список — черезdefault_factory.
Проверьте себя
Ответь сначала сам, потом открой ответ.
Когда для точки лучше кортеж, а когда класс?
ОтветСкрыть ответ
Для короткого численного алгоритма (x, y) может быть достаточно. Если важны понятные имена полей, дополнительные правила и действия с точками в большой программе, класс делает договорённость явной. Наличие класса само по себе не повышает качество; важно, уменьшает ли он сложность задачи.
Как записать вызов book.advance(30) через имя класса?
ОтветСкрыть ответ
ReadingProgress.advance(book, 30).
Чему равно Rectangle(4, 6).area? Что случится при rectangle.area = 10?
ОтветСкрыть ответ
24; присваивание вызовет AttributeError — у свойства нет способа записи.
Что вернёт Task("Читать").label()?
ОтветСкрыть ответ
○ Читать.
Что выведут print(Point(0, 1)) и Point(0, 1) == Point(0, 1.0)?
ОтветСкрыть ответ
Point(x=0, y=1) и True: 1 == 1.0.
Практика
Для проверки ответов нужен JavaScript. Условия, подсказки и решения доступны ниже.
Что напечатает программа?
class ReadingProgress: def __init__(self, title, total_pages): self.title = title self.total_pages = total_pages self.current_page = 0 def advance(self, pages): self.current_page = min(self.total_pages, self.current_page + pages) def fraction(self): return self.current_page / self.total_pages book = ReadingProgress("Python", 200)book.advance(50)book.advance(170)print(book.current_page, book.fraction())Запишите напечатанные значения через пробел в порядке вывода.
Подсказка
min не даёт перескочить последнюю страницу: 50 + 170 больше 200.
Разбор
После второго advance страница стала бы 220, но min(200, 220) = 200. Доля 200 / 200 = 1.0.
Ответ: 200 1.0
Что напечатает программа?
class Bad: notes = [] a = Bad()b = Bad()a.notes.append(1)print(b.notes)Запишите список так, как его печатает Python.
Подсказка
Список создан в теле класса, а не в __init__: он один на все экземпляры.
Разбор
a.notes и b.notes — два имени одного списка, поэтому добавленная единица видна через b.
Ответ: [1]
Напиши класс: кошелёк
Напиши класс Wallet: атрибут balance (сначала 0), метод deposit(amount) пополняет баланс, метод withdraw(amount) снимает деньги и вызывает ValueError, если их не хватает. Создай кошелёк, выполни deposit(100) и withdraw(30), напечатай balance, запусти и запиши, что напечатала программа.
Введите число.
Подсказка
Проверка в withdraw идёт до изменения баланса: if amount > self.balance: raise ValueError(…).
Разбор
Например:
class Wallet: def __init__(self): self.balance = 0 def deposit(self, amount): self.balance += amount def withdraw(self, amount): if amount > self.balance: raise ValueError("Недостаточно денег") self.balance -= amount wallet = Wallet()wallet.deposit(100)wallet.withdraw(30)print(wallet.balance)100 − 30 = 70.
Ответ: 70