ООП в Python — классы и объектно-ориентированное программирование для аналитика

Что такое ООП

Объектно-ориентированное программирование — подход, при котором данные и функции для работы с ними объединяются в одну сущность — объект. Объект создаётся по шаблону, который называется классом. В Python всё является объектом: строки, списки, даже функции. Но чтобы создавать свои объекты, нужно разобраться с классами.

Для аналитиков ООП — не ежедневный инструмент. Большую часть задач можно решить функциями и pandas. Но на собеседованиях тему спрашивают, а в реальной работе классы полезны для пайплайнов, кастомных трансформеров и структурирования кода.

Синтаксис классов

Класс — это чертёж объекта. Метод __init__ вызывается при создании экземпляра и задаёт начальное состояние. self — ссылка на текущий экземпляр.

class Metric:
    def __init__(self, name, values):
        self.name = name
        self.values = values

    def mean(self):
        return sum(self.values) / len(self.values)

    def describe(self):
        return f"{self.name}: mean={self.mean():.2f}, n={len(self.values)}"


revenue = Metric("Revenue", [100, 150, 130, 170, 160])
print(revenue.mean())       # 142.0
print(revenue.describe())   # Revenue: mean=142.00, n=5

self.name и self.values — атрибуты экземпляра. Каждый объект хранит свои данные независимо. mean() и describe() — методы: функции, привязанные к объекту.

init и self подробнее

__init__ — не конструктор (конструктор — это __new__), а инициализатор. Он получает уже созданный объект и заполняет его атрибутами.

self — не ключевое слово, а конвенция. Технически можно назвать как угодно, но self — стандарт, и отступать от него не стоит.

class ABTest:
    def __init__(self, control, treatment, metric_name="conversion"):
        self.control = control
        self.treatment = treatment
        self.metric_name = metric_name
        self.is_significant = None  # заполним позже

    def run(self, alpha=0.05):
        from scipy import stats
        stat, p_value = stats.mannwhitneyu(self.control, self.treatment)
        self.is_significant = p_value < alpha
        return p_value

Здесь __init__ принимает данные двух групп и название метрики. Метод run() выполняет тест и сохраняет результат в атрибут is_significant.

Наследование

Наследование позволяет создать новый класс на основе существующего, переиспользуя и расширяя его логику.

class BaseTransformer:
    def __init__(self, column):
        self.column = column

    def fit(self, df):
        return self

    def transform(self, df):
        raise NotImplementedError("Переопредели в подклассе")


class FillNaTransformer(BaseTransformer):
    def __init__(self, column, fill_value=0):
        super().__init__(column)  # вызываем __init__ родителя
        self.fill_value = fill_value

    def transform(self, df):
        df = df.copy()
        df[self.column] = df[self.column].fillna(self.fill_value)
        return df


class LogTransformer(BaseTransformer):
    def transform(self, df):
        import numpy as np
        df = df.copy()
        df[self.column] = np.log1p(df[self.column])
        return df

super().__init__(column) вызывает инициализатор родительского класса. Оба трансформера имеют единый интерфейс (fit / transform), что позволяет использовать их в пайплайне.

Когда аналитику нужен ООП

Пайплайны обработки данных. Когда трансформации выстраиваются в цепочку, классы дают единый интерфейс.

class Pipeline:
    def __init__(self, steps):
        self.steps = steps

    def run(self, df):
        for step in self.steps:
            df = step.transform(df)
        return df

pipe = Pipeline([
    FillNaTransformer("revenue", fill_value=0),
    LogTransformer("revenue"),
])
result = pipe.run(raw_df)

Кастомные трансформеры для sklearn. Если вы строите ML-модели, sklearn ожидает объекты с методами fit() и transform().

Хранение конфигурации. Класс удобно группирует связанные параметры и методы — вместо разрозненных переменных и функций.

Инкапсуляция: публичное и приватное

Python не имеет настоящего private, но есть конвенции:

class User:
    def __init__(self, name, salary):
        self.name = name          # публичный атрибут
        self._salary = salary     # «приватный» (по конвенции)
        self.__bonus = 0          # name mangling: User__bonus

    def total_comp(self):
        return self._salary + self.__bonus

