Перейти к содержимому
Главная страница

Классы и объекты в Python: полное руководство по ООП с примерами для практики

Логотип языка программирования Python на синем фоне с надписью «PYTHON»

Классы и объекты в Python: полное практическое руководство

Этот материал объясняет ООП на Python последовательно и без «магии». Разберём разницу между классом и объектом, принципы ООП, синтаксис class, конструктор __init__ и роль self, атрибуты и методы, уровни доступа по соглашениям, наследование, множественное наследование и порядок разрешения методов (MRO), полиморфизм, инкапсуляцию через @property и дескрипторы, dataclasses, спецметоды data model, перегрузку операторов, сравнение и хеширование, __slots__, __new__ и метаклассы, сериализацию, контекстные менеджеры, а также ошибки и стили кодирования. Примеры даны в минималистичной форме, чтобы вы могли сразу переносить их в рабочие проекты.

1. Объекты и классы

Класс — шаблон, описывающий состояние (атрибуты) и поведение (методы). Объект — экземпляр класса, содержащий конкретные данные. В Python «всё — объект»: числа, функции, модули, классы.

class Car:
    pass

car = Car()
print(isinstance(car, Car))  # True 

Класс определяет контракт: какие поля и методы доступны, какие значения допустимы, какие инварианты должны сохраняться.

2. Четыре принципа ООП

  • Абстракция: скрываем детали реализации, оставляем существенное.
  • Инкапсуляция: ограничиваем прямой доступ к состоянию, используем методы/свойства.
  • Наследование: переиспользуем код предков.
  • Полиморфизм: единый интерфейс — разные реализации.
class Animal:
    def sound(self) -> str:
        raise NotImplementedError

class Dog(Animal):
def sound(self) -> str:
return "woof"

class Cat(Animal):
def sound(self) -> str:
return "meow"

def speak(x: Animal) -> None:
print(x.sound())

speak(Dog())
speak(Cat()) 

3. Синтаксис class и структура класса

Класс можно объявить без скобок наследования или с ними. Докстрока кратко фиксирует назначение и инварианты.

class ClassName(Base1, Base2):
    """Краткое описание, инварианты и ожидания по использованию."""
    VERSION = 1  # атрибут класса

```
def __init__(self, x: int):
    self.x = x  # атрибут экземпляра

def method(self) -> int:
    return self.x * self.VERSION
```

  • Имена классов — CapWords: HttpClient, OrderService.
  • Методы и поля — snake_case: max_items, load_data.
  • Старайтесь создавать атрибуты только в __init__, чтобы структура экземпляра была очевидной.

4. Конструктор __init__ и self

__init__ инициализирует состояние, self — ссылка на текущий объект.

class User:
    def __init__(self, username: str, is_active: bool = True):
        self.username = username
        self.is_active = is_active

u = User("alice")
print(u.username, u.is_active) 

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

class Percentage:
    def __init__(self, value: float):
        if not (0.0 <= value <= 100.0):
            raise ValueError("Процент от 0 до 100")
        self.value = value
  • Не используйте изменяемые объекты как значения по умолчанию. Паттерн: items=None и внутри self.items = [] if items is None else list(items).
  • Держите конструктор коротким: валидация + присваивания. Тяжёлую инициализацию выносите в методы-фабрики/класса.

5. Атрибуты: уровня класса и уровня экземпляра

Атрибуты класса общие для всех экземпляров. Атрибуты экземпляра — уникальны для объекта.

class Config:
    DEFAULT_TIMEOUT = 5  # статическое поле (уровень класса)

```
def __init__(self, timeout: int | None = None):
    self.timeout = timeout if timeout is not None else Config.DEFAULT_TIMEOUT
```

a = Config()
b = Config(timeout=10)
print(a.timeout, b.timeout, Config.DEFAULT_TIMEOUT)  # 5 10 5 

Если вам нужно общее для всех экземпляров, держите на уровне класса. Если значение зависит от конкретного объекта — храните на self.

6. Методы экземпляра, @classmethod и @staticmethod

class Path:
    def __init__(self, parts: list[str]):
        self.parts = parts

```
def join(self, *parts: str) -> "Path":      # метод экземпляра
    return Path(self.parts + list(parts))

@classmethod
def from_str(cls, s: str) -> "Path":        # альтернативный конструктор
    return cls(s.split("/"))

@staticmethod
def is_abs(s: str) -> bool:                 # логически относится к классу
    return s.startswith("/")
```

7. Уровни доступа: public, protected, private (соглашения)

