← Назад к вопросам

Что такое инвариантность класса?

2.0 Middle🔥 141 комментариев
#Python Core

Комментарии (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)

Итоги

Инвариант класса — это гарантия что объект всегда в валидном состоянии.

Правильное использование:

  1. Определить инварианты при проектировании класса
  2. Защитить инварианты через инкапсуляцию
  3. Проверить их перед возвращением из методов
  4. Документировать инварианты
  5. Тестировать нарушения инвариантов

Частые ошибки:

  1. Публичный доступ к внутренним данным
  2. Не проверять входные данные
  3. Забывать про многопоточность
  4. Нарушать инварианты в наследовании

Инварианты — это не просто код, это контракт между вами и будущим вами. Они гарантируют что объект надёжен и предсказуем.