ШПАРГАЛКА

ООП в 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данным объекта
@classmethodclsданным класса
@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

Абстрактные классы удобны, когда нужно гарантировать, что все наследники реализуют определённый интерфейс.

Поддержать проект