В Python нет строгих модификаторов, используются соглашения:

  • public: name — обычный атрибут.
  • protected: _name — «для внутреннего использования» и классов-наследников.
  • private: __name — включает name mangling в _Class__name.
class BankAccount:
    def __init__(self, owner: str, balance: int = 0):
        self.owner = owner
        self._currency = "RUB"
        self.__balance = balance

```
def deposit(self, amount: int) -> None:
    if amount <= 0:
        raise ValueError("Amount must be positive")
    self.__balance += amount

def get_balance(self) -> int:
    return self.__balance
```

8. Наследование, множественное наследование и MRO

Наследование устраняет дублирование кода. При множественном наследовании порядок поиска атрибутов задаёт MRO (C3-линеаризация). Правильный стиль — «кооперативные» методы с super(), согласованные сигнатуры и отказ от «жёстких» вызовов предков.

class A:
    def who(self) -> str:
        return "A"

class B(A):
def who(self) -> str:
return super().who() + "→B"

class C(A):
def who(self) -> str:
return super().who() + "→C"

class D(B, C):
def who(self) -> str:
return super().who() + "→D"

print(D().who())   # A→C→B→D
print(D.**mro**)   # порядок разрешения 

Миксины добавляют небольшое, локальное поведение без самостоятельной предметной роли.

class LoggingMixin:
    def log(self, msg: str) -> None:
        print(f"[{self.__class__.__name__}] {msg}")

class Service(LoggingMixin):
def run(self) -> None:
self.log("start")
# ...
self.log("done") 

9. Полиморфизм: утиная типизация и singledispatch

from functools import singledispatch

@singledispatch
def area(shape):
raise NotImplementedError

@area.register
def _(shape: tuple):   # прямоугольник (w, h)
w, h = shape
return w * h

@area.register
def _(shape: float):   # радиус круга
import math
return math.pi * shape * shape 

10. Инкапсуляция через @property и дескрипторы

@property позволяет валидировать доступ, сохраняя внешний интерфейс.

class Celsius:
    def __init__(self, value: float = 0.0):
        self._value = float(value)

```
@property
def value(self) -> float:
    return self._value

@value.setter
def value(self, new: float) -> None:
    if new < -273.15:
        raise ValueError("Ниже абсолютного нуля")
    self._value = float(new)
```

Дескриптор реализует протокол __get__/__set__/__delete__ и управляет атрибутом на уровне класса.

class NonEmptyStr:
    def __set_name__(self, owner, name):
        self.private_name = "_" + name
    def __get__(self, obj, objtype=None):
        return getattr(obj, self.private_name)
    def __set__(self, obj, value):
        if not isinstance(value, str) or not value.strip():
            raise ValueError("Строка должна быть непустой")
        setattr(obj, self.private_name, value)

class Person:
name = NonEmptyStr()
def **init**(self, name: str):
self.name = name 

11. Dataclasses: field(), default_factory, post_init, frozen

dataclasses генерируют конструктор, __repr__, сравнение и др. по аннотациям полей. frozen=True делает объект иммутабельным, order=True — добавляет упорядочивание.

from dataclasses import dataclass, field

@dataclass(order=True, frozen=True)
class Point:
x: int
y: int
tags: list[str] = field(default_factory=list, compare=False)

p = Point(1, 2)
print(p)                     # Point(x=1, y=2, tags=[])

# p.x = 5  # ошибка: объект заморожен

from dataclasses import dataclass

@dataclass
class Rect:
    w: float
    h: float
    def __post_init__(self):
        if self.w <= 0 or self.h <= 0:
            raise ValueError("Положительные размеры")

12. Data Model: спецметоды и перегрузка операторов

Спецметоды делают объекты «нативными» для языка: итерация, сравнение, арифметика, контекстный менеджмент.

class Vector:
    def __init__(self, *coords):
        self._c = tuple(coords)
    def __repr__(self):
        return f"Vector{self._c!r}"
    def __str__(self):
        return "⟨" + ", ".join(map(str, self._c)) + "⟩"
    def __len__(self):
        return len(self._c)
    def __iter__(self):
        return iter(self._c)
    def __add__(self, other):
        return Vector(*[a+b for a, b in zip(self._c, other._c)])
    def __eq__(self, other):
        return self._c == other._c
    def __hash__(self):
        return hash(self._c)

13. Абстрактные классы (abc) и протоколы (typing.Protocol)

from abc import ABC, abstractmethod

class Shape(ABC):
@abstractmethod
def area(self) -> float: ... 
from typing import Protocol

class Sized(Protocol):
    def __len__(self) -> int: ...

