Лекция 1. Классы, объекты и инкапсуляция
Введение: зачем нужна парадигма ООП
Программирование как дисциплина прошло несколько этапов развития: от машинных кодов и ассемблера к процедурному программированию, далее — к структурному подходу и, наконец, к объектно-ориентированному программированию (ООП). Первыми языками, поддержавшими идеи ООП, стали Simula 67 и Smalltalk; впоследствии эти принципы легли в основу C++, Java, C# и оказали глубокое влияние на Python.
Объектно-ориентированное программирование — это методология, в которой программа рассматривается как совокупность взаимодействующих объектов. Каждый объект является носителем как данных (состояния), так и методов их обработки (поведения). ООП формирует прочную связь между программной моделью и предметной областью: мы описываем программу в тех же терминах, в которых мыслим о задаче.
Процедурный подход против объектно-ориентированного
В процедурном программировании данные и функции существуют раздельно. Данные «текут» через функции, и за их согласованность отвечает программист вручную.
# Процедурный стиль: данные и функции отдельноdef make_account(balance): return {"balance": balance}
def deposit(account, amount): account["balance"] += amount # никто не мешает положить отрицательную сумму
acc = {"balance": 100}deposit(acc, 50)acc["balance"] = -1000 # данные ничем не защищеныПроблема в том, что словарь acc открыт для любых изменений, а правила («баланс не
может быть отрицательным») нигде не закреплены. По мере роста программы такие
неявные договорённости становится всё труднее соблюдать.
В ООП данные и операции над ними объединяются в единой сущности — объекте. Объект сам следит за корректностью своего состояния, а внешний код работает с ним через ограниченный, продуманный интерфейс.
# Объектно-ориентированный стиль: данные и поведение вместеclass Account: def __init__(self, balance: float): self._balance = balance
def deposit(self, amount: float) -> None: if amount <= 0: raise ValueError("Сумма пополнения должна быть положительной") self._balance += amountГлавные преимущества подхода: управление сложностью, повторное использование кода, локализация изменений и моделирование задачи в естественных терминах.
Класс и объект
Что такое класс
Класс — это абстракция, описание набора объектов с одинаковыми характеристиками. Класс задаёт три вещи:
- состояние — какими атрибутами обладают объекты;
- поведение — какие методы (операции) им доступны;
- инварианты — ограничения, которым подчиняются объекты данного класса.
С точки зрения теории множеств класс — это определение множества, а объект — элемент этого множества. Класс можно сравнить с чертежом или формой для выпечки, а объект — с конкретным изделием, изготовленным по этому чертежу.
class Rectangle: def __init__(self, width: float, height: float): self.width = width self.height = height
def area(self) -> float: return self.width * self.height
def perimeter(self) -> float: return 2 * (self.width + self.height)
r = Rectangle(3, 4)print(r.area()) # 12print(r.perimeter()) # 14Что такое объект
Объект (экземпляр класса) — это конкретная сущность с собственными значениями атрибутов. У каждого объекта есть три свойства: идентичность (он уникален), состояние (текущие значения атрибутов) и поведение (методы).
class Point: def __init__(self, x: float, y: float): self.x = x self.y = y
p1 = Point(0, 0)p2 = Point(0, 0)print(p1 is p2) # False — это разные объекты с одинаковым состояниемprint(p1 == p2) # False — по умолчанию сравнение идёт по идентичностиОператор is проверяет идентичность (один и тот же объект в памяти), а == по
умолчанию — тоже идентичность, пока мы не переопределим поведение сравнения (см. ниже
__eq__).
Конструктор __init__ и параметр self
Метод __init__ — это инициализатор: он вызывается автоматически сразу после
создания объекта и заполняет его начальное состояние. Первый параметр любого метода
экземпляра — self — это ссылка на сам создаваемый/вызывающий объект. Через self
мы записываем и читаем атрибуты конкретного экземпляра.
class User: def __init__(self, name: str, age: int): self.name = name # self.name — атрибут ИМЕННО этого объекта self.age = age
u = User("Анна", 25) # Python сам передаёт u как selfprint(u.name, u.age) # Анна 25Важно: self не является ключевым словом — это просто соглашение об имени первого
параметра. Python подставляет объект автоматически, поэтому при вызове u.name
аргумент self мы не пишем.
Атрибуты класса и атрибуты экземпляра
- Атрибут экземпляра принадлежит конкретному объекту и обычно создаётся в
__init__черезself. - Атрибут класса объявляется в теле класса и является общим для всех экземпляров.
class Counter: created = 0 # атрибут класса — общий для всех объектов
def __init__(self): self.value = 0 # атрибут экземпляра — свой у каждого объекта Counter.created += 1
def inc(self): self.value += 1
c1 = Counter(); c1.inc()c2 = Counter()print(Counter.created) # 2 — счётчик общийprint(c1.value, c2.value) # 1 0 — значения независимыБудьте осторожны с изменяемыми атрибутами класса (списками, словарями): если их
менять «на месте», изменение увидят все экземпляры. Для индивидуальных данных
инициализируйте атрибуты в __init__.
Методы: экземпляра, классов и статические
В Python существуют три вида методов, различающихся тем, что они получают первым аргументом и к чему имеют доступ.
class Pizza: standard_radius = 15 # атрибут класса
def __init__(self, ingredients: list[str]): self.ingredients = ingredients
# 1. Метод экземпляра: получает self, работает с конкретным объектом def describe(self) -> str: return "Пицца с: " + ", ".join(self.ingredients)
# 2. Метод класса: получает cls, работает с самим классом @classmethod def margherita(cls) -> "Pizza": return cls(["томаты", "моцарелла", "базилик"]) # фабричный метод
# 3. Статический метод: ни self, ни cls — обычная функция в пространстве класса @staticmethod def area(radius: float) -> float: from math import pi return pi * radius ** 2
p = Pizza.margherita() # удобный «фабричный» конструкторprint(p.describe())print(Pizza.area(15)) # вспомогательная функция, не требующая объекта- Метод экземпляра (
self) — основной вид; имеет доступ к данным объекта. - Метод класса (
@classmethod,cls) — работает с классом, а не с экземпляром. Часто используется как альтернативный конструктор (фабричный метод):cls(...)корректно создаёт объект даже в наследниках. - Статический метод (
@staticmethod) — функция, логически связанная с классом, но не нуждающаяся ни в объекте, ни в классе. Удобна для вспомогательных операций.
Инкапсуляция
Инкапсуляция — это объединение данных и методов в рамках объекта и сокрытие деталей реализации. Она реализует идею «чёрного ящика»: внешний код пользуется объектом через публичный интерфейс, не зная и не завися от того, как объект устроен внутри. Это обеспечивает модульность и позволяет менять реализацию, не ломая код, который пользуется объектом.
В отличие от Java или C++, в Python нет жёстких модификаторов доступа
(private, protected). Вместо них используются соглашения об именах и встроенные
средства языка.
Соглашение _protected
Одиночное подчёркивание в начале имени (_balance) — это сигнал «для внутреннего
использования». Технически язык ничего не запрещает, но по соглашению такой атрибут
не следует трогать извне.
class Temperature: def __init__(self, celsius: float): self._celsius = celsius # «защищённый» по соглашению__private и name mangling
Двойное подчёркивание в начале имени (__token) запускает механизм
name mangling: интерпретатор автоматически переименовывает атрибут в
_ИмяКласса__token. Это не настоящая защита, а способ избежать случайного конфликта
имён при наследовании.
class Secret: def __init__(self): self.__token = "abc" # фактически станет _Secret__token
def reveal(self): return self.__token
s = Secret()print(s.reveal()) # abc# print(s.__token) # AttributeError: атрибута с таким именем нетprint(s._Secret__token) # abc — доступ всё же возможен, защита «мягкая»Вывод: инкапсуляция в Python основана на договорённости и культуре, а не на запретах. «Мы все здесь взрослые люди» — так звучит известный принцип сообщества.
Свойства @property и сеттеры
Свойство (@property) позволяет обращаться к методу так, будто это обычный атрибут.
Это даёт лучшее из двух миров: простой синтаксис доступа (obj.value) и при этом
контроль — валидацию, вычисления, защиту от некорректных значений.
class Temperature: def __init__(self, celsius: float): self._celsius = celsius
@property def celsius(self) -> float: # геттер: читаем как атрибут return self._celsius
@celsius.setter def celsius(self, value: float): # сеттер: с проверкой при записи if value < -273.15: raise ValueError("Температура ниже абсолютного нуля невозможна") self._celsius = value
@property def fahrenheit(self) -> float: # вычисляемое свойство, только на чтение return self._celsius * 9 / 5 + 32
t = Temperature(25)print(t.celsius) # 25 — вызывается геттерprint(t.fahrenheit) # 77.0 — вычисляется на летуt.celsius = 30 # вызывается сеттер с проверкой# t.celsius = -300 # ValueErrorСвойства позволяют начать с публичного атрибута, а позже добавить логику, не меняя внешний интерфейс класса. Это и есть практическая ценность инкапсуляции.
Магические методы (введение)
Магические (или «дандер», от double underscore) методы имеют имена вида
__name__ и определяют, как объект ведёт себя со встроенными операциями языка:
печатью, сравнением, арифметикой и т. д. Здесь рассмотрим три базовых.
class Money: def __init__(self, amount: int, currency: str = "RUB"): self.amount = amount self.currency = currency
# Понятное для пользователя представление (print, str) def __str__(self) -> str: return f"{self.amount} {self.currency}"
# Однозначное представление для разработчика (repr, отладка, консоль) def __repr__(self) -> str: return f"Money(amount={self.amount!r}, currency={self.currency!r})"
# Сравнение на равенство «по значению» def __eq__(self, other) -> bool: if not isinstance(other, Money): return NotImplemented return self.amount == other.amount and self.currency == other.currency
m1 = Money(100)m2 = Money(100)print(m1) # 100 RUB — сработал __str__print(repr(m1)) # Money(amount=100, currency='RUB') — сработал __repr__print(m1 == m2) # True — сработал __eq__ (сравнение по значению)__str__— «человеческое» представление, для пользователя (print,str()).__repr__— однозначное техническое представление, для отладки; в идеале по нему можно воссоздать объект. Если определён только__repr__, он используется и вместо__str__.__eq__— определяет смысл==. Часто его дополняют__hash__, чтобы объекты можно было использовать в множествах и как ключи словарей.
Магических методов в Python десятки (__len__, __add__, __getitem__ и др.) — они
будут подробно разобраны в следующих лекциях. Сейчас важно понять идею: реализуя
«дандер»-методы, мы интегрируем свои классы в синтаксис языка.
Краткие итоги
- ООП объединяет данные и поведение в объектах, в отличие от процедурного подхода, где они разделены; это упрощает управление сложностью.
- Класс — это шаблон (описание состояния, поведения и инвариантов), объект — конкретный экземпляр со своей идентичностью и состоянием.
__init__инициализирует объект, аself— ссылка на текущий экземпляр.- Атрибуты класса общие для всех экземпляров; атрибуты экземпляра
индивидуальны и обычно создаются в
__init__. - Существуют три вида методов: экземпляра (
self), класса (@classmethod,cls) и статические (@staticmethod). - Инкапсуляция в Python мягкая:
_protected— соглашение,__private— name mangling, а@propertyдаёт контролируемый доступ к данным. - Магические методы (
__str__,__repr__,__eq__и др.) встраивают объекты в синтаксис языка.
Вопросы для самопроверки
- Чем объектно-ориентированный подход отличается от процедурного? Какие проблемы процедурного стиля решает ООП?
- В чём разница между классом и объектом? Приведите аналогию.
- Для чего нужен параметр
selfи почему его не передают явно при вызове метода? - Чем атрибут класса отличается от атрибута экземпляра? Когда изменение атрибута класса затронет все объекты?
- Назовите три вида методов в Python. В каких случаях уместен
@classmethod, а в каких —@staticmethod? - Как работает механизм name mangling для имён вида
__private? Является ли это настоящей защитой данных? - Зачем нужны свойства
@property? Какое преимущество они дают перед прямым доступом к атрибуту? - В чём разница между
__str__и__repr__? Что произойдёт приprint(obj), если определён только__repr__?