Python & AI Tutorials Logo
Программирование Python

30. Введение в классы и объекты

30.1) Идея объектно-ориентированного программирования (создание собственных типов)

На протяжении этой книги вы работали со встроенными типами Python: целыми числами, строками, списками, словарями и другими. Каждый тип объединяет данные (например, символы в строке) с операциями, которые можно выполнять над этими данными (например, .upper() или .split()). Такое сочетание данных и поведения очень мощное — оно позволяет думать о строках как о полноценных сущностях с собственными возможностями, а не просто как о «сырых» последовательностях символов.

Объектно-ориентированное программирование (OOP) развивает эту идею: оно позволяет создавать собственные пользовательские типы, называемые классами, которые объединяют данные и поведение, специфичные для вашей предметной области. Так же как Python предоставляет тип str для работы с текстом и тип list для работы с последовательностями, вы можете создать тип BankAccount для управления финансовыми транзакциями, тип Student для отслеживания учебных результатов или тип Product для системы учета складских запасов.

Зачем создавать собственные типы?

Рассмотрим управление информацией о студентах в школьной системе. Без классов вы могли бы использовать отдельные переменные или словари:

python
# Использование отдельных переменных — быстро становится запутанным и неудобным
student1_name = "Alice Johnson"
student1_id = "S12345"
student1_gpa = 3.8
 
student2_name = "Bob Smith"
student2_id = "S12346"
student2_gpa = 3.5
 
# Или использование словарей — лучше, но все равно ограниченно
student1 = {"name": "Alice Johnson", "id": "S12345", "gpa": 3.8}
student2 = {"name": "Bob Smith", "id": "S12346", "gpa": 3.5}

Этот подход работает для простых случаев, но у него есть ограничения:

  1. Нет валидации: ничто не мешает установить gpa в недопустимое значение вроде -5.0 или "excellent"
  2. Нет связанного поведения: операции вроде вычисления статуса «с отличием» или форматирования информации о студенте — это отдельные функции, разбросанные по всему коду
  3. Нет проверки типов: словарь, представляющий студента, выглядит так же, как любой другой словарь — Python не поможет вам поймать ошибки, когда вы случайно используете словарь продукта там, где ожидался словарь студента

Классы решают эти проблемы, позволяя определить новый тип, который представляет ровно то, чем является студент, и какие операции имеют смысл для студентов:

python
# Мы будем двигаться к этому — класс Student, который объединяет данные и поведение
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
    
    def is_honors(self):
        return self.gpa >= 3.5
    
    def display_info(self):
        status = "Honors" if self.is_honors() else "Regular"
        return f"{self.name} ({self.student_id}) - GPA: {self.gpa} [{status}]"
 
# Теперь мы можем создавать объекты-студенты
alice = Student("Alice Johnson", "S12345", 3.8)
bob = Student("Bob Smith", "S12346", 3.5)
 
print(alice.display_info())  # Output: Alice Johnson (S12345) - GPA: 3.8 [Honors]
print(bob.is_honors())       # Output: True

Эта глава научит вас создавать такие классы с нуля. Мы начнем с самых простых классов и постепенно будем добавлять возможности, пока вы не сможете создавать богатые и полезные пользовательские типы.

Классы и экземпляры: аналогия с чертежом

Понимание различия между классом и экземпляром — фундаментально для объектно-ориентированного программирования:

  • Класс похож на чертеж или шаблон. Он определяет, какие данные будет хранить объект этого типа и какие операции он сможет выполнять. Сам класс — это не конкретный студент; это определение того, что значит быть студентом.

  • Экземпляр (также называемый объектом) — это конкретный пример, созданный по этому чертежу. Когда вы создаете alice = Student("Alice Johnson", "S12345", 3.8), вы создаете один конкретный экземпляр студента с данными именно Алисы.

Класс Student
Чертеж

экземпляр alice
Имя: Alice Johnson
ID: S12345
GPA: 3.8

экземпляр bob
Имя: Bob Smith
ID: S12346
GPA: 3.5

экземпляр carol
Имя: Carol Davis
ID: S12347
GPA: 3.9