def total(x: Sized) -> int:
    return len(x)

Протоколы не требуют наследования — достаточно «утино» совместимого интерфейса.

14. __slots__: экономия памяти и запрет динамических атрибутов

class PackedPoint:
    __slots__ = ("x", "y")  # нет __dict__
    def __init__(self, x: float, y: float):
        self.x, self.y = x, y

Ограничения: нельзя добавлять произвольные атрибуты; наследование со __slots__ требует аккуратности.

15. __new__ и метаклассы

__new__ создаёт объект; полезен для неизменяемых типов и фабрикации. Метакласс — «класс для классов», позволяет перехватывать создание классов.

class Singleton(type):
    def __call__(cls, *a, **kw):
        if not hasattr(cls, "_inst"):
            cls._inst = super().__call__(*a, **kw)
        return cls._inst

class Config(metaclass=Singleton):
pass 

16. Сериализация и копирование

import copy, json, pickle

class Node:
def **init**(self, v, child=None):
self.v, self.child = v, child

root = Node(1, Node(2))
deep = copy.deepcopy(root)
blob = pickle.dumps(root)
data = json.dumps({"v": root.v}) 

17. Контекстные менеджеры

class Resource:
    def __enter__(self):
        print("acquire"); return self
    def __exit__(self, exc_type, exc, tb):
        print("release"); return False

with Resource() as r:
print("use") 

18. Композиция против наследования

Композицию выбирайте, когда «имеет» лучше, чем «является». Наследование — когда новый тип сохраняет контракт предка и расширяет поведение.

class Engine:
    def start(self): print("engine on")

class Vehicle:
def **init**(self, engine: Engine):
self.engine = engine

```
def go(self):
    self.engine.start()
    print("moving...")
```

19. Практический пример: Human, House, SmallHouse

class Human:
    def __init__(self, name: str, money: int = 0, house: "House | None" = None):
        self.name = name
        self._money = money
        self.house = house

```
def earn(self, amount: int) -> None:
    if amount <= 0:
        raise ValueError("amount must be positive")
    self._money += amount

def buy_house(self, house: "House", discount: float = 0.0) -> bool:
    price = house.final_price(discount)
    if self._money >= price:
        self._money -= price
        self.house = house
        return True
    return False

@property
def money(self) -> int:
    return self._money
```

class House:
def **init**(self, area: int, price: int):
if area <= 0 or price <= 0:
raise ValueError("area/price must be positive")
self.area = area
self.price = price

```
def final_price(self, discount: float = 0.0) -> int:
    return int(self.price * (1.0 - discount))
```

class SmallHouse(House):
DEFAULT_AREA = 40
def **init**(self, price: int):
super().**init**(area=SmallHouse.DEFAULT_AREA, price=price)

h = Human("Alex", money=100_000)
small = SmallHouse(price=90_000)
print(h.buy_house(small, discount=0.05))  # True
print(h.house.area, h.money)              # 40 14500 

20. Расширения примера

  • Добавьте @property с проверкой, чтобы площадь и цена были положительными, скидка — в [0, 1).
  • Сделайте CountryHouse с дополнительным полем plot_area и переопределением final_price.
  • Реализуйте сравнение домов по цене и площади через functools.total_ordering.
  • Сериализуйте список домов в JSON и восстановите его.

21. Типичные ошибки и приёмы

  • Создание атрибутов «на лету» в методах, а не в __init__. Симптом: неоднозначная структура экземпляров.
  • Отсутствие super() при множественном наследовании. Это ломает кооперативные цепочки.
  • Смешение ответственности: «боже-объект» с десятками методов. Делите на небольшие классы.
  • Неверное равенство/хеш: если определили __eq__, подумайте о согласованном __hash__ и иммутабельных полях.
  • Злоупотребление наследованием там, где нужна композиция или делегирование.

22. Короткий чек-лист

  • Определите контракт класса словом и кодом: докстрока, аннотации, инварианты.
  • Создавайте все поля в __init__. Валидацию — сразу в конструкторе.
  • Отдельно решайте «что является чем» (наследование) и «что содержит что» (композиция).
  • Для согласованной иерархии используйте super() и единые сигнатуры.
  • Инкапсуляция: @property и дескрипторы вместо «жёстких» публичных полей.
  • Когда данные преобладают — подумайте о dataclasses.

Структура и акценты согласованы с учебными материалами по ООП (темы: «классы и объекты», «атрибуты/поля», «методы», «уровни доступа», «конструктор __init__», «практика»).

0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

Достигнут лимит времени. Пожалуйста, введите CAPTCHA снова.

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии