Изучение классов Python через практические задания

Раздел: Продвинутый Python -> Объектно-ориентированное программирование

Задания на классы в Python: от простого к сложному

В этом разделе рассматриваются несколько типовых задач по объектно-ориентированному программированию на Python. Каждое задание включает основное эффективное решение, альтернативные подходы, пояснения шагов и типичные трудности.

Задание 1. Как создать банковский счет с проверкой транзакций?

Основное решение использует инкапсуляцию и проверки внутри методов класса.

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self._balance = balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError('Сумма депозита должна быть положительной')
        self._balance += amount

    def withdraw(self, amount):
        if amount > self._balance:
            raise ValueError('Недостаточно средств')
        if amount <= 0:
            raise ValueError('Сумма снятия должна быть положительной')
        self._balance -= amount

    @property
    def balance(self):
        return self._balance

    def __str__(self):
        return f'Счет {self.owner}: {self._balance} рублей'

атрибуты класса python (атрибуты классов и объектов в python)

Пояснение: приватный атрибут _balance защищает данные от прямого изменения. Свойство balance предоставляет доступ только для чтения. Методы проверяют корректность входных данных и состояние счета.

Типичные ошибки: забывают проверять отрицательные суммы, используют публичный атрибут без проверок, теряют контекст при наследовании. Решение: всегда валидировать данные в методах, а не в __init__.

Вариант с использованием дескрипторов для автоматической проверки:

class PositiveNumber:
    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, obj, objtype=None):
        return obj.__dict__[self.name]

    def __set__(self, obj, value):
        if value < 0:
            raise ValueError('Значение не может быть отрицательным')
        obj.__dict__[self.name] = value

class BankAccount:
    balance = PositiveNumber()
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance

библиотека классов python (библиотека классов в python)

Этот подход отделяет логику проверки от основного класса, что улучшает переиспользование. Однако он менее читаем для новичков.

Задание 2. Как реализовать геометрическую фигуру с вычислением площади и периметра?

Класс Rectangle с заданными длинами сторон и вычисляемыми атрибутами.

class Rectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height

    @property
    def width(self):
        return self._width

    @width.setter
    def width(self, value):
        if value <= 0:
            raise ValueError('Ширина должна быть положительной')
        self._width = value

    @property
    def height(self):
        return self._height

    @height.setter
    def height(self, value):
        if value <= 0:
            raise ValueError('Высота должна быть положительной')
        self._height = value

    @property
    def area(self):
        return self._width * self._height

    @property
    def perimeter(self):
        return 2 * (self._width + self._height)

метод объекта python (методы объектов в python)

Использование свойств для вычисляемых значений позволяет обращаться к ним как к атрибутам и гарантирует актуальность данных.

Ошибка: хранение вычисляемых значений как отдельных атрибутов приводит к несогласованности при изменении сторон. Решение: вычислять на лету через свойства.

Вариант с использованием __slots__ для экономии памяти:

class Rectangle:
    __slots__ = ('_width', '_height')
    def __init__(self, width, height):
        self._width = width
        self._height = height
    # ... свойства аналогично

метод call python (метод __call__ в python)

Этот вариант подходит при создании миллионов экземпляров, но ограничивает добавление новых атрибутов.

Задание 3. Как организовать иерархию сотрудников с переопределением методов?

Базовый класс Employee и дочерние Manager, Developer с переопределением метода work.

class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def work(self):
        return f'{self.name} выполняет общие задачи'

class Manager(Employee):
    def work(self):
        return f'{self.name} управляет командой и планирует задачи'

class Developer(Employee):
    def __init__(self, name, salary, programming_language):
        super().__init__(name, salary)
        self.programming_language = programming_language

    def work(self):
        return f'{self.name} пишет код на {self.programming_language}'

Python структура объекта (структура объекта в python)

Пояснение: вызов super() позволяет расширить инициализацию родительского класса, а переопределение метода work реализует полиморфизм.

Типичная ошибка: забыть вызвать super().__init__ в дочернем классе, что приводит к отсутствию атрибутов базового класса. Решение: всегда вызывать super() при необходимости.

Вариант с использованием абстрактного базового класса (ABC) для обязательной реализации метода:

from abc import ABC, abstractmethod

class Employee(ABC):
    @abstractmethod
    def work(self):
        pass

class Manager(Employee):
    def work(self):
        return 'управление'

# Developer также обязан переопределить work

Python создание объектов (создание объектов в python)

Это гарантирует, что все подклассы предоставят реализацию, но усложняет код.

Задание 4. Как сделать вектор с поддержкой арифметических операций?

Определение магических методов __add__, __sub__, __mul__ для пользовательского класса Vector.

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        return Vector(self.x + other.x, self.y + other.y)

    def __sub__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        return Vector(self.x - other.x, self.y - other.y)

    def __mul__(self, scalar):
        if not isinstance(scalar, (int, float)):
            return NotImplemented
        return Vector(self.x * scalar, self.y * scalar)

    def __repr__(self):
        return f'Vector({self.x}, {self.y})'

Self object python (объект self в python)