Вы можете создать столько экземпляров, сколько нужно, из одного класса — так же, как архитектор может использовать один чертеж, чтобы построить много домов. У каждого экземпляра свои данные (GPA Алисы отличается от GPA Боба), но все они разделяют одну и ту же структуру и возможности, определенные классом.

Что вы изучите в этой главе

Эта глава знакомит с основными концепциями объектно-ориентированного программирования в Python:

  1. Определение классов с ключевым словом class
  2. Создание экземпляров и доступ к их атрибутам
  3. Добавление методов, которые работают с данными экземпляра
  4. Понимание self и то, как методы получают доступ к данным экземпляра
  5. Инициализация экземпляров с методом __init__
  6. Управление строковыми представлениями с __str__ и __repr__
  7. Создание нескольких независимых экземпляров из одного и того же класса

К концу этой главы вы сможете проектировать и реализовывать собственные пользовательские типы, которые сделают ваши программы более организованными, поддерживаемыми и выразительными. Мы продолжим развивать эти основы в главе 31, добавив более продвинутые возможности классов, а в главе 32 рассмотрим наследование и полиморфизм.

30.2) Определение простых классов с class

Начнем с создания самого простого возможного класса — такого, который пока лишь определяет новый тип без данных и поведения.

Ключевое слово class

Класс определяется с помощью ключевого слова class, за которым следует имя класса и двоеточие:

python
class Student:
    pass  # Пока что пустой класс
 
# Создаем экземпляр
alice = Student()
print(alice)  # Output: <__main__.Student object at 0x...>
print(type(alice))  # Output: <class '__main__.Student'>

Даже этот минимальный класс полезен — он создает новый тип с именем Student. Когда вы создаете экземпляр с alice = Student(), Python создает новый объект типа Student. Вывод показывает, что alice действительно является объектом Student, хотя пока он не делает ничего интересного.

Соглашения об именовании классов

Имена классов в Python следуют определенному соглашению, называемому CapWords или PascalCase: каждое слово начинается с заглавной буквы, без подчеркиваний между словами:

python
class BankAccount:      # Хорошо: CapWords
    pass
 
class ProductInventory:  # Хорошо: CapWords
    pass
 
class HTTPRequest:      # Хорошо: аббревиатуры пишутся заглавными
    pass
 
# Избегайте этих стилей для классов:
# class bank_account:   # Wrong: snake_case is for functions/variables
# class bankaccount:    # Wrong: трудно читать
# class BANKACCOUNT:    # Wrong: ALL_CAPS is for constants

Это соглашение помогает отличать классы от функций и переменных (которые используют snake_case) при чтении кода.

Создание экземпляров

Создание экземпляра из класса выглядит как вызов функции — вы используете имя класса, за которым следуют круглые скобки:

python
class Product:
    pass
 
# Создаем три разных экземпляра продукта
item1 = Product()
item2 = Product()
item3 = Product()
 
# Каждый экземпляр — отдельный объект
print(item1)  # Output: <__main__.Product object at 0x...>
print(item2)  # Output: <__main__.Product object at 0x...>
print(item3)  # Output: <__main__.Product object at 0x...>
 
# Это разные объекты, хотя тип у них одинаковый
print(item1 is item2)  # Output: False
print(type(item1) is type(item2))  # Output: True

Каждый вызов Product() создает новый независимый экземпляр. Адреса в памяти (часть 0x...) разные, что подтверждает: это отдельные объекты в памяти.

Зачем начинать с пустых классов?

Возможно, вам интересно, почему мы начинаем с классов, которые ничего не делают. Есть две причины:

  1. Концептуальная ясность: понимание того, что класс — это просто новый тип, отдельный от его данных и поведения, помогает усвоить базовую концепцию до добавления сложности.

  2. Практическая польза: даже пустые классы могут быть полезны как маркеры или заглушки. Например, вы можете определять собственные типы исключений:

python
class InvalidGradeError:
    pass
 
class StudentNotFoundError:
    pass
 
# Эти пустые классы служат различимыми типами ошибок

Однако пустые классы редко встречаются в реальном коде. Давайте добавим данные, чтобы наши классы стали полезными.

30.3) Создание экземпляров и доступ к атрибутам

