Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Инвариантность класса (Class Invariant)
Инвариантность класса — это набор условий и правил, которые должны быть всегда истинными для объекта класса. Это гарантирует, что объект находится в валидном состоянии и его поведение предсказуемо. Нарушение инварианта означает, что объект испорчен.
Простой пример
class BankAccount:
"""Банковский счёт"""
def __init__(self, account_number: str, balance: float):
self.account_number = account_number
self.balance = balance
# ИНВАРИАНТЫ класса:
# 1. balance всегда >= 0 (нет минусовых денег)
# 2. account_number никогда не меняется
# 3. account_number непустая строка
def deposit(self, amount: float):
"""Пополнить счёт"""
if amount <= 0:
raise ValueError("Amount must be positive")
self.balance += amount
# Инвариант сохраняется: balance все ещё >= 0
def withdraw(self, amount: float):
"""Снять деньги"""
if amount <= 0:
raise ValueError("Amount must be positive")
if amount > self.balance:
raise ValueError("Insufficient funds")
self.balance -= amount
# Инвариант сохраняется: balance все ещё >= 0
def transfer(self, other: 'BankAccount', amount: float):
"""Перевод между счётами"""
if not isinstance(other, BankAccount):
raise TypeError("Recipient must be BankAccount")
self.withdraw(amount)
other.deposit(amount)
# Инварианты обоих счётов сохранены
# ❌ Нарушение инварианта (ПЛОХО)
account = BankAccount("123456", 1000)
account.balance = -500 # Прямой доступ нарушает инвариант!
# Теперь balance < 0, инвариант нарушен
# ✅ Соблюдение инвариантов (ХОРОШО)
account = BankAccount("123456", 1000)
account.withdraw(500) # balance = 500, инвариант в порядке
account.deposit(200) # balance = 700, инвариант в порядке
Инварианты на уровне класса
class Rectangle:
"""Прямоугольник"""
def __init__(self, width: float, height: float):
if width <= 0 or height <= 0:
raise ValueError("Width and height must be positive")
self._width = width
self._height = height
# ИНВАРИАНТЫ:
# 1. width > 0
# 2. height > 0
# 3. area = width * height (calculated, not stored)
@property
def width(self) -> float:
return self._width
@width.setter
def width(self, value: float):
if value <= 0:
raise ValueError("Width must be positive")
self._width = value
# Инвариант сохраняется
@property
def height(self) -> float:
return self._height
@height.setter
def height(self, value: float):
if value <= 0:
raise ValueError("Height must be positive")
self._height = value
# Инвариант сохраняется
@property
def area(self) -> float:
# Вычисляется на лету, инвариант гарантирует
# что area всегда правильно
return self._width * self._height
def __repr__(self) -> str:
# Инвариант гарантирует что объект в порядке
return f"Rectangle({self._width}x{self._height})"
rect = Rectangle(10, 20)
print(rect.area) # 200, гарантировано правильно
rect.width = 5 # 5 > 0, инвариант проверен
print(rect.area) # 100, всё ещё правильно
Инварианты в многопоточности
Критично в многопоточных программах!
import threading
from typing import List
class ThreadSafeList:
"""Потокобезопасный список"""
def __init__(self):
self._data: List[int] = []
self._lock = threading.Lock()
# ИНВАРИАНТ:
# Доступ к self._data ВСЕГДА защищен self._lock
# (Это критический инвариант!)
def append(self, value: int):
with self._lock: # Защищаем инвариант
self._data.append(value)
# Инвариант сохраняется: доступ защищен
def get(self, index: int) -> int:
with self._lock: # Защищаем инвариант
return self._data[index]
def length(self) -> int:
with self._lock:
return len(self._data)
# ❌ НЕПРАВИЛЬНО: нарушает инвариант
def bad_append(self, value: int):
# БЕЗ lock — другой поток может читать одновременно!
self._data.append(value)
# Инвариант нарушен в многопоточном контексте
# Использование
list_obj = ThreadSafeList()
for i in range(1000):
list_obj.append(i) # Инвариант сохраняется даже при многопоточности
Инварианты в контрактном программировании
class Stack:
"""Стек данных (Last In, First Out)"""
def __init__(self):
self._items = []
# ИНВАРИАНТЫ:
# 1. self._items never None
# 2. size >= 0 (никогда не отрицательный)
# 3. if empty: size == 0
def push(self, item):
"""Добавить элемент (PRECONDITION: item not None)"""
if item is None:
raise ValueError("Cannot push None")
self._items.append(item)
# POSTCONDITION: size увеличился на 1
# INVARIANT: сохраняется
def pop(self):
"""Извлечь элемент (PRECONDITION: not empty)"""
if self.is_empty():
raise IndexError("Stack is empty")
item = self._items.pop()
# POSTCONDITION: size уменьшился на 1
# INVARIANT: сохраняется
return item
def peek(self):
"""Посмотреть верхний элемент (PRECONDITION: not empty)"""
if self.is_empty():
raise IndexError("Stack is empty")
return self._items[-1]
# POSTCONDITION: ничего не изменилось
# INVARIANT: сохраняется
def is_empty(self) -> bool:
"""Проверить пустой ли стек"""
return len(self._items) == 0
def size(self) -> int:
"""Размер стека (INVARIANT: всегда >= 0)"""
return len(self._items)
# Проверить инвариант (для отладки)
def _check_invariant(self) -> bool:
assert self._items is not None, "items is None!"
assert len(self._items) >= 0, "size is negative!"
if self.is_empty():
assert len(self._items) == 0, "Empty but size != 0"
return True
Инварианты и наследование
class Animal:
def __init__(self, name: str, age: int):
if age < 0:
raise ValueError("Age cannot be negative")
self.name = name
self.age = age
# ИНВАРИАНТЫ базового класса:
# 1. name is not empty
# 2. age >= 0
class Dog(Animal):
def __init__(self, name: str, age: int, breed: str):
super().__init__(name, age)
if not breed:
raise ValueError("Breed cannot be empty")
self.breed = breed
# ИНВАРИАНТЫ Dog (НАСЛЕДУЮТСЯ + новые):
# 1. name is not empty (от Animal)
# 2. age >= 0 (от Animal)
# 3. breed is not empty (новый)
# ✅ Liskov Substitution Principle сохраняется
# Dog инварианты >= Animal инварианты
# Правило для наследования инвариантов:
# Дочерний класс может ДОБАВЛЯТЬ инварианты
# но НИКОГДА не должен их ослаблять!
Защита инвариантов через инкапсуляцию
class User:
def __init__(self, username: str, email: str):
self._username = username # private
self._email = email # private
self._verified = False # private
# ИНВАРИАНТЫ:
# 1. _username never changes
# 2. _email валидный email
# 3. _verified либо True либо False
@property
def username(self) -> str:
return self._username # read-only
@property
def email(self) -> str:
return self._email
@email.setter
def email(self, new_email: str):
# Проверяем инвариант перед изменением
if not self._is_valid_email(new_email):
raise ValueError(f"Invalid email: {new_email}")
self._email = new_email
def verify(self):
# Метод проверяет и сохраняет инвариант
self._verified = True
@staticmethod
def _is_valid_email(email: str) -> bool:
return '@' in email and '.' in email.split('@')[1]
# ✅ Инвариант защищен
user = User("john", "john@example.com")
user.email = "invalid" # Ошибка! Инвариант не нарушается
Проверка инвариантов в тестах
import unittest
class TestBankAccount(unittest.TestCase):
def test_invariant_balance_never_negative(self):
"""Главный инвариант: balance >= 0"""
account = BankAccount("123", 1000)
# Попытка нарушить инвариант
with self.assertRaises(ValueError):
account.withdraw(2000) # Больше чем есть
# Инвариант сохраняется
self.assertGreaterEqual(account.balance, 0)
def test_invariant_transfer(self):
"""Инвариант при переводе между счётами"""
acc1 = BankAccount("123", 1000)
acc2 = BankAccount("456", 500)
acc1.transfer(acc2, 300)
# Оба инварианта сохранены
self.assertGreaterEqual(acc1.balance, 0)
self.assertGreaterEqual(acc2.balance, 0)
self.assertEqual(acc1.balance, 700)
self.assertEqual(acc2.balance, 800)
Итоги
Инвариант класса — это гарантия что объект всегда в валидном состоянии.
✅ Правильное использование:
- Определить инварианты при проектировании класса
- Защитить инварианты через инкапсуляцию
- Проверить их перед возвращением из методов
- Документировать инварианты
- Тестировать нарушения инвариантов
❌ Частые ошибки:
- Публичный доступ к внутренним данным
- Не проверять входные данные
- Забывать про многопоточность
- Нарушать инварианты в наследовании
Инварианты — это не просто код, это контракт между вами и будущим вами. Они гарантируют что объект надёжен и предсказуем.