Одно подчёркивание _salary — «не трогайте снаружи, но технически можно». Двойное __bonus — срабатывает name mangling, доступ через _User__bonus. На практике одного подчёркивания достаточно.

dataclass — классы без шаблонного кода

Начиная с Python 3.7, dataclass автоматически генерирует __init__, __repr__, __eq__ и другие методы:

from dataclasses import dataclass

@dataclass
class ExperimentConfig:
    name: str
    control_group: str
    treatment_group: str
    metric: str
    alpha: float = 0.05
    min_sample_size: int = 1000

config = ExperimentConfig(
    name="pricing_test",
    control_group="A",
    treatment_group="B",
    metric="revenue_per_user",
)
print(config)
# ExperimentConfig(name='pricing_test', control_group='A',
#   treatment_group='B', metric='revenue_per_user',
#   alpha=0.05, min_sample_size=1000)

dataclass идеален для хранения структурированных данных — конфигураций, результатов экспериментов, метаданных. Не нужно вручную писать __init__ и __repr__. Подробнее про типы данных Python.

Магические методы

Методы вида __xxx__ (dunder-методы) позволяют определить поведение объекта с операторами и встроенными функциями:

@dataclass
class MetricResult:
    name: str
    value: float

    def __str__(self):
        return f"{self.name}: {self.value:.4f}"

    def __lt__(self, other):
        return self.value < other.value

results = [
    MetricResult("CR", 0.032),
    MetricResult("ARPU", 156.5),
    MetricResult("Retention D7", 0.41),
]
print(sorted(results))  # сортировка работает через __lt__

Основные: __str__ (для print), __repr__ (для отладки), __len__, __getitem__, __eq__, __lt__.

Вопросы с собеседований

Что такое self в Python?self — ссылка на текущий экземпляр класса. Через неё метод получает доступ к атрибутам и другим методам объекта. При вызове obj.method() Python автоматически передаёт obj как первый аргумент (self).

Чем отличается класс от экземпляра? — Класс — шаблон (чертёж), экземпляр — конкретный объект, созданный по этому шаблону. Класс User один, а экземпляров User("Анна"), User("Борис") может быть сколько угодно. У каждого экземпляра свои значения атрибутов.

Что делает super()?super() возвращает объект-прокси родительского класса. Используется, чтобы вызвать метод родителя из дочернего класса — чаще всего super().__init__() в инициализаторе, чтобы не дублировать логику.

Что такое dataclass и когда его использовать?dataclass (из модуля dataclasses, Python 3.7+) автоматически генерирует __init__, __repr__, __eq__ по аннотациям типов. Используют для классов, которые в основном хранят данные — конфиги, DTO, результаты. Экономит шаблонный код.

Что такое множественное наследование и какие проблемы оно создаёт? — Класс может наследовать от нескольких родителей: class C(A, B). Проблема — diamond problem: если A и B наследуют от общего предка и переопределяют один метод, непонятно, какую версию использовать. Python решает это через MRO (Method Resolution Order) — линеаризацию C3.

FAQ

Нужен ли ООП для работы аналитиком?

Для ежедневной работы с данными — нет, хватит функций, pandas и SQL. Но понимание ООП важно: вы читаете код библиотек (pandas DataFrame — это класс), пишете кастомные трансформеры для sklearn, структурируете пайплайны. На собеседованиях тема встречается на middle+ уровне. Базовые знания — class, init, наследование — нужны всем.

Чем dataclass отличается от обычного класса?

dataclass — обычный класс с автоматически сгенерированными методами. Декоратор @dataclass создаёт __init__, __repr__, __eq__ из аннотаций типов. Можно добавлять свои методы. Разница — только в удобстве: меньше шаблонного кода. Для сложной логики используйте обычные классы, для хранения данных — dataclass.

Как выбрать между функцией и классом?

Если логика — одно действие без состояния (посчитать метрику, преобразовать столбец) — используйте функцию. Если нужно хранить состояние между вызовами, объединить несколько связанных операций или обеспечить единый интерфейс для разных реализаций — класс. Подробнее про функциональный подход — в гайде по Python для аналитика.


Потренируйте вопросы по Python на реальных задачах — откройте тренажёр. 1500+ вопросов, которые спрашивают на собеседованиях аналитика. Бесплатно.