Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. ООП
oop

ООП

Классы, методы, наследование, @property, @classmethod, @staticmethod, __str__, __repr__, __new__, __init__

Открыть лабораториюv0.6.2Запускается локально из публичного репозитория

Объектно-ориентированное программирование в Python

  • Маршрут: Инженерная основа · тема 2 из 8
  • Навыки: M2, S1
  • До урока: functions, exceptions, python_testing.
  • Результат: выбор между функцией, композицией и объектом на основе контракта, а не привычки.
  • Основа: классы, состояние экземпляра/класса и наследование. Работа в эксплуатации: композиция, проектирование через Protocol и инварианты. Углубление: модель данных, MRO/C3, совместный super() и поиск атрибутов.
  • Подтверждение: рефакторинг «функция ↔ объект» и трассировка модели данных.

Класс объединяет создание объектов, их состояние и поведение. Используйте его, когда такой контракт понятнее набора функций и словарей; не каждую задачу нужно переводить в иерархию классов.

#1. Базовое определение класса

class Dog: species = "Canis lupus" # атрибут класса (общий для всех экземпляров) def __init__(self, name: str): self.name = name # атрибут экземпляра def bark(self): return f"{self.name} says woof!" dog = Dog("Rex") print(dog.bark()) # Rex says woof!
  • Атрибуты класса хранятся в Dog.__dict__ и разделяются всеми экземплярами.
  • Атрибуты экземпляра хранятся в dog.__dict__ и уникальны для каждого объекта.

#2. __new__, __init__, __del__

#__new__ — создание объекта

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

class Singleton: _instance = None def __new__(cls, *args, **kwargs): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance def __init__(self, value): self.value = value # вызывается каждый раз, даже для существующего экземпляра! a = Singleton(1) b = Singleton(2) print(a is b) # True — один объект print(a.value) # 2 — __init__ перезаписал атрибут

__new__ необходим при наследовании от неизменяемых типов:

class PositiveInt(int): def __new__(cls, value): if value <= 0: raise ValueError("Должно быть положительным") return super().__new__(cls, value)

#__del__ — деструктор

Вызывается, когда счётчик ссылок объекта достигает нуля (не детерминировано):

class Resource: def __del__(self): print("Ресурс освобождён") # Не полагайтесь на этот метод для критических ресурсов! # Используйте контекстные менеджеры (with).

#3. Типы методов

ТипСигнатураДоступПример использования
Instance methoddef method(self, ...)self, clsdog.bark()
Class method@classmethod
def method(cls, ...)
cls (класс)Альтернативные конструкторы
Static method@staticmethod
def method(...)
Нет self/clsВспомогательные функции
class Date: def __init__(self, year, month, day): self.year = year self.month = month self.day = day @classmethod def from_string(cls, s): year, month, day = map(int, s.split('-')) return cls(year, month, day) # создаёт экземпляр подкласса при наследовании @staticmethod def is_valid(year, month, day): return 1 <= month <= 12 and 1 <= day <= 31 d = Date.from_string("2025-12-25") Date.is_valid(2025, 13, 1) # False

@classmethod — идиоматичный способ создания альтернативных конструкторов (from_string, from_json, from_env).

#4. Специальные методы (dunder)

#Строковые представления

МетодКогда вызываетсяНазначение
__str__str(obj), print(obj), f-строкиЧитаемый текст для пользователя
__repr__repr(obj), REPL, логи, !r в f-строкахТочное представление для разработчика

Если __str__ не определён, Python использует __repr__. Обратное неверно.

class Point: def __init__(self, x, y): self.x = x self.y = y def __repr__(self): # Полезная форма для отладки: видно имя типа и аргументы. return f"Point({self.x!r}, {self.y!r})" def __str__(self): return f"({self.x}, {self.y})" p = Point(1, 2) print(repr(p)) # Point(1, 2) print(str(p)) # (1, 2) print(f"{p!r}") # Point(1, 2) — !r вызывает __repr__ print(f"{p}") # (1, 2) — по умолчанию __str__

#__eq__ и __hash__

class Point: def __init__(self, x, y): self.x = x self.y = y def __eq__(self, other): if not isinstance(other, Point): return NotImplemented # не False! — позволяет other.__eq__ попробовать return self.x == other.x and self.y == other.y # Если определить __eq__, Python автоматически устанавливает __hash__ = None! # Объект становится нехэшируемым. Чтобы исправить — определите __hash__: def __hash__(self): return hash((self.x, self.y)) # хэш кортежа из полей

