К содержанию

Модуль 8 · Урок 8.1

Классы

Свой тип данных: экземпляры и self, атрибуты, свойства, наследование, композиция и dataclass.

3 задачи

Дополнительный урок — для продвинутых. Он для тех, кто хочет писать программы больше экзаменационных: в темах ЕГЭ классы не встречаются. Задачи основных уроков решаются без него.

Ты читаешь несколько книг и хочешь вести учёт: название, число страниц, текущая страница. Словарь на книгу хранит данные, но правило «продвинуться, не перескочив последнюю страницу» живёт где-то отдельно, и его легко нарушить. Класс описывает новый тип объектов: какие данные хранит каждый объект и какие действия с ними разрешены.

Класс и экземпляр

Класс — описание нового типа, экземпляр — конкретный объект этого типа. Опишем класс прогресса чтения и создадим две книги:

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)
Python 30 Алгоритмы 0

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)
0.15 ValueError: Нельзя читать назад

Проверки стоят внутри методов, поэтому их не обойти: любая книга создаётся через __init__ и продвигается через advance. Ошибки — тот же raise ValueError, что в уроке об исключениях.

Атрибут класса

Атрибут экземпляра у каждого объекта свой, атрибут класса — один на всех. Атрибут экземпляра создают в __init__ через self. Переменная в теле класса вне методов — атрибут класса: его видят все экземпляры. Для общей константы это удобно, а для списка — ловушка. Сравни:

в __init__
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)
15

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)
Point(x=1.0, y=2.0) True

Декоратор сам пишет __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)
Course(name='Python', lessons=['Циклы']) []

default_factory=list вызывает list() для каждого нового экземпляра: у каждого курса свой список уроков.

Итог

Класс собирает данные и правила их изменения в один тип; экземпляров может быть сколько угодно. Вот что понадобится дальше.

  • Класс и экземплярclass описывает тип; вызов класса создаёт экземпляр, __init__ задаёт начальные данные.
  • selfЭкземпляр, для которого вызван метод; self.x — его атрибут.
  • Атрибут классаОдин на всех: список в теле класса общий, свои данные создают в __init__.
  • @propertyВычисляемое свойство без скобок; наследование уточняет класс, композиция — «содержит».
  • @dataclass__init__, __repr__, __eq__ по полям; список — через default_factory.

Проверьте себя

Ответь сначала сам, потом открой ответ.

Вопрос 1

Когда для точки лучше кортеж, а когда класс?

ОтветСкрыть ответ
Ответ

Для короткого численного алгоритма (x, y) может быть достаточно. Если важны понятные имена полей, дополнительные правила и действия с точками в большой программе, класс делает договорённость явной. Наличие класса само по себе не повышает качество; важно, уменьшает ли он сложность задачи.

Вопрос 2

Как записать вызов book.advance(30) через имя класса?

ОтветСкрыть ответ
Ответ

ReadingProgress.advance(book, 30).

Вопрос 3

Чему равно Rectangle(4, 6).area? Что случится при rectangle.area = 10?

ОтветСкрыть ответ
Ответ

24; присваивание вызовет AttributeError — у свойства нет способа записи.

Вопрос 4

Что вернёт Task("Читать").label()?

ОтветСкрыть ответ
Ответ

○ Читать.

Вопрос 5

Что выведут 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())

Запишите напечатанные значения через пробел в порядке вывода.

Что напечатает программа?

class Bad:    notes = [] a = Bad()b = Bad()a.notes.append(1)print(b.notes)

Запишите список так, как его печатает Python.

Напиши класс: кошелёк

Напиши класс Wallet: атрибут balance (сначала 0), метод deposit(amount) пополняет баланс, метод withdraw(amount) снимает деньги и вызывает ValueError, если их не хватает. Создай кошелёк, выполни deposit(100) и withdraw(30), напечатай balance, запусти и запиши, что напечатала программа.

Введите число.