Используется NotImplemented для корректной обработки типов. Возврат NotImplemented заставляет Python пытаться выполнить операцию с обратным порядком операндов.

Ошибка: не возвращать NotImplemented, а сразу выбрасывать исключение, что ломает рефлексивные операции. Решение: следовать протоколу, возвращая NotImplemented при неподдерживаемом типе.

Вариант с перегрузкой операторов через __iadd__ (in-place) и поддержкой унарных операций:

class Vector:
    def __iadd__(self, other):
        self.x += other.x
        self.y += other.y
        return self

    def __neg__(self):
        return Vector(-self.x, -self.y)

Это позволяет писать v += w и менять сам объект, а не создавать новый.

- Python класс данных (класс данных в python)
- Class method python (методы классов в python)
- Python object methods (методы объектов в python)

Расширенные примеры и нестандартные задачи

Использование классов-декораторов для логирования

Класс с методом __call__ может выступать в роли декоратора. Пример: логгер, записывающий вызовы функций.

Пример
class Logger:
    def __init__(self, func):
        self.func = func
        self.calls = 0

    def __call__(self, *args, **kwargs):
        self.calls += 1
        print(f'Вызов {self.func.__name__}, номер {self.calls}')
        return self.func(*args, **kwargs)

@Logger
def add(a, b):
    return a + b

print(add(1,2))
print(add(3,4))
Вызов add, номер 1
3
Вызов add, номер 2
7

Дескрипторы для автоматической валидации и кэширования

Дескриптор, кэширующий результат вычисления.

Пример
class CachedProperty:
    def __init__(self, func):
        self.func = func
        self.name = func.__name__

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        value = self.func(obj)
        obj.__dict__[self.name] = value  # кешируем
        return value

class Circle:
    def __init__(self, radius):
        self.radius = radius

    @CachedProperty
    def area(self):
        print('Вычисляю площадь...')
        return 3.14159 * self.radius ** 2

c = Circle(3)
print(c.area)
print(c.area)
Вычисляю площадь...
28.27431
28.27431

Слоты для оптимизации памяти при большом количестве объектов

Использование __slots__ в классах, где заранее известны все атрибуты, позволяет снизить потребление памяти.

Пример
class Point:
    __slots__ = ('x', 'y')
    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Point(1,2)
print(p.__slots__)  # доступно
# p.z = 3  # AttributeError

Этот подход экономит до 40% памяти по сравнению с обычным классом за счет отказа от __dict__.

Метаклассы и автоматическая регистрация подклассов

Метакласс, который собирает все подклассы в реестр.

Пример
class RegistryMeta(type):
    def __init__(cls, name, bases, attrs):
        super().__init__(name, bases, attrs)
        if not hasattr(cls, 'registry'):
            cls.registry = {}
        cls.registry[name] = cls

class Base(metaclass=RegistryMeta):
    pass

class A(Base):
    pass

class B(Base):
    pass

print(Base.registry)
{'Base': <class '__main__.Base'>, 'A': <class '__main__.A'>, 'B': <class '__main__.B'>}

Абстрактные базовые классы (ABC) с примесью (mixin)

Создание миксина для добавления функциональности сравнения объектов.

Пример
class ComparableMixin:
    def __lt__(self, other):
        return self.value < other.value if hasattr(self, 'value') else NotImplemented

    def __eq__(self, other):
        return self.value == other.value if hasattr(self, 'value') else NotImplemented

class Data(ComparableMixin):
    def __init__(self, value):
        self.value = value

d1 = Data(5)
d2 = Data(10)
print(d1 < d2)  # True
print(d1 == d2) # False
True
False

Использование миксинов позволяет избежать дублирования кода в разных классах.

Декоратор класса для проверки типов аргументов

Декоратор, который автоматически добавляет проверки в методы.

Пример
import functools

def type_check(*types):
    def decorator(cls):
        original_init = cls.__init__
        @functools.wraps(original_init)
        def new_init(self, *args, **kwargs):
            for i, (arg, t) in enumerate(zip(args, types)):
                if not isinstance(arg, t):
                    raise TypeError(f'Аргумент {i} должен быть {t.__name__}, получен {type(arg).__name__}')
            original_init(self, *args, **kwargs)
        cls.__init__ = new_init
        return cls
    return decorator

@type_check(str, int)
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

# Person('Alice', 'young')  # TypeError

Такой подход удобен для отладки и гарантии корректности входных данных.

Контекстный менеджер через класс (__enter__ и __exit__)

Пример
class FileManager:
    def __init__(self, filename, mode):
        self.filename = filename
        self.mode = mode

    def __enter__(self):
        self.file = open(self.filename, self.mode)
        return self.file

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.file.close()
        if exc_type:
            print(f'Ошибка: {exc_val}')
        return True  # подавляем исключение

with FileManager('test.txt', 'w') as f:
    f.write('Hello')

with FileManager('test.txt', 'r') as f:
    print(f.read())
Hello

Реализация контекстного менеджера в классе даёт полный контроль над ресурсами.

Задания на классы в Python - comments

En
Python классы задания (python)