Классы становятся полезными, когда они хранят данные. В Python вы можете добавлять атрибуты(attributes) (данные, прикрепленные к экземпляру) в любой момент, просто присваивая им значения.

Добавление атрибутов экземплярам

Вы можете добавлять атрибуты экземпляру с помощью точечной нотации:

python
class Student:
    pass
 
# Создаем экземпляр
alice = Student()
 
# Добавляем атрибуты
alice.name = "Alice Johnson"
alice.student_id = "S12345"
alice.gpa = 3.8
 
# Доступ к атрибутам
print(alice.name)        # Output: Alice Johnson
print(alice.student_id)  # Output: S12345
print(alice.gpa)         # Output: 3.8

Оператор точка (.) обращается к атрибутам: alice.name означает «получить атрибут name объекта alice». Это тот же синтаксис, который вы использовали со строками (например, text.upper()) и списками (например, numbers.append(5)) — там вы обращались к методам и атрибутам этих объектов.

У каждого экземпляра свои атрибуты

Разные экземпляры одного и того же класса имеют независимые атрибуты:

python
class Student:
    pass
 
# Создаем двух студентов
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
bob = Student()
bob.name = "Bob Smith"
bob.gpa = 3.5
 
# У каждого экземпляра свои данные
print(alice.name)  # Output: Alice Johnson
print(bob.name)    # Output: Bob Smith
 
# Изменение одного не влияет на другого
alice.gpa = 3.9
print(alice.gpa)  # Output: 3.9
print(bob.gpa)    # Output: 3.5 (unchanged)

Эта независимость критически важна: alice и bob — отдельные объекты с отдельными данными. Изменение alice.gpa не влияет на bob.gpa.

Атрибуты могут быть любого типа

Атрибуты не ограничены простыми типами — они могут хранить любое значение Python:

python
class Student:
    pass
 
student = Student()
student.name = "Carol Davis"
student.grades = [95, 88, 92, 90]  # Атрибут-список
student.contact = {                 # Атрибут-словарь
    "email": "carol@example.com",
    "phone": "555-0123"
}
student.is_active = True            # Булевый атрибут
 
# Доступ к вложенным данным
print(student.grades[0])           # Output: 95
print(student.contact["email"])    # Output: carol@example.com

Эта гибкость позволяет моделировать сложные реальные сущности с богатыми структурами данных.

Доступ к несуществующим атрибутам

Попытка обратиться к атрибуту, которого не существует, вызывает AttributeError:

python
class Student:
    pass
 
student = Student()
student.name = "David Lee"
 
print(student.name)  # Output: David Lee
# print(student.age)  # AttributeError: 'Student' object has no attribute 'age'

Эта ошибка полезна — она помогает обнаруживать опечатки и логические ошибки, когда вы ожидаете, что атрибут существует, но его нет.

Проблема ручного присваивания атрибутов

Хотя вы можете добавлять атрибуты вручную после создания экземпляра, у этого подхода есть серьезные недостатки:

python
class Student:
    pass
 
# Легко забыть атрибуты или сделать в них опечатку
alice = Student()
alice.name = "Alice Johnson"
alice.student_id = "S12345"
# Forgot to set gpa!
 
bob = Student()
bob.name = "Bob Smith"
bob.stuent_id = "S12346"  # Typo: stuent instead of student
bob.gpa = 3.5
 
# Now alice is missing gpa, and bob has a typo
# print(alice.gpa)  # AttributeError
# print(bob.student_id)  # AttributeError

Это подвержено ошибкам и утомительно. Вам нужен способ гарантировать, что каждый экземпляр начинается с правильными атрибутами. Здесь и появляется метод __init__, который мы рассмотрим в разделе 30.5. Но сначала давайте разберемся с методами — функциями, которые принадлежат классу.

30.4) Добавление методов экземпляра: понимание self

Методы — это функции, определенные внутри класса, которые работают с данными экземпляра. Они дают вашим классам поведение, а не только данные.

Определение простого метода

Давайте добавим метод в наш класс Student:

python
class Student:
    def display_info(self):
        print(f"{self.name} - GPA: {self.gpa}")
 
# Создаем экземпляр и добавляем атрибуты
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
# Вызываем метод
alice.display_info()  # Output: Alice Johnson - GPA: 3.8

