30. Введение в классы и объекты
30.1) Идея объектно-ориентированного программирования (создание собственных типов)
На протяжении этой книги вы работали со встроенными типами Python: целыми числами, строками, списками, словарями и другими. Каждый тип объединяет данные (например, символы в строке) с операциями, которые можно выполнять над этими данными (например, .upper() или .split()). Такое сочетание данных и поведения очень мощное — оно позволяет думать о строках как о полноценных сущностях с собственными возможностями, а не просто как о «сырых» последовательностях символов.
Объектно-ориентированное программирование (OOP) развивает эту идею: оно позволяет создавать собственные пользовательские типы, называемые классами, которые объединяют данные и поведение, специфичные для вашей предметной области. Так же как Python предоставляет тип str для работы с текстом и тип list для работы с последовательностями, вы можете создать тип BankAccount для управления финансовыми транзакциями, тип Student для отслеживания учебных результатов или тип Product для системы учета складских запасов.
Зачем создавать собственные типы?
Рассмотрим управление информацией о студентах в школьной системе. Без классов вы могли бы использовать отдельные переменные или словари:
# Использование отдельных переменных — быстро становится запутанным и неудобным
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}Этот подход работает для простых случаев, но у него есть ограничения:
- Нет валидации: ничто не мешает установить
gpaв недопустимое значение вроде-5.0или"excellent" - Нет связанного поведения: операции вроде вычисления статуса «с отличием» или форматирования информации о студенте — это отдельные функции, разбросанные по всему коду
- Нет проверки типов: словарь, представляющий студента, выглядит так же, как любой другой словарь — 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), вы создаете один конкретный экземпляр студента с данными именно Алисы.
Вы можете создать столько экземпляров, сколько нужно, из одного класса — так же, как архитектор может использовать один чертеж, чтобы построить много домов. У каждого экземпляра свои данные (GPA Алисы отличается от GPA Боба), но все они разделяют одну и ту же структуру и возможности, определенные классом.
Что вы изучите в этой главе
Эта глава знакомит с основными концепциями объектно-ориентированного программирования в Python:
- Определение классов с ключевым словом
class - Создание экземпляров и доступ к их атрибутам
- Добавление методов, которые работают с данными экземпляра
- Понимание
selfи то, как методы получают доступ к данным экземпляра - Инициализация экземпляров с методом
__init__ - Управление строковыми представлениями с
__str__и__repr__ - Создание нескольких независимых экземпляров из одного и того же класса
К концу этой главы вы сможете проектировать и реализовывать собственные пользовательские типы, которые сделают ваши программы более организованными, поддерживаемыми и выразительными. Мы продолжим развивать эти основы в главе 31, добавив более продвинутые возможности классов, а в главе 32 рассмотрим наследование и полиморфизм.
30.2) Определение простых классов с class
Начнем с создания самого простого возможного класса — такого, который пока лишь определяет новый тип без данных и поведения.
Ключевое слово class
Класс определяется с помощью ключевого слова class, за которым следует имя класса и двоеточие:
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: каждое слово начинается с заглавной буквы, без подчеркиваний между словами:
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) при чтении кода.
Создание экземпляров
Создание экземпляра из класса выглядит как вызов функции — вы используете имя класса, за которым следуют круглые скобки:
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...) разные, что подтверждает: это отдельные объекты в памяти.
Зачем начинать с пустых классов?
Возможно, вам интересно, почему мы начинаем с классов, которые ничего не делают. Есть две причины:
-
Концептуальная ясность: понимание того, что класс — это просто новый тип, отдельный от его данных и поведения, помогает усвоить базовую концепцию до добавления сложности.
-
Практическая польза: даже пустые классы могут быть полезны как маркеры или заглушки. Например, вы можете определять собственные типы исключений:
class InvalidGradeError:
pass
class StudentNotFoundError:
pass
# Эти пустые классы служат различимыми типами ошибокОднако пустые классы редко встречаются в реальном коде. Давайте добавим данные, чтобы наши классы стали полезными.
30.3) Создание экземпляров и доступ к атрибутам
Классы становятся полезными, когда они хранят данные. В Python вы можете добавлять атрибуты(attributes) (данные, прикрепленные к экземпляру) в любой момент, просто присваивая им значения.
Добавление атрибутов экземплярам
Вы можете добавлять атрибуты экземпляру с помощью точечной нотации:
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)) — там вы обращались к методам и атрибутам этих объектов.
У каждого экземпляра свои атрибуты
Разные экземпляры одного и того же класса имеют независимые атрибуты:
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:
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:
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'Эта ошибка полезна — она помогает обнаруживать опечатки и логические ошибки, когда вы ожидаете, что атрибут существует, но его нет.
Проблема ручного присваивания атрибутов
Хотя вы можете добавлять атрибуты вручную после создания экземпляра, у этого подхода есть серьезные недостатки:
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:
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.
Вот что происходит «за кулисами»:
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 — это соглашение, а не ключевое слово. Технически можно использовать любое имя:
class Student:
def display_info(this): # Работает, но так делать не стоит
print(f"{this.name} - GPA: {this.gpa}")Однако всегда используйте self. Это универсальное соглашение в Python, которое делает ваш код читаемым для других разработчиков на Python. Использование любого другого имени будет сбивать читателей с толку и нарушать стандарты сообщества.
Методы с несколькими экземплярами
Сила self становится очевидной, когда у вас есть несколько экземпляров:
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 адаптируется к тому экземпляру, который его вызвал.
Методы могут принимать дополнительные параметры
Методы могут принимать параметры помимо self:
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.
Методы могут возвращать значения
Методы могут возвращать значения так же, как и обычные функции:
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и т. д.) - Логически принадлежит типу (это то, что студент как тип делает или чем является)
- Изменяет состояние экземпляра
Используйте отдельную функцию, когда операция:
- Не нуждается в данных экземпляра
- Работает с несколькими типами
- Является общей утилитой
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-методы (возвращают вычисляемую информацию):
class Student:
def get_full_info(self):
return f"{self.name} ({self.student_id}) - GPA: {self.gpa}"Setter-методы (изменяют атрибуты с валидацией):
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")Методы-запросы (отвечают на вопросы да/нет):
class Student:
def is_honors(self):
return self.gpa >= 3.5
def is_failing(self):
return self.gpa < 2.0Методы-действия (выполняют операции):
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 автоматически вызывает при создании нового экземпляра:
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:
- Создает новый пустой экземпляр
Student - Вызывает
__init__с этим экземпляром какselfи вашими аргументами - Возвращает инициализированный экземпляр
Метод __init__ не возвращает значение явно — он изменяет экземпляр «на месте», устанавливая его атрибуты. Если попытаться вернуть значение из __init__, Python вызовет TypeError.
class Student:
def __init__(self, name):
self.name = name
# Ничего не возвращайте из __init__
# return self # Wrong! TypeError: __init__() should return None, not 'Student'Как работает __init__
Давайте разберем, что происходит, шаг за шагом:
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:
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__, как и в обычных функциях:
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__, чтобы гарантировать, что экземпляры создаются в корректном состоянии:
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 нужно преобразовать его в строку. По умолчанию вы получаете что-то малополезное:
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():
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():
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:
>>> alice = Student("Alice Johnson", "S12345", 3.8)
>>> alice
Student('Alice Johnson', 'S12345', 3.8)Метод __repr__ должен возвращать строку, которая выглядит как валидный код Python для воссоздания объекта. Думайте о нем как о «представлении для разработчика» — оно должно быть однозначным и полезным для отладки.
Использование и __str__, и __repr__
Вы можете определить оба метода для разных целей:
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:
>>> 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__обычно важнее.
# Определен только __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__:
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) Создание нескольких независимых экземпляров
Одна из самых мощных сторон классов — возможность создавать много независимых экземпляров, каждый со своими данными. Давайте подробно это рассмотрим.
У каждого экземпляра свои данные
Когда вы создаете несколько экземпляров из одного и того же класса, каждый из них поддерживает собственные отдельные атрибуты:
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Эта независимость — основа объектно-ориентированного программирования. Каждый экземпляр — отдельная сущность со своим состоянием.
Экземпляры в коллекциях
Вы можете хранить экземпляры в списках, словарях или любой другой коллекции:
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Это распространенный шаблон: создать множество экземпляров, сохранить их в коллекции, а затем обработать с помощью циклов и генераторных выражений.
Экземпляры могут ссылаться на другие экземпляры
Экземпляры могут иметь атрибуты, которые ссылаются на другие экземпляры, создавая отношения между объектами:
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. Это моделирует реальную связь, когда несколько студентов могут проходить один и тот же курс.
Идентичность и равенство экземпляров
Каждый экземпляр — уникальный объект, даже если у него такие же данные, как у другого экземпляра:
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.