Правило: если объекты равны (a == b), их хэши должны совпадать (hash(a) == hash(b)).

#Протокол последовательности

class Stack: def __init__(self): self._items = [] def __len__(self): # len(stack) return len(self._items) def __getitem__(self, idx): # stack[0], stack[-1], stack[1:3] return self._items[idx] def __setitem__(self, idx, val): # stack[0] = 'x' self._items[idx] = val def __contains__(self, item): # item in stack — O(1) если хэш-структура return item in self._items # без __contains__ Python использует __getitem__ в цикле def __call__(self): # stack() — делает объект вызываемым return self._items.pop()

#__enter__ / __exit__ в классах

class DatabaseConnection: def __enter__(self): self.conn = connect_db() return self.conn def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is not None: self.conn.rollback() else: self.conn.commit() self.conn.close() return False # не подавляем исключение with DatabaseConnection() as conn: conn.execute("INSERT ...")

#5. @property — геттеры, сеттеры, удалители

class Circle: def __init__(self, radius): self._radius = radius # соглашение: _ означает "внутренний" @property def radius(self): return self._radius @radius.setter def radius(self, value): if value < 0: raise ValueError("Radius must be non-negative") self._radius = value @radius.deleter def radius(self): del self._radius @property def area(self): return 3.14159 * self._radius ** 2 # вычисляемый атрибут (нет setter) c = Circle(5) print(c.radius) # 5 — getter c.radius = 10 # setter print(c.area) # ~314.159 del c.radius # deleter

#6. Инкапсуляция: public, _protected, __private

class BankAccount: def __init__(self, balance): self.owner = "Alice" # public — свободный доступ self._balance = balance # protected — соглашение "не трогай снаружи" self.__pin = "1234" # private — name mangling! def get_balance(self): return self._balance # Name mangling: __pin → _BankAccount__pin acc = BankAccount(1000) print(acc._balance) # работает, но нарушает соглашение # print(acc.__pin) # AttributeError! print(acc._BankAccount__pin) # "1234" — технически доступно, но запрещено стилем

Name mangling (__attr → _ClassName__attr) предотвращает случайное переопределение в подклассах, но не является настоящей защитой.

#7. Наследование и MRO

#Простое наследование и super()

class Animal: def __init__(self, name): self.name = name def speak(self): return "sound" class Dog(Animal): def __init__(self, name, breed): super().__init__(name) # вызывает Animal.__init__ self.breed = breed def speak(self): return f"{self.name} says woof"

Та же механика подходит для иерархии доменных ошибок. Общий родитель позволяет ловить всё семейство, а конкретный подкласс — отдельный случай:

class AppError(Exception): pass class FieldError(AppError): pass

#Множественное наследование и diamond problem

class A: def method(self): print("A") class B(A): def method(self): print("B") super().method() class C(A): def method(self): print("C") super().method() class D(B, C): def method(self): print("D") super().method() D().method() # D → B → C → A (C3 линеаризация — алгоритм MRO)

MRO (Method Resolution Order) — алгоритм C3 линеаризации гарантирует:

  1. Подкласс перед родителем
  2. Порядок родителей сохраняется
  3. Каждый класс встречается в MRO ровно один раз
print(D.__mro__) # (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)

super() в Python 3 без аргументов работает через магию компилятора (__class__ ячейка).

#isinstance() vs type()

class Animal: pass class Dog(Animal): pass dog = Dog() type(dog) == Dog # True — точное сравнение, без учёта наследования isinstance(dog, Dog) # True isinstance(dog, Animal) # True — учитывает всю иерархию type(dog) == Animal # False! # Предпочитайте isinstance() для полиморфизма def process(obj): if isinstance(obj, Animal): # работает и для Dog, и для Cat obj.speak()

#issubclass()

issubclass(Dog, Animal) # True issubclass(Dog, object) # True — всё наследует от object issubclass(int, (str, list, int)) # True — принимает кортеж типов issubclass(bool, int) # True — bool является подклассом int

#8. __slots__ — экономия памяти