Метод display_info определяется внутри класса с помощью def, как и обычные функции. Ключевое отличие — первый параметр: self.

Понимание self

Параметр self — это способ, которым метод получает доступ к конкретному экземпляру, над которым он работает. Когда вы вызываете alice.display_info(), Python автоматически передает alice как первый аргумент методу. Внутри метода self ссылается на alice, поэтому self.name обращается к alice.name, а self.gpa — к alice.gpa.

Вот что происходит «за кулисами»:

python
class Student:
    def display_info(self):
        print(f"{self.name} - GPA: {self.gpa}")
 
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
# Эти два вызова эквивалентны:
alice.display_info()           # Обычный способ
Student.display_info(alice)    # То, что реально делает Python
 
# Оба выводят: Alice Johnson - GPA: 3.8

Когда вы пишете alice.display_info(), Python преобразует это в Student.display_info(alice). Экземпляр (alice) становится параметром self внутри метода.

Почему «self»?

Имя self — это соглашение, а не ключевое слово. Технически можно использовать любое имя:

python
class Student:
    def display_info(this):  # Работает, но так делать не стоит
        print(f"{this.name} - GPA: {this.gpa}")

Однако всегда используйте self. Это универсальное соглашение в Python, которое делает ваш код читаемым для других разработчиков на Python. Использование любого другого имени будет сбивать читателей с толку и нарушать стандарты сообщества.

Методы с несколькими экземплярами

Сила self становится очевидной, когда у вас есть несколько экземпляров:

python
class Student:
    def display_info(self):
        print(f"{self.name} - GPA: {self.gpa}")
 
# Создаем двух студентов
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
bob = Student()
bob.name = "Bob Smith"
bob.gpa = 3.5
 
# Один и тот же метод, разные данные
alice.display_info()  # Output: Alice Johnson - GPA: 3.8
bob.display_info()    # Output: Bob Smith - GPA: 3.5

Когда вы вызываете alice.display_info(), self — это alice. Когда вы вызываете bob.display_info(), self — это bob. Один и тот же код метода работает для любого экземпляра, потому что self адаптируется к тому экземпляру, который его вызвал.

alice.display_info

self = alice

bob.display_info

self = bob

Доступ к alice.name
alice.gpa

Доступ к bob.name
bob.gpa

Методы могут принимать дополнительные параметры

Методы могут принимать параметры помимо self:

python
class Student:
    def update_gpa(self, new_gpa):
        self.gpa = new_gpa
        print(f"Updated {self.name}'s GPA to {self.gpa}")
 
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
alice.update_gpa(3.9)  # Output: Updated Alice Johnson's GPA to 3.9
print(alice.gpa)       # Output: 3.9

Когда вы вызываете alice.update_gpa(3.9), Python передает alice как self, а 3.9 — как new_gpa. Сигнатура метода — def update_gpa(self, new_gpa), но при вызове вы передаете только один аргумент — Python автоматически обрабатывает self.

Методы могут возвращать значения

Методы могут возвращать значения так же, как и обычные функции:

python
class Student:
    def is_honors(self):
        return self.gpa >= 3.5
    
    def get_status(self):
        if self.is_honors():
            return "Honors Student"
        else:
            return "Regular Student"
 
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
bob = Student()
bob.name = "Bob Smith"
bob.gpa = 3.2
 
print(alice.get_status())  # Output: Honors Student
print(bob.get_status())    # Output: Regular Student

Обратите внимание, что get_status вызывает другой метод (is_honors) с помощью self.is_honors(). Методы могут вызывать другие методы того же экземпляра.

Методы vs функции: когда использовать что

Возможно, вам интересно, когда стоит использовать метод, а когда — отдельную функцию. Вот правило:

Используйте метод, когда операция:

  • Нуждается в доступе к данным экземпляра (self.name, self.gpa и т. д.)
  • Логически принадлежит типу (это то, что студент как тип делает или чем является)
  • Изменяет состояние экземпляра

Используйте отдельную функцию, когда операция:

  • Не нуждается в данных экземпляра
  • Работает с несколькими типами
  • Является общей утилитой
python
class Student:
    # Метод: нужны данные экземпляра
    def is_honors(self):
        return self.gpa >= 3.5
 
