Изучение классов 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 также обязан переопределить workPython создание объектов (создание объектов в 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 и менять сам объект, а не создавать новый.
Расширенные примеры и нестандартные задачи
Использование классов-декораторов для логирования
Класс с методом __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) # FalseTrue 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
Реализация контекстного менеджера в классе даёт полный контроль над ресурсами.