class Point: __slots__ = ('x', 'y') # запрещает __dict__, разрешает только x и y def __init__(self, x, y): self.x = x self.y = y p = Point(1, 2) # p.z = 3 # AttributeError: 'Point' object has no attribute 'z' # p.__dict__ # AttributeError: no __dict__ # Экономия памяти: ~40-50% на большом числе объектов import sys class WithDict: def __init__(self): self.x = 1; self.y = 2 class WithSlots: __slots__ = ('x', 'y') def __init__(self): self.x = 1; self.y = 2 print(sys.getsizeof(WithDict())) # ~48 байт + __dict__ ~232 байт print(sys.getsizeof(WithSlots())) # ~56 байт

Ограничения __slots__:

  • Нельзя добавлять новые атрибуты динамически
  • При наследовании дочерний класс должен тоже определить __slots__, иначе __dict__ появится
  • Нельзя использовать weakref без '__weakref__' в слотах

#9. Абстрактные базовые классы (ABC)

from abc import ABC, abstractmethod class Shape(ABC): @abstractmethod def area(self) -> float: """Площадь фигуры.""" @abstractmethod def perimeter(self) -> float: """Периметр фигуры.""" def describe(self): # конкретный метод — можно переопределять, но не обязательно return f"Площадь: {self.area():.2f}, периметр: {self.perimeter():.2f}" class Rectangle(Shape): def __init__(self, w, h): self.w = w self.h = h def area(self): return self.w * self.h def perimeter(self): return 2 * (self.w + self.h) # Shape() → TypeError: Can't instantiate abstract class Shape # Rectangle(3, 4) → OK, оба метода реализованы

#__init_subclass__ — хук при наследовании

class Plugin(ABC): _registry = {} def __init_subclass__(cls, name=None, **kwargs): super().__init_subclass__(**kwargs) if name: Plugin._registry[name] = cls class PluginA(Plugin, name="plugin_a"): pass class PluginB(Plugin, name="plugin_b"): pass print(Plugin._registry) # {'plugin_a': <class 'PluginA'>, 'plugin_b': <class 'PluginB'>}

#10. Protocol — структурная типизация

from typing import Protocol class Drawable(Protocol): def draw(self) -> None: ... def resize(self, factor: float) -> None: ... class Circle: # НЕ наследует от Drawable def draw(self): print("O") def resize(self, factor): self.radius *= factor def render(obj: Drawable) -> None: # принимает любой объект с draw() и resize() obj.draw() render(Circle()) # работает — структурная типизация (duck typing + статический анализ)

Разница: ABC — номинальная типизация (нужно наследовать), Protocol — структурная (достаточно иметь нужные методы).

#11. Dataclasses vs NamedTuple

from dataclasses import dataclass, field from typing import NamedTuple # NamedTuple — иммутабельный, tuple под капотом class PointNT(NamedTuple): x: float y: float label: str = "" # значение по умолчанию p = PointNT(1.0, 2.0) print(p.x, p[0]) # 1.0 1.0 — доступ и по имени, и по индексу # p.x = 3 # AttributeError — иммутабельный # dataclass — изменяемый по умолчанию, богатые возможности @dataclass class PointDC: x: float y: float label: str = "" history: list = field(default_factory=list) # изменяемые дефолты — через field! def distance_to(self, other: "PointDC") -> float: return ((self.x - other.x)**2 + (self.y - other.y)**2) ** 0.5 p = PointDC(1.0, 2.0) p.x = 3 # OK — изменяемый
ХарактеристикаNamedTuple@dataclass
ИзменяемостьНетПо умолчанию да (frozen=True → нет)
Наследование от tupleДаНет
Автогенерация __eq__ДаДа
__hash__Если все поля хэшируемыОбычно при frozen=True; зависит от eq и unsafe_hash
Доступ по индексуДаНет
__slots__Нет (вручную)@dataclass(slots=True) (3.10+)

#12. __dict__ и vars()

class Person: species = "Homo sapiens" def __init__(self, name, age): self.name = name self.age = age p = Person("Alice", 30) print(vars(p)) # {'name': 'Alice', 'age': 30} — только атрибуты экземпляра print(p.__dict__) # то же самое print(vars(Person)) # атрибуты класса, включая методы и species

#13. Паттерны ООП в Python

#Singleton через __new__

class Config: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance

#Factory Method через @classmethod

class Animal: @classmethod def create(cls, animal_type: str): types = {"dog": Dog, "cat": Cat} return types[animal_type]() class Dog(Animal): pass class Cat(Animal): pass assert isinstance(Animal.create("dog"), Dog)