# Функция: общая утилита, работает с любым значением GPA
def calculate_letter_grade(gpa):
    if gpa >= 3.7:
        return "A"
    elif gpa >= 3.0:
        return "B"
    elif gpa >= 2.0:
        return "C"
    else:
        return "D"
 
alice = Student()
alice.gpa = 3.8
 
# Используем метод для проверки, зависящей от экземпляра
print(alice.is_honors())  # Output: True
 
# Используем функцию для общих вычислений
print(calculate_letter_grade(alice.gpa))  # Output: A
print(calculate_letter_grade(2.5))        # Output: C

Распространенные шаблоны методов

Вот несколько распространенных шаблонов, которые вы будете часто использовать:

Getter-методы (возвращают вычисляемую информацию):

python
class Student:
    def get_full_info(self):
        return f"{self.name} ({self.student_id}) - GPA: {self.gpa}"

Setter-методы (изменяют атрибуты с валидацией):

python
class Student:
    def set_gpa(self, new_gpa):
        if 0.0 <= new_gpa <= 4.0:
            self.gpa = new_gpa
        else:
            print("Invalid GPA: must be between 0.0 and 4.0")

Методы-запросы (отвечают на вопросы да/нет):

python
class Student:
    def is_honors(self):
        return self.gpa >= 3.5
    
    def is_failing(self):
        return self.gpa < 2.0

Методы-действия (выполняют операции):

python
class Student:
    def add_grade(self, grade):
        self.grades.append(grade)
        # Пересчитываем GPA на основе всех оценок
        self.gpa = sum(self.grades) / len(self.grades)

30.5) Инициализация экземпляров с __init__

Ручная установка атрибутов после создания экземпляра утомительна и подвержена ошибкам. Метод __init__ решает эту проблему, позволяя инициализировать экземпляры данными в момент их создания.

Метод __init__

Метод __init__ (произносится как «dunder init» или «init») — это специальный метод, который Python автоматически вызывает при создании нового экземпляра:

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
 
# Создаем экземпляры с начальными данными
alice = Student("Alice Johnson", "S12345", 3.8)
bob = Student("Bob Smith", "S12346", 3.5)
 
print(alice.name)  # Output: Alice Johnson
print(bob.gpa)     # Output: 3.5

Когда вы пишете Student("Alice Johnson", "S12345", 3.8), Python:

  1. Создает новый пустой экземпляр Student
  2. Вызывает __init__ с этим экземпляром как self и вашими аргументами
  3. Возвращает инициализированный экземпляр

Метод __init__ не возвращает значение явно — он изменяет экземпляр «на месте», устанавливая его атрибуты. Если попытаться вернуть значение из __init__, Python вызовет TypeError.

python
class Student:
    def __init__(self, name):
        self.name = name
        # Ничего не возвращайте из __init__
        # return self  # Wrong! TypeError: __init__() should return None, not 'Student'

Как работает __init__

Давайте разберем, что происходит, шаг за шагом:

python
class Student:
    def __init__(self, name, student_id, gpa):
        print(f"Initializing student: {name}")
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
        print(f"Initialization complete")
 
alice = Student("Alice Johnson", "S12345", 3.8)
# Output:
# Initializing student: Alice Johnson
# Initialization complete
 
print(alice.name)  # Output: Alice Johnson

Параметры после self (name, student_id, gpa) становятся обязательными аргументами при создании экземпляра. Если вы их не передадите, Python вызовет TypeError:

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
 
# student = Student()  # TypeError: __init__() missing 3 required positional arguments
# student = Student("Alice")  # TypeError: __init__() missing 2 required positional arguments
student = Student("Alice Johnson", "S12345", 3.8)  # Correct

Это гораздо лучше, чем ручное присваивание атрибутов — Python гарантирует, что каждый экземпляр начинается с обязательными данными.

Значения параметров по умолчанию в __init__

Вы можете использовать значения параметров по умолчанию в __init__, как и в обычных функциях:

python
class Student:
    def __init__(self, name, student_id, gpa=0.0):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
 
# GPA необязателен, по умолчанию 0.0
alice = Student("Alice Johnson", "S12345", 3.8)
bob = Student("Bob Smith", "S12346")  # Использует gpa=0.0 по умолчанию
 
