Классы и объекты в 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__», «практика»).
