
Классы и объекты в Python
Полное практическое руководство по ООП в Python: что такое класс и объект, как работают self, **init** и **new**, уровни доступа, свойства @property и дескрипторы, наследование, полиморфизм, протоколы коллекций, абстрактные классы и Protocol, сравнение и хеширование, **slots**, dataclass и обобщения, контекстные менеджеры, тестирование и производительность.
Введение: зачем нужно ООП
ООП моделирует предметную область через сущности и их поведение. Класс задаёт контракт, объект хранит состояние и реализует методы. Базовые принципы: абстракция, инкапсуляция, наследование, полиморфизм. Преимущества: читаемость, повторное использование, контроль инвариантов, удобное тестирование и расширяемость.
Класс и объект: определения, идентичность и равенство
Класс — шаблон с атрибутами и методами. Объект — экземпляр класса. Отличайте идентичность (is) и равенство (== → **eq**). Если объект сравним по полям и изменяем, то хешировать его нельзя.
class Point:
def **init**(self, x: int, y: int):
self.x, self.y = x, y
```
def __repr__(self) -> str:
return f"Point({self.x}, {self.y})"
def __eq__(self, other: object) -> bool:
return isinstance(other, Point) and (self.x, self.y) == (other.x, other.y)
__hash__ = None # изменяемый объект не должен быть хешируемым
```
Объявление класса и создание экземпляров
Скелет: ключевое слово class и тело. Экземпляр создаётся вызовом класса. Проверяйте принадлежность через isinstance(), тип — через type(). Атрибуты лучше объявлять в __init__.
class User:
pass
u = User()
print(isinstance(u, User)) # True
print(type(u) is User) # True
self и методы экземпляра
self — ссылка на текущий объект и первый параметр методов экземпляра. Вызов obj.m(x) разворачивается в Class.m(obj, x). Соблюдайте PEP 8 и не переименовывайте self без повода.
class Vector:
def __init__(self, x: float, y: float):
self.x, self.y = x, y
```
def move(self, dx: float, dy: float) -> None:
self.x += dx
self.y += dy
```
v = Vector(1, 2)
v.move(3, 4)
Жизненный цикл объекта: __new__ и __init__
__new__(cls, ...) создаёт экземпляр и возвращает его; __init__(self, ...) инициализирует состояние. В большинстве классов достаточно __init__. __new__ нужен при наследовании от неизменяемых типов.
class Pair(tuple):
def __new__(cls, a, b):
return super().__new__(cls, (a, b))
```
def __repr__(self) -> str:
return f"Pair{tuple(self)}"
```
Поля класса и экземпляра, затенение, __dict__, __slots__
Поле класса общее для всех объектов. Поле экземпляра принадлежит конкретному объекту. Присвоение одноимённого атрибута на экземпляре затеняет поле класса. obj.__dict__ — словарь атрибутов. __slots__ экономит память и запрещает произвольные поля.
class C:
kind = "config"
def __init__(self, n: int):
self.n = n
c1, c2 = C(1), C(2)
c1.kind = "local" # затенение поля класса
class P:
**slots** = ("x", "y")
def **init**(self, x: int, y: int):
self.x, self.y = x, y
Уровни доступа: public, protected, private, name mangling
В Python уровни доступа — соглашение. Public — без подчёркиваний. Protected — _name для внутреннего использования. Private — __name с подстановкой имени класса (name mangling), фактический атрибут хранится как _Class__name. Это защита от случайных конфликтов, не механизм безопасности.
class A:
def __init__(self):
self.pub = 1
self._prot = 2
self.__priv = 3
```
def leak(self):
return self._A__priv # доступ с учётом name mangling
```
Свойства и дескрипторы: @property как частный случай
@property — дескриптор, инкапсулирующий доступ и валидацию с синтаксисом поля. Пользовательские дескрипторы удобны для правил, кеширования и логирования. Ниже пример собственного дескриптора и классического @property.
class NonNegative:
def __set_name__(self, owner, name):
self.name = name
```
def __get__(self, obj, owner=None):
if obj is None:
return self
return obj.__dict__.get(self.name, 0)
def __set__(self, obj, val):
if val < 0:
raise ValueError("non-negative only")
obj.__dict__[self.name] = val
```
class Account:
balance = NonNegative()
```
def __init__(self, owner: str, balance: int = 0):
self.owner = owner
self.balance = balance
```
class Temperature:
def **init**(self, c: float):
self._c = None
self.c = c
```
@property
def c(self) -> float:
return self._c
@c.setter
def c(self, v: float) -> None:
if v < -273.15:
raise ValueError("ниже абсолютного нуля")
self._c = v
```
# Использование
acc = Account("Bob", 10)
acc.balance = 25
t = Temperature(0.0)
t.c = 36.6
Методы класса и статические методы
@classmethod получает cls и подходит для альтернативных конструкторов и фабрик. @staticmethod — утилита без доступа к self/cls.
class Color:
def __init__(self, r: int, g: int, b: int):
self.r, self.g, self.b = r, g, b
```
@classmethod
def from_hex(cls, h: str) -> "Color":
r = int(h[1:3], 16)
g = int(h[3:5], 16)
b = int(h[5:7], 16)
return cls(r, g, b)
@staticmethod
def clamp(v: int) -> int:
return max(0, min(255, v))
```
c = Color.from_hex("#336699")
Наследование, super() и порядок разрешения методов (MRO)
Наследование переиспользует код базового класса. super() вызывает следующую реализацию по MRO. В ромбовидных иерархиях используйте единообразные вызовы super(), не обращайтесь к родителям по имени.
class A:
def who(self):
return "A"
class B(A):
def who(self):
return "B>" + super().who()
class C(A):
def who(self):
return "C>" + super().who()
class D(B, C):
def who(self):
return "D>" + super().who()
print(D().who()) # D>B>C>A
print(D.**mro**) # порядок MRO
Полиморфизм и перегрузка операторов
Полиморфизм даёт единый интерфейс для разных реализаций. Dunder-методы интегрируют класс с синтаксисом Python: представление, сравнение, арифметика, итерация. Реализуйте только осмысленные операции и тестируйте свойства сравнения.
class V:
def __init__(self, x: int, y: int):
self.x, self.y = x, y
```
def __repr__(self) -> str:
return f"V({self.x}, {self.y})"
def __add__(self, o: "V") -> "V":
return V(self.x + o.x, self.y + o.y)
def __iter__(self):
return iter((self.x, self.y))
def __eq__(self, o: object) -> bool:
return isinstance(o, V) and (self.x, self.y) == (o.x, o.y)
```
Протоколы коллекций: __len__, __iter__, __getitem__
Чтобы класс работал как контейнер, поддержите длину, итерацию и индексацию. Для изменяемых контейнеров добавьте __setitem__ и __delitem__.
class Bag:
def __init__(self, *xs):
self._xs = list(xs)
```
def __len__(self) -> int:
return len(self._xs)
def __iter__(self):
return iter(self._xs)
def __getitem__(self, i: int):
return self._xs[i]
```
Контекстные менеджеры: __enter__ и __exit__
Добавьте поддержку with, чтобы управлять ресурсами. __enter__ возвращает ресурс, __exit__ может подавить исключение, вернув True.
class Timer:
import time as _t
```
def __enter__(self):
self.t0 = self._t.perf_counter()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.dt = self._t.perf_counter() - self.t0
return False
```
with Timer() as t:
sum(range(1_000_000))
print(t.dt)
Абстрактные классы и typing.Protocol
abc.ABC и @abstractmethod задают формальные интерфейсы. Protocol — структурная типизация без наследования. Для частичных реализаций применяйте mixin-классы.
from abc import ABC, abstractmethod
class Repo(ABC):
@abstractmethod
def get(self, key: str) -> str:
...
from typing import Protocol
class SupportsClose(Protocol):
def close(self) -> None:
...
Аннотации типов, обобщения и Generic[T]
Аннотации улучшают автодополнение и статпроверку. Обобщения через typing.Generic описывают семейства классов с параметрами типов.
from typing import Generic, TypeVar
T = TypeVar("T")
class Box(Generic[T]):
def **init**(self, value: T):
self.value = value
def get(self) -> T:
return self.value
dataclass: генерация методов, слоты, (не)изменяемость
@dataclass генерирует __init__, __repr__, __eq__ и при необходимости порядок. Полезные параметры: frozen, slots, order, kw_only. Для коллекций используйте default_factory.
from dataclasses import dataclass, field
@dataclass(slots=True, order=True)
class User:
name: str
tags: list[str] = field(default_factory=list)
active: bool = True
Сериализация: asdict, json, pickle, pydantic
Для dataclass — asdict/astuple. Для внешних API используйте json. pickle — для временных кэшей, не для долговременного хранения. Для строгой валидации — pydantic.
from dataclasses import asdict
import json
payload = json.dumps(asdict(User("Ann")))
print(payload)
Копирование: copy и deepcopy, слоты и дескрипторы
copy.copy делает поверхностную копию, copy.deepcopy — глубокую. При __slots__ и дескрипторах по необходимости определяйте __copy__/__deepcopy__.
import copy
class Node:
**slots** = ("val", "next")
def **init**(self, val, next=None):
self.val, self.next = val, next
n1 = Node(1, Node(2))
n2 = copy.deepcopy(n1)
Исключения, инварианты и защита состояния
Инварианты определяют корректное состояние класса. Проверяйте их в конструкторах и сеттерах, поднимайте конкретные исключения, логируйте нарушения.
class Temperature:
def __init__(self, c: float):
self.c = c
```
@property
def c(self) -> float:
return self._c
@c.setter
def c(self, v: float) -> None:
if v < -273.15:
raise ValueError("ниже абсолютного нуля")
self._c = v
```
Производительность и память: слоты, ленивость, __getattribute__
__slots__ снижает расход памяти и ускоряет доступ. Ленивая и мемоизированная инициализация полезна для дорогих вычислений. Переопределяйте __getattribute__ только при необходимости — это медленно и опасно рекурсией.
class Lazy:
def __init__(self, factory):
self._f = factory
self._val = None
```
@property
def value(self):
if self._val is None:
self._val = self._f()
return self._val
```
Логирование и представление: __repr__ и __str__
__repr__ — для отладки и должен быть однозначным; __str__ — человекочитаемым. Если определить только __repr__, он используется и как __str__.
class Order:
def __init__(self, id: int, amount: float):
self.id, self.amount = id, amount
```
def __repr__(self) -> str:
return f"Order(id={self.id!r}, amount={self.amount!r})"
```
Метаклассы и type: когда это нужно
type — метакласс по умолчанию. Пользовательские метаклассы позволяют перехватывать создание класса, регистрировать типы, валидировать поля. Применяйте умеренно — чаще достаточно дескрипторов и декораторов.
class Registry(type):
registry = set()
def __new__(mcls, name, bases, ns):
cls = super().__new__(mcls, name, bases, ns)
mcls.registry.add(cls)
return cls
class Base(metaclass=Registry):
pass
Композиция против наследования: практические шаблоны
Наследование — «является разновидностью». Композиция — «содержит/использует». Композиция снижает связанность и упрощает тесты. Выделяйте стратегии, используйте внедрение зависимостей.
class Engine:
def start(self) -> str:
return "on"
class Car:
def **init**(self, engine: Engine):
self.engine = engine
def drive(self) -> str:
return "go:" + self.engine.start()
print(Car(Engine()).drive())
Паттерны проектирования в Python
Стратегия, Наблюдатель, Адаптер, Фабричный метод реализуются компактно благодаря функциям первого класса и динамической природе языка.
# стратегия
class Tax:
def __init__(self, rule):
self.rule = rule
def price(self, base: float) -> float:
return base + self.rule(base)
vat = Tax(lambda x: x * 0.2)
print(vat.price(100))
Работа с ошибками и контрактами
Определяйте собственные исключения, не проглатывайте ошибки, добавляйте контекст. В публичном API возвращайте простые объекты или протоколы, а не внутренние структуры.
class BalanceError(RuntimeError):
pass
class Wallet:
def **init**(self, amt: int = 0):
self._amt = amt
def withdraw(self, x: int) -> None:
if x > self._amt:
raise BalanceError("insufficient funds")
self._amt -= x
Тестирование классов: pytest, doctest, фикстуры
Покрывайте конструктор, геттеры/сеттеры, репрезентацию, сравнение и ошибки. Изолируйте состояние фикстурами. Простые примеры — в doctest. Тестируйте поведение, а не реализацию.
def test_wallet_withdraw():
w = Wallet(10)
w.withdraw(3)
assert w._amt == 7
Практикум: 12 задач на ООП
1) Альтернативный конструктор через @classmethod
class Url:
def __init__(self, scheme: str, host: str):
self.scheme, self.host = scheme, host
```
@classmethod
def parse(cls, s: str) -> "Url":
scheme, host = s.split("://", 1)
return cls(scheme, host)
```
print(Url.parse("[https://example.com").host](https://example.com%22%29.host))
2) Свойство с валидацией
class Temperature:
def __init__(self, c: float):
self.c = c
```
@property
def c(self) -> float:
return self._c
@c.setter
def c(self, v: float) -> None:
if v < -273.15:
raise ValueError("abs zero")
self._c = v
```
3) Итератор и индексатор
class Window:
def __init__(self, *tabs: str):
self._tabs = list(tabs)
def __iter__(self):
return iter(self._tabs)
def __getitem__(self, i: int) -> str:
return self._tabs[i]
4) Перегрузка операторов
class Money:
def __init__(self, amt: float):
self.amt = amt
def __add__(self, o: "Money") -> "Money":
return Money(self.amt + o.amt)
def __repr__(self) -> str:
return f"{self.amt:.2f}"
5) Контекстный менеджер
class Lock:
def __init__(self):
self._locked = False
def __enter__(self):
self._locked = True
return self
def __exit__(self, exc, exv, tb):
self._locked = False
return False
6) Дескриптор NonNegative
class NonNegative:
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, owner=None):
return obj.__dict__[self.name]
def __set__(self, obj, val):
if val < 0:
raise ValueError
obj.__dict__[self.name] = val
7) Dataclass с order и slots
from dataclasses import dataclass
@dataclass(order=True, slots=True)
class Item:
name: str
price: float
8) Generic Box[T]
from typing import TypeVar, Generic
T = TypeVar('T')
class Box(Generic[T]):
def **init**(self, v: T):
self.v = v
def get(self) -> T:
return self.v
9) Абстрактный репозиторий
from abc import ABC, abstractmethod
class Repo(ABC):
@abstractmethod
def get(self, key: str) -> str:
...
10) Протокол SupportsClose
from typing import Protocol
class SupportsClose(Protocol):
def close(self) -> None:
...
11) __slots__ и экономия памяти
class Node:
__slots__ = ("val", "next")
def __init__(self, val, next=None):
self.val, self.next = val, next
12) Ленивое свойство
class Lazy:
def __init__(self, f):
self._f = f
self._v = None
@property
def value(self):
if self._v is None:
self._v = self._f()
return self._v
Мини-проект: Human → House → SmallHouse (расширенный)
Цель — связать классы через наследование и композицию, отработать свойства, инварианты, альтернативные конструкторы, протоколы и тестирование.
from dataclasses import dataclass
from typing import Protocol
class Priced(Protocol):
def final_price(self, discount: float = 0.0) -> int:
...
@dataclass(slots=True)
class House:
area: int
price: int
def final_price(self, discount: float = 0.0) -> int:
if not 0.0 <= discount < 1.0:
raise ValueError("bad discount")
return int(self.price * (1 - discount))
class SmallHouse(House):
BASE_AREA = 40
def **init**(self, price: int, location_coef: float = 1.0):
super().**init**(area=self.BASE_AREA, price=int(price * location_coef))
class Human:
def **init**(self, name: str, age: int, balance: int = 0):
self.name, self.age = name, age
self._balance = balance
self.house: House | None = None
```
@property
def balance(self) -> int:
return self._balance
def earn(self, amount: int) -> int:
if amount <= 0:
raise ValueError("amount>0")
self._balance += amount
return self._balance
def buy(self, item: Priced, discount: float = 0.0) -> str:
cost = item.final_price(discount)
if cost > self._balance:
return "Недостаточно средств"
self._balance -= cost
self.house = item
return "Покупка успешна"
```
# Демонстрация
h = Human("Bob", 30, balance=100_000)
print(h.buy(SmallHouse(90_000, 1.1), discount=0.05))
Расширения: журнал транзакций, валидация возраста, сравнение домов по цене и площади, сериализация покупки в JSON, тесты граничных скидок.
Ошибки и анти-паттерны
- Изменяемые значения по умолчанию в
__init__. - Путаница между полем класса и экземпляра.
- Чрезмерная «приватность» с
__name. - Ручные вызовы родителей вместо super().
- Перегрузка операторов без тестов.
- Нарушение контракта сравнения и
__hash__. - Преждевременная оптимизация через
__getattribute__. - Отсутствие
__repr__и логов.
Стиль кода: PEP 8 и организация класса
Имена классов — CamelCase, методов и атрибутов — snake_case. Порядок: константы, поля класса, __init__, dunder-методы, публичные, приватные. Докстринги краткие и предметные. Типизация на публичных границах. Сложные методы делите на функции.
Чек-лист перед релизом класса
- Определён публичный API и инварианты.
- Реализованы
__repr__, при необходимости__eq__,__hash__и порядок. - Нет изменяемых значений по умолчанию.
- Свойства валидируют входные данные.
- Тесты покрывают граничные случаи и ошибки.
- Производительность и память оценены, при необходимости используются
__slots__.
FAQ
Поле класса живёт в объекте класса и общее для экземпляров; присвоение одноимённого атрибута на экземпляре создаёт затенение и не меняет класс. Поле экземпляра хранится в __dict__ или слотах конкретного объекта.
@classmethod — для альтернативных конструкторов и логики, где важен текущий класс (включая наследников). @staticmethod — для утилит без доступа к self/cls, чтобы сгруппировать их рядом с классом.
__new__ используйте при наследовании от immutable-типов или когда нужен контроль создания/кеширования. В остальных случаях — __init__.
_name — сигнал «внутреннее», __name включает name mangling и защищает от конфликтов имён. Реальную инкапсуляцию обеспечивают свойства и границы модулей.
super() идёт по MRO класса-владельца метода. Вызывайте super() один раз на уровень и не обращайтесь к родителям по имени, чтобы цепочка не рвалась.
Когда у вас много однотипных объектов и важна память, а также когда нужно запретить неожиданные атрибуты. Учитывайте влияние на сериализацию/копирование.
Чтобы убрать шаблонный код, получить сравнение и репрезентацию «из коробки», задать неизменяемость, порядок и слоты, повысить читаемость и простоту тестирования.
Заключение
Теперь у вас системная картина классов и объектов в Python: от базовой механики и инкапсуляции до дескрипторов, протоколов и метапрограммирования. Проектируйте публичный API, фиксируйте инварианты свойствами, выбирайте композицию вместо лишнего наследования, используйте dataclass для простых сущностей, покрывайте ключевые методы тестами и измеряйте производительность — так ваши классы будут предсказуемы, расширяемы и экономны.