print(alice.gpa)  # Output: 3.8
print(bob.gpa)    # Output: 0.0

Это полезно для атрибутов, у которых есть разумные значения по умолчанию, но которые при необходимости можно настроить.

Валидация в __init__

Вы можете валидировать входные данные в __init__, чтобы гарантировать, что экземпляры создаются в корректном состоянии:

python
class Student:
    def __init__(self, name, student_id, gpa):
        if not name:
            print("Error: Name cannot be empty")
            self.name = "Unknown"
        else:
            self.name = name
        
        self.student_id = student_id
        
        if 0.0 <= gpa <= 4.0:
            self.gpa = gpa
        else:
            print(f"Warning: Invalid GPA {gpa}, setting to 0.0")
            self.gpa = 0.0
 
alice = Student("Alice Johnson", "S12345", 3.8)
print(alice.gpa)  # Output: 3.8
 
bob = Student("", "S12346", 5.0)
# Output:
# Error: Name cannot be empty
# Warning: Invalid GPA 5.0, setting to 0.0
print(bob.name)  # Output: Unknown
print(bob.gpa)   # Output: 0.0

Это гарантирует, что даже если кто-то передаст некорректные данные, экземпляр окажется в разумном состоянии.

30.6) Строковые представления с __str__ и __repr__

Когда вы печатаете экземпляр с print() или просматриваете его в интерактивной оболочке, Python нужно преобразовать его в строку. По умолчанию вы получаете что-то малополезное:

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
 
alice = Student("Alice Johnson", "S12345", 3.8)
print(alice)  # Output: <__main__.Student object at 0x...>

Вывод по умолчанию показывает имя класса и адрес в памяти, но ничего о фактических данных Алисы. Вы можете настроить это с помощью специальных методов __str__ и __repr__.

Метод __str__

Метод __str__ определяет, как ваши экземпляры преобразуются в строки с помощью print() и str():

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
    
    def __str__(self):
        return f"{self.name} ({self.student_id}) - GPA: {self.gpa}"
 
alice = Student("Alice Johnson", "S12345", 3.8)
print(alice)  # Output: Alice Johnson (S12345) - GPA: 3.8
print(str(alice))  # Output: Alice Johnson (S12345) - GPA: 3.8

Метод __str__ должен возвращать строку, которая читаема и информативна для конечных пользователей. Думайте о нем как о «дружелюбном» представлении.

Метод __repr__

Метод __repr__ определяет «официальное» строковое представление ваших экземпляров, используемое REPL и repr():

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
    
    def __repr__(self):
        return f"Student('{self.name}', '{self.student_id}', {self.gpa})"
 
alice = Student("Alice Johnson", "S12345", 3.8)
print(repr(alice))  # Output: Student('Alice Johnson', 'S12345', 3.8)

В REPL:

python
>>> alice = Student("Alice Johnson", "S12345", 3.8)
>>> alice
Student('Alice Johnson', 'S12345', 3.8)

Метод __repr__ должен возвращать строку, которая выглядит как валидный код Python для воссоздания объекта. Думайте о нем как о «представлении для разработчика» — оно должно быть однозначным и полезным для отладки.

Использование и __str__, и __repr__

Вы можете определить оба метода для разных целей:

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
    
    def __str__(self):
        # Дружелюбный, читаемый формат
        return f"{self.name} - GPA: {self.gpa}"
    
    def __repr__(self):
        # Однозначный, похожий на код формат
        return f"Student('{self.name}', '{self.student_id}', {self.gpa})"
 
alice = Student("Alice Johnson", "S12345", 3.8)
 
print(alice)        # Uses __str__
# Output: Alice Johnson - GPA: 3.8
 
print(repr(alice))  # Uses __repr__
# Output: Student('Alice Johnson', 'S12345', 3.8)

В REPL:

python
>>> alice = Student("Alice Johnson", "S12345", 3.8)
>>> alice  # Uses __repr__
Student('Alice Johnson', 'S12345', 3.8)
>>> print(alice)  # Uses __str__
Alice Johnson - GPA: 3.8

Когда определять какой метод