#Композиция — объект получает поведение как зависимость

Наследование выражает отношение «является»: Cat является Animal. Композиция выражает «использует»: Checkout использует выбранную политику скидки. Политику можно заменить без создания подкласса Checkout.

class NoDiscount: def apply(self, amount: float) -> float: return amount class PercentageDiscount: def __init__(self, rate: float) -> None: if not 0 <= rate <= 1: raise ValueError("rate must be between 0 and 1") self.rate = rate def apply(self, amount: float) -> float: return amount * (1 - self.rate) class Checkout: def __init__(self, discount) -> None: self._discount = discount def total(self, amount: float) -> float: return self._discount.apply(amount) regular = Checkout(NoDiscount()) promo = Checkout(PercentageDiscount(0.2)) assert regular.total(100) == 100 assert promo.total(100) == 80

Если поведение не хранит состояния и имеет один вызов, параметра-функции часто достаточно. Объект-композиция полезен, когда политике нужны настройки, несколько связанных операций или собственный жизненный цикл. Наследование выбирайте, только когда подтип действительно сохраняет контракт базового типа.

#Mixin — добавление функциональности через множественное наследование

class JSONMixin: def to_json(self): import json return json.dumps(vars(self)) class User(JSONMixin): def __init__(self, name, email): self.name = name self.email = email User("Alice", "a@b.com").to_json() # '{"name": "Alice", "email": "a@b.com"}'

#Практика: от пробы к рабочему решению

#Шаг 1. Соберите минимальный пример

Создавайте класс, когда состояние и инварианты должны жить вместе. Результат основы — маленький объект с явной границей допустимых значений и без общего изменяемого состояния экземпляров.

#Шаг 2. Проверьте поведение

from __future__ import annotations class Positive: def __set_name__(self, owner: type, name: str) -> None: self.storage_name = f"_{name}" def __get__(self, instance: object, owner: type | None = None) -> int | Positive: if instance is None: return self return getattr(instance, self.storage_name) def __set__(self, instance: object, value: int) -> None: if value <= 0: raise ValueError("значение должно быть положительным") setattr(instance, self.storage_name, value) class Product: price = Positive() def __init__(self, price: int) -> None: self.price = price assert Product(10).price == 10

#Шаг 3. Доведите решение до рабочего сценария

Практика с подсказками: реализуйте одну и ту же политику скидки функцией и объектом-композицией. Добавьте вторую политику, сравните объём изменений и не используйте наследование без подстановочного контракта.

#Шаг 4. Объясните внутренний механизм

Добавьте в дескриптор журнал чтения и записи, затем проследите поиск атрибута через дескриптор данных, словарь экземпляра и MRO. Отдельно объясните, почему это не основание превращать каждое поле в дескриптор.

#Упражнения

  1. Реализуйте класс CachedProperty, который кэширует результат вычисления свойства при первом доступе.
  2. Почему __eq__ должен возвращать NotImplemented, а не False, при несовместимых типах?
  3. Напишите класс LimitedDict, который ограничивает количество пар ключ-значение (например, 100), удаляя самые старые при переполнении.
  4. Как работает super() без аргументов в Python 3?
  5. Реализуйте класс Vector с операторами +, * (скалярное произведение), len(), repr().
  6. Напишите пример, где __init_subclass__ используется для автоматической регистрации плагинов.

#Самопроверка

  • Для CachedProperty второй доступ не должен повторять вычисление, а значения разных экземпляров не должны смешиваться.
  • LimitedDict проверьте на обновлении существующего ключа и на точном порядке вытеснения.
  • Vector должен возвращать NotImplemented для неподдерживаемого операнда и иметь repr, из которого понятны все данные объекта.
  • Для регистрации плагинов проверьте два подкласса и дубликат имени.
  • Реализуйте скидку также функцией и подклассом. Сравнение считается полным, если письменно указано, что меняется при добавлении второй политики.

#Практическая лаборатория

Закрепите тему в лаборатории «Кошелёк с дескриптором и композицией политики». Вы проследите классовый и экземплярный доступ к дескриптору, изолируете состояние объектов и докажете атомарность операции при сбое политики.

git clone --branch v0.6.2 --depth 1 https://gitlab.potapov.me/courses/python-labs.git cd python-labs uv sync --group test uv run --group test pytest oop/tests

Далее: Dataclasses