ООП в Python
Шпаргалка по ООП в Python: классы, self, __init__, методы, @property, наследование, super(), MRO, инкапсуляция, магические методы, dataclasses и ABC.
Объектно-ориентированное программирование (ООП) — это способ организовать код вокруг объектов, которые объединяют данные (атрибуты) и поведение (методы). В этой шпаргалке собрано всё ключевое про ООП в Python: от первого класса до абстрактных классов. Каждый блок — с коротким рабочим примером и комментариями-результатами.
Класс и объект
Класс — это шаблон (чертёж), а объект (экземпляр) — конкретная вещь, созданная по этому шаблону. Класс описываем через class, объект создаём вызовом класса как функции.
class Dog:
pass
rex = Dog() # создаём объект (экземпляр)
print(type(rex)) # <class '__main__.Dog'>
print(isinstance(rex, Dog)) # True
__init__ и self
Метод __init__ — это конструктор: он вызывается автоматически при создании объекта и обычно задаёт начальные атрибуты. Первый параметр любого метода экземпляра — self, ссылка на сам объект.
class Dog:
def __init__(self, name, age):
self.name = name # атрибут экземпляра
self.age = age
rex = Dog("Рекс", 3)
print(rex.name) # Рекс
print(rex.age) # 3
self не передают вручную при вызове — Python подставляет его сам: rex.bark() равнозначно Dog.bark(rex).
Атрибуты класса vs атрибуты экземпляра
Атрибут класса общий для всех экземпляров и объявляется прямо в теле класса. Атрибут экземпляра уникален для каждого объекта и обычно задаётся в __init__ через self.
class Dog:
species = "Canis familiaris" # атрибут класса (общий)
def __init__(self, name):
self.name = name # атрибут экземпляра (свой)
a = Dog("Рекс")
b = Dog("Бобик")
print(a.species, b.species) # Canis familiaris Canis familiaris
print(a.name, b.name) # Рекс Бобик
Dog.species = "собака" # меняем для всех
print(b.species) # собака
Осторожно с изменяемыми атрибутами класса (списки, словари): один общий объект делится между всеми экземплярами.
class Bad:
items = [] # ОДИН список на всех!
x = Bad(); y = Bad()
x.items.append(1)
print(y.items) # [1] — сюрприз
class Good:
def __init__(self):
self.items = [] # свой список у каждого
Методы: обычные, @classmethod, @staticmethod
Есть три вида методов:
| Вид | Первый аргумент | Доступ к |
|---|---|---|
| метод экземпляра | self | данным объекта |
@classmethod | cls | данным класса |
@staticmethod | — | ни к чему (просто функция в классе) |
class Pizza:
def __init__(self, ingredients):
self.ingredients = ingredients
def describe(self): # метод экземпляра
return f"Пицца с: {self.ingredients}"
@classmethod
def margherita(cls): # фабрика через cls
return cls(["сыр", "томаты"])
@staticmethod
def is_vegetarian(ingredients): # утилита без self/cls
return "мясо" not in ingredients
p = Pizza.margherita()
print(p.describe()) # Пицца с: ['сыр', 'томаты']
print(Pizza.is_vegetarian(["сыр"])) # True
@classmethod часто используют как альтернативные конструкторы (фабрики), а @staticmethod — для вспомогательных функций, логически относящихся к классу.
@property — геттеры и сеттеры
Декоратор @property превращает метод в «вычисляемый атрибут»: обращаемся как к полю, но за этим стоит логика. Сеттер задаётся через @имя.setter и позволяет валидировать данные.
class Temperature:
def __init__(self, celsius=0):
self._celsius = celsius # «защищённое» поле
@property
def celsius(self): # геттер
return self._celsius
@celsius.setter
def celsius(self, value): # сеттер с проверкой
if value < -273.15:
raise ValueError("Ниже абсолютного нуля")
self._celsius = value
@property
def fahrenheit(self): # только для чтения
return self._celsius * 9 / 5 + 32
t = Temperature(25)
print(t.celsius) # 25
print(t.fahrenheit) # 77.0
t.celsius = 30 # вызовется сеттер
print(t.fahrenheit) # 86.0
Наследование и super()
Наследование позволяет создать класс-потомок, который перенимает атрибуты и методы родителя. Функция super() вызывает реализацию из родительского класса — чаще всего его __init__.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return "..."
class Dog(Animal): # Dog наследует Animal
def __init__(self, name, breed):
super().__init__(name) # вызываем __init__ родителя
self.breed = breed
def speak(self):
return "Гав!"
rex = Dog("Рекс", "овчарка")
print(rex.name, rex.breed) # Рекс овчарка
print(rex.speak()) # Гав!
print(isinstance(rex, Animal)) # True
Переопределение методов
Потомок может переопределить (override) метод родителя, задав одноимённый метод. При этом внутри можно дополнить поведение родителя через super(), а не полностью заменить.
class Shape:
def area(self):
return 0
def describe(self):
return f"Площадь = {self.area()}"
class Circle(Shape):
def __init__(self, r):
self.r = r
def area(self): # переопределяем
return 3.14159 * self.r ** 2
class LabeledCircle(Circle):
def describe(self): # дополняем родителя
return "Круг. " + super().describe()
print(Circle(2).describe()) # Площадь = 12.56636
print(LabeledCircle(2).describe()) # Круг. Площадь = 12.56636
Множественное наследование и MRO
Класс может наследоваться сразу от нескольких. Порядок поиска методов задаёт MRO (Method Resolution Order) — алгоритм C3. Посмотреть порядок можно через ClassName.__mro__ или ClassName.mro().
class A:
def greet(self):
return "A"
class B(A):
def greet(self):
return "B"
class C(A):
def greet(self):
return "C"
class D(B, C): # наследует B и C
pass
d = D()
print(d.greet()) # B (по MRO)
print([cls.__name__ for cls in D.__mro__])
# ['D', 'B', 'C', 'A', 'object']
В кооперативном множественном наследовании super() идёт по цепочке MRO, а не просто «к родителю» — поэтому каждый метод должен вызывать super().
Инкапсуляция: _protected и __private
В Python нет жёстких модификаторов доступа — есть соглашения:
name— публичный атрибут;_name— «защищённый» по соглашению (трогать не стоит, но технически доступен);__name— «приватный»: срабатывает name mangling — имя превращается в_ClassName__name.
class Account:
def __init__(self, balance):
self._currency = "RUB" # protected по соглашению
self.__balance = balance # private (name mangling)
def get_balance(self):
return self.__balance
acc = Account(1000)
print(acc.get_balance()) # 1000
print(acc._currency) # RUB (доступ есть, но не принято)
# print(acc.__balance) # AttributeError
print(acc._Account__balance) # 1000 — так до него всё же добраться можно
Магические методы (dunder)
«Магические» (dunder, double underscore) методы определяют поведение объектов с операторами и встроенными функциями. Самые ходовые:
| Метод | Зачем |
|---|---|
__str__ | читаемая строка (print, str()) |
__repr__ | отладочное представление (для разработчика) |
__eq__ | сравнение через == |
__len__ | поддержка len() |
__add__ | оператор + |
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __str__(self):
return f"({self.x}, {self.y})"
def __repr__(self):
return f"Vector({self.x!r}, {self.y!r})"
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __len__(self):
return 2
v = Vector(1, 2)
print(str(v)) # (1, 2)
print(repr(v)) # Vector(1, 2)
print(v == Vector(1, 2)) # True
print(v + Vector(3, 4)) # (4, 6)
print(len(v)) # 2
Правило: __repr__ делайте всегда (полезен в отладке и логах); __str__ — когда нужна «человеческая» строка. Если есть только __repr__, его же использует и print.
dataclasses
Декоратор @dataclass автоматически генерирует __init__, __repr__ и __eq__ по аннотированным полям — меньше шаблонного кода для классов-данных.
from dataclasses import dataclass, field
@dataclass
class Point:
x: int
y: int = 0 # значение по умолчанию
tags: list = field(default_factory=list) # изменяемое — через factory
p = Point(1, 2)
print(p) # Point(x=1, y=2, tags=[])
print(p == Point(1, 2)) # True (сравнение по полям из коробки)
@dataclass(frozen=True) # неизменяемый и хешируемый
class Color:
r: int
g: int
b: int
c = Color(255, 0, 0)
# c.r = 0 # FrozenInstanceError
print({c: "red"}) # можно класть в set/dict
Абстрактные классы (ABC)
Модуль abc позволяет задать «контракт»: абстрактный класс нельзя инстанцировать, а потомки обязаны реализовать все методы, помеченные @abstractmethod.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
...
def describe(self): # обычный метод тоже можно
return f"Площадь = {self.area()}"
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self): # обязаны реализовать
return self.side ** 2
# s = Shape() # TypeError: Can't instantiate abstract class
sq = Square(4)
print(sq.area()) # 16
print(sq.describe()) # Площадь = 16
Абстрактные классы удобны, когда нужно гарантировать, что все наследники реализуют определённый интерфейс.