Вот правило:

  • Всегда определяйте __repr__: он используется REPL и инструментами отладки. Если определять только один, определяйте этот.
  • Определяйте __str__, когда нужен дружественный для пользователя формат: если ваш класс будет печататься для конечных пользователей, предоставьте читаемый __str__.
  • Если вы определите только __repr__: Python использует его для repr(), а str() тоже будет использовать __repr__ (поэтому print() также будет использовать его).
  • Если вы определите только __str__: print() использует __str__, но repr() и REPL используют __repr__ по умолчанию (показывая адрес в памяти). Поэтому определение __repr__ обычно важнее.
python
# Определен только __repr__
class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price
    
    def __repr__(self):
        return f"Product('{self.name}', {self.price})"
 
item = Product("Laptop", 999.99)
print(item)        # Uses __repr__ as fallback
# Output: Product('Laptop', 999.99)
print(repr(item))  # Uses __repr__
# Output: Product('Laptop', 999.99)

Строковое представление в коллекциях

Когда экземпляры находятся внутри коллекций (списков, словарей и т. п.), Python использует __repr__ для их отображения, а не __str__:

python
class Student:
    def __init__(self, name, gpa):
        self.name = name
        self.gpa = gpa
    
    def __str__(self):
        return f"{self.name}: {self.gpa}"
    
    def __repr__(self):
        return f"Student('{self.name}', {self.gpa})"
 
students = [
    Student("Alice", 3.8),
    Student("Bob", 3.5),
    Student("Carol", 3.9)
]
 
# Печать списка использует __repr__ для каждого student
print(students)
# Output: [Student('Alice', 3.8), Student('Bob', 3.5), Student('Carol', 3.9)]
 
# Печать отдельных student использует __str__
for student in students:
    print(student)
# Output:
# Alice: 3.8
# Bob: 3.5
# Carol: 3.9

Поэтому __repr__ должен быть однозначным — он помогает понять, что находится в ваших структурах данных во время отладки. Когда вы печатаете список, Python по сути вызывает repr() для каждого элемента, чтобы четко показать структуру.

30.7) Создание нескольких независимых экземпляров

Одна из самых мощных сторон классов — возможность создавать много независимых экземпляров, каждый со своими данными. Давайте подробно это рассмотрим.

У каждого экземпляра свои данные

Когда вы создаете несколько экземпляров из одного и того же класса, каждый из них поддерживает собственные отдельные атрибуты:

python
class BankAccount:
    def __init__(self, account_number, holder_name, balance=0.0):
        self.account_number = account_number
        self.holder_name = holder_name
        self.balance = balance
    
    def deposit(self, amount):
        self.balance += amount
        print(f"Deposited ${amount:.2f}. New balance: ${self.balance:.2f}")
    
    def withdraw(self, amount):
        if amount <= self.balance:
            self.balance -= amount
            print(f"Withdrew ${amount:.2f}. New balance: ${self.balance:.2f}")
            return True
        else:
            print(f"Insufficient funds. Balance: ${self.balance:.2f}")
            return False
    
    def __str__(self):
        return f"{self.holder_name}'s account ({self.account_number}): ${self.balance:.2f}"
 
# Создаем три независимых счета
alice_account = BankAccount("ACC-001", "Alice Johnson", 1000.0)
bob_account = BankAccount("ACC-002", "Bob Smith", 500.0)
carol_account = BankAccount("ACC-003", "Carol Davis", 2000.0)
 
# Операции по одному счету не влияют на другие
alice_account.deposit(500)
# Output: Deposited $500.00. New balance: $1500.00
 
bob_account.withdraw(200)
# Output: Withdrew $200.00. New balance: $300.00
 
# Каждый счет поддерживает свой баланс
print(alice_account)  # Output: Alice Johnson's account (ACC-001): $1500.00
print(bob_account)    # Output: Bob Smith's account (ACC-002): $300.00
print(carol_account)  # Output: Carol Davis's account (ACC-003): $2000.00

Эта независимость — основа объектно-ориентированного программирования. Каждый экземпляр — отдельная сущность со своим состоянием.

Экземпляры в коллекциях

