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

Содержание статьи
Класс и объект
Класс описывает, какие данные будут у объектов и что они умеют делать. Объект (экземпляр) - конкретная сущность, созданная по этому описанию. Например, класс Student задает, что у студента есть имя и оценка, а объекты - это Тимур с оценкой 5 и Антон с оценкой 3.
Класс объявляется ключевым словом class. Имена классов по соглашению PEP 8 пишут в стиле CapWords: каждое слово с заглавной буквы, без подчеркиваний. Самый короткий класс состоит из одной инструкции pass:
class Student:
pass
s = Student() # создание объекта
print(type(s)) # <class '__main__.Student'>Подробный разбор синтаксиса с пошаговым примером есть в статье про классы и объекты. Здесь же сосредоточимся на том, как из этих элементов складывается объектный подход.
Конструктор, атрибуты и методы
Метод __init__ вызывается автоматически при создании объекта и заполняет его начальными данными. Первый параметр любого обычного метода - self, ссылка на текущий объект. Через него метод обращается к атрибутам конкретного экземпляра.
class Student:
count = 0 # атрибут класса, общий для всех
def __init__(self, name, mark):
self.name = name # атрибуты экземпляра
self.mark = mark
Student.count += 1
def info(self):
return f"Имя: {self.name}, оценка: {self.mark}"
st1 = Student("Тимур", 5)
st2 = Student("Антон", 3)
print(st1.info()) # Имя: Тимур, оценка: 5
print(st2.info()) # Имя: Антон, оценка: 3
print(Student.count) # 2Обратите внимание на разницу между двумя видами атрибутов:
| Вид | Где задается | Чей | Пример |
|---|---|---|---|
| Атрибут класса | В теле класса | Общий для всех объектов | Student.count |
| Атрибут экземпляра | Через self, обычно в __init__ | Свой у каждого объекта | st1.name |
Частая ошибка - хранить в атрибуте класса изменяемый объект, например список. Тогда все экземпляры будут дописывать данные в один общий список. Изменяемые значения создавайте в __init__ через self.
Добавление, изменение и удаление атрибутов
В Python объекту можно добавить атрибут уже после создания, изменить его или удалить оператором del:
st1.course = 2 # добавить атрибут
st1.course = 3 # изменить
del st1.course # удалитьКогда имя атрибута известно только во время выполнения, например приходит из файла настроек, используют встроенные функции:
getattr(obj, name, default)- прочитать атрибут, вернутьdefault, если его нет;hasattr(obj, name)- проверить наличие;setattr(obj, name, value)- задать или создать атрибут;delattr(obj, name)- удалить.
field = "mark"
print(getattr(st1, field)) # 5
setattr(st1, field, 4)
print(hasattr(st1, "course")) # False
print(getattr(st1, "course", 1)) # 1Имя атрибута передается строкой. Больше примеров с динамическим доступом приведено в материале про getattr.
Инкапсуляция
Инкапсуляция означает, что объект сам контролирует свое состояние, а внешний код работает с ним через методы. В Python нет модификаторов доступа, как private в Java. Вместо них действуют соглашения об именах:
_mark- один знак подчеркивания говорит, что атрибут внутренний и снаружи его трогать не стоит;__mark- два подчеркивания включают искажение имени: внутри класса атрибут превращается в_Student__mark, что защищает от случайного переопределения в наследниках.
Для проверки значений при записи используют свойства. Декоратор @property позволяет обращаться к методу как к обычному атрибуту, а сеттер проверяет данные:
class Student:
def __init__(self, name, mark):
self.name = name
self.mark = mark # сработает сеттер
@property
def mark(self):
return self._mark
@mark.setter
def mark(self, value):
if not 1 <= value <= 5:
raise ValueError("Оценка должна быть от 1 до 5")
self._mark = value
s = Student("Олег", 4)
s.mark = 7 # ValueError: Оценка должна быть от 1 до 5Подробнее о геттерах и сеттерах написано в статье про property.
Наследование
Наследование позволяет создать класс на основе существующего: потомок получает все атрибуты и методы родителя и может добавить свои или переопределить имеющиеся. Родитель указывается в скобках после имени класса, а функция super() вызывает его методы.
class Person:
def __init__(self, name):
self.name = name
def info(self):
return f"Имя: {self.name}"
class Student(Person):
def __init__(self, name, mark):
super().__init__(name)
self.mark = mark
def info(self):
return f"{super().info()}, оценка: {self.mark}"
s = Student("Тимур", 5)
print(s.info()) # Имя: Тимур, оценка: 5
print(isinstance(s, Person)) # TrueБез вызова super().__init__() атрибут name не будет создан, и метод info() упадет с AttributeError. Все тонкости, включая порядок поиска методов, разобраны в статье про наследование классов.
Полиморфизм
Полиморфизм - возможность вызывать одинаковый метод у объектов разных классов, не проверяя их тип. Каждый класс выполняет метод по-своему. В Python это работает даже без общего родителя: достаточно, чтобы у объекта был нужный метод. Такой подход называют утиной типизацией.
class Teacher(Person):
def info(self):
return f"Преподаватель {self.name}"
people = [Student("Антон", 3), Teacher("Ирина Петровна")]
for p in people:
print(p.info())
# Имя: Антон, оценка: 3
# Преподаватель Ирина ПетровнаЦикл не знает, кто перед ним, и просто вызывает info(). Так же устроены встроенные функции: len() работает со списком, строкой и словарем, потому что у каждого из них есть метод __len__. Определив такие специальные методы в своем классе, вы научите объекты работать с операторами и встроенными функциями.
Частые вопросы
Ответы на вопросы, с которыми сталкиваются при первом знакомстве с ООП в Python.
Зачем нужен self?
Метод описан один раз в классе, а объектов может быть много. self указывает, с каким именно объектом работает метод. Python передает его автоматически: вызов st1.info() равносилен Student.info(st1).
Чем __init__ отличается от конструктора в других языках?
Объект создает метод __new__, а __init__ только инициализирует уже созданный объект. На практике __new__ переопределяют редко, поэтому __init__ обычно и называют конструктором.
Можно ли сделать атрибут по-настоящему приватным?
Нет. Даже атрибут с двумя подчеркиваниями доступен снаружи под искаженным именем. Защита в Python строится на соглашениях и свойствах, а не на запретах.
Когда стоит использовать классы, а когда хватит функций?
Класс оправдан, когда данные и операции над ними тесно связаны и создается несколько однотипных объектов: пользователи, товары, игровые персонажи. Для разовых преобразований данных обычно достаточно функций.