Вы можете хранить экземпляры в списках, словарях или любой другой коллекции:

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
    
    def is_honors(self):
        return self.gpa >= 3.5
    
    def __repr__(self):
        return f"Student('{self.name}', '{self.student_id}', {self.gpa})"
 
# Создаем список студентов
students = [
    Student("Alice Johnson", "S12345", 3.8),
    Student("Bob Smith", "S12346", 3.2),
    Student("Carol Davis", "S12347", 3.9),
    Student("David Lee", "S12348", 3.4)
]
 
# Находим всех студентов с отличием
honors_students = []
for student in students:
    if student.is_honors():
        honors_students.append(student)
 
print("Honors students:")
for student in honors_students:
    print(f"  {student.name}: {student.gpa}")
# Output:
# Honors students:
#   Alice Johnson: 3.8
#   Carol Davis: 3.9
 
# Вычисляем средний GPA
total_gpa = sum(student.gpa for student in students)
average_gpa = total_gpa / len(students)
print(f"Average GPA: {average_gpa:.2f}")  # Output: Average GPA: 3.58

Это распространенный шаблон: создать множество экземпляров, сохранить их в коллекции, а затем обработать с помощью циклов и генераторных выражений.

Экземпляры могут ссылаться на другие экземпляры

Экземпляры могут иметь атрибуты, которые ссылаются на другие экземпляры, создавая отношения между объектами:

python
class Course:
    def __init__(self, course_code, course_name):
        self.course_code = course_code
        self.course_name = course_name
    
    def __str__(self):
        return f"{self.course_code}: {self.course_name}"
 
class Student:
    def __init__(self, name, student_id):
        self.name = name
        self.student_id = student_id
        self.courses = []  # Список экземпляров Course
    
    def enroll(self, course):
        self.courses.append(course)
        print(f"{self.name} enrolled in {course.course_name}")
    
    def list_courses(self):
        print(f"{self.name}'s courses:")
        for course in self.courses:
            print(f"  {course}")
 
# Создаем курсы
python_course = Course("CS101", "Introduction to Python")
data_course = Course("CS102", "Data Structures")
web_course = Course("CS103", "Web Development")
 
# Создаем студентов и записываем их на курсы
alice = Student("Alice Johnson", "S12345")
alice.enroll(python_course)
alice.enroll(data_course)
# Output:
# Alice Johnson enrolled in Introduction to Python
# Alice Johnson enrolled in Data Structures
 
bob = Student("Bob Smith", "S12346")
bob.enroll(python_course)
bob.enroll(web_course)
# Output:
# Bob Smith enrolled in Introduction to Python
# Bob Smith enrolled in Web Development
 
# Выводим курсы каждого студента
alice.list_courses()
# Output:
# Alice Johnson's courses:
#   CS101: Introduction to Python
#   CS102: Data Structures
 
bob.list_courses()
# Output:
# Bob Smith's courses:
#   CS101: Introduction to Python
#   CS103: Web Development

Обратите внимание: и Алиса, и Боб записаны на python_course — они ссылаются на один и тот же экземпляр Course. Это моделирует реальную связь, когда несколько студентов могут проходить один и тот же курс.

Идентичность и равенство экземпляров

Каждый экземпляр — уникальный объект, даже если у него такие же данные, как у другого экземпляра:

python
class Student:
    def __init__(self, name, gpa):
        self.name = name
        self.gpa = gpa
 
alice1 = Student("Alice", 3.8)
alice2 = Student("Alice", 3.8)
 
# Разные объекты, даже при идентичных данных
print(alice1 is alice2)  # Output: False
print(id(alice1) == id(alice2))  # Output: False

По умолчанию == также проверяет идентичность (являются ли они одним и тем же объектом), а не то, одинаковы ли у них данные. В главе 31 мы узнаем, как настраивать сравнение на равенство с помощью специального метода __eq__.


Эта глава познакомила вас с основами объектно-ориентированного программирования в Python. Вы узнали, как определять классы, создавать экземпляры, добавлять методы, инициализировать экземпляры с __init__, управлять строковыми представлениями и работать с несколькими независимыми экземплярами. Эти концепции формируют фундамент для более продвинутых возможностей ООП, которые мы рассмотрим в главах 31 и 32.


© 2025. Primesoft Co., Ltd.
support@primesoft.ai