Практика 1. Практическая работа 1. Классы, объекты и инкапсуляция
Цель
Закрепить базовые понятия ООП на Python: проектирование классов, разграничение
атрибутов экземпляра и класса, методы трёх видов (self, @classmethod,
@staticmethod), инкапсуляцию (_protected, __private, name mangling),
управляемый доступ через @property и магические методы __str__, __repr__,
__eq__. По итогам работы вы должны уметь самостоятельно описать предметную
сущность классом с продуманным интерфейсом и защищёнными инвариантами.
Краткая теория
- Класс задаёт состояние (атрибуты), поведение (методы) и инварианты (ограничения на состояние). Объект — конкретный экземпляр со своей идентичностью и значениями атрибутов.
__init__инициализирует объект;self— ссылка на текущий экземпляр. Атрибут класса общий для всех объектов, атрибут экземпляра создаётся в__init__черезself.- Метод экземпляра работает с объектом (
self);@classmethodполучает класс (cls) и часто служит фабричным конструктором;@staticmethod— вспомогательная функция в пространстве имён класса. - Инкапсуляция в Python «мягкая»:
_name— соглашение «для внутреннего использования»;__name— name mangling (_Класс__name);@propertyдаёт контролируемый доступ (геттер/сеттер с валидацией, вычисляемые свойства). - Магические методы:
__str__— представление для пользователя,__repr__— однозначное представление для отладки (по нему в идеале можно воссоздать объект),__eq__— сравнение «по значению» (возвращаетNotImplementedдля чужих типов).
Общие требования ко всем заданиям:
- добавляйте аннотации типов для параметров и возвращаемых значений;
- у каждого класса определяйте
__repr__; - нарушение инвариантов сигнализируйте исключением
ValueError; - сопровождайте решения минимальными проверками (
assertилиpytest).
Задания
Задание 1. Класс Rectangle (базовый)
Реализуйте класс прямоугольника с вычисляемыми характеристиками.
Требования:
- атрибуты экземпляра
width: float,height: float; - методы
area() -> floatиperimeter() -> float; __repr__, по которому можно восстановить объект;- стороны должны быть положительными (иначе
ValueError).
class Rectangle: def __init__(self, width: float, height: float) -> None: ... # сохраните стороны, проверьте положительность
def area(self) -> float: ... def perimeter(self) -> float: ... def __repr__(self) -> str: ... # пример: Rectangle(width=3, height=4)Проверка: Rectangle(3, 4).area() == 12 и .perimeter() == 14.
Задание 2. Класс Counter (атрибуты класса)
Покажите разницу между атрибутом класса и атрибутом экземпляра.
Требования:
- атрибут класса
created— общее число созданных объектов; - атрибут экземпляра
count— собственное значение счётчика (стартует с 0); - метод
inc()увеличиваетcountна 1; @classmethod how_many() -> intвозвращаетcreated.
class Counter: created = 0 # общий для всех экземпляров
def __init__(self) -> None: ... def inc(self) -> None: ...
@classmethod def how_many(cls) -> int: ...Проверка: после создания двух объектов Counter.how_many() == 2, а их count
независимы.
Задание 3. Класс Temperature (@property и инкапсуляция)
Реализуйте температуру с контролируемым доступом и вычисляемым свойством.
Требования:
- храните значение в «защищённом» атрибуте
_celsius; - свойство-геттер
celsiusи сеттерcelsiusс проверкой: ниже-273.15выбрасывайтеValueError; - вычисляемое свойство
fahrenheit(только чтение):c * 9 / 5 + 32; - альтернативный конструктор
@classmethod from_fahrenheit(value).
class Temperature: def __init__(self, celsius: float) -> None: self._celsius = ... # пройдите через проверку сеттера
@property def celsius(self) -> float: ...
@celsius.setter def celsius(self, value: float) -> None: ... # валидация абсолютного нуля
@property def fahrenheit(self) -> float: ...
@classmethod def from_fahrenheit(cls, value: float) -> "Temperature": ...Проверка:
t = Temperature(25)assert t.fahrenheit == 77.0t.celsius = 30 # сеттер срабатывает# t.celsius = -300 # должно вызвать ValueErrorЗадание 4. Класс BankAccount (инкапсуляция + магические методы)
Объедините инкапсуляцию состояния с интеграцией в синтаксис языка.
Требования:
- баланс храните в
_balance, номер счёта — в__number(name mangling); - свойство
balanceтолько на чтение; - методы
deposit(amount)иwithdraw(amount)с валидацией: сумма должна быть положительной, снятие не должно уводить баланс в минус (иначеValueError); __str__— «человеческое» представление (например,Счёт ****1234: 150 RUB);__repr__— техническое представление для отладки;__eq__— равенство «по значению» (по номеру счёта и балансу), для чужих типов вернутьNotImplemented;@staticmethod is_valid_amount(amount) -> bool— вспомогательная проверка.
class BankAccount: def __init__(self, number: str, balance: float = 0) -> None: self.__number = ... self._balance = ...
@property def balance(self) -> float: ...
def deposit(self, amount: float) -> None: ... def withdraw(self, amount: float) -> None: ...
@staticmethod def is_valid_amount(amount: float) -> bool: ...
def __str__(self) -> str: ... def __repr__(self) -> str: ... def __eq__(self, other) -> bool: ...Проверка:
acc = BankAccount("1234", 100)acc.deposit(50)acc.withdraw(30)assert acc.balance == 120# acc.balance = 0 # AttributeError: свойство только на чтениеЗадание 5. Класс Fraction (повышенной сложности)
Реализуйте неизменяемую (по соглашению) обыкновенную дробь.
Требования:
- атрибуты
_numerator,_denominator; знаменатель не равен нулю (ValueError); - при инициализации сокращайте дробь (используйте
math.gcd) и нормализуйте знак (минус хранится в числителе); - свойства
numeratorиdenominatorтолько на чтение; - арифметика:
__add__и__mul__(возвращают новыйFraction); __eq__— сравнение по значению;__repr__видаFraction(3, 4)и__str__вида3/4;@classmethod from_int(value)— создать дробь из целого числа.
from math import gcd
class Fraction: def __init__(self, numerator: int, denominator: int) -> None: ... # проверка знаменателя, сокращение, нормализация знака
@property def numerator(self) -> int: ...
@property def denominator(self) -> int: ...
def __add__(self, other: "Fraction") -> "Fraction": ... def __mul__(self, other: "Fraction") -> "Fraction": ...
@classmethod def from_int(cls, value: int) -> "Fraction": ...Проверка:
assert Fraction(2, 4) == Fraction(1, 2)assert Fraction(1, 2) + Fraction(1, 3) == Fraction(5, 6)assert str(Fraction(6, 8)) == "3/4"Критерии оценки
- Задания 1–2 (базовые) — корректные классы, разделение атрибутов класса и
экземпляра, наличие
__repr__: 30%. - Задания 3–4 — корректная работа
@property(геттер/сеттер/только чтение), инкапсуляция (_/__), валидация черезValueError, фабричный/статический метод: 35%. - Задание 5 — корректные магические методы (
__str__,__repr__,__eq__, арифметика), сокращение и нормализация дроби: 20%. - Качество кода — аннотации типов, читаемое именование, отсутствие дублирования, наличие проверок/тестов: 15%.
Штрафы: прямой доступ к «защищённым» атрибутам извне вместо интерфейса, отсутствие валидации инвариантов, изменяемый атрибут класса вместо атрибута экземпляра.
Вопросы для самопроверки
- Чем атрибут класса отличается от атрибута экземпляра? Что произойдёт с
createdиз задания 2, если объявить его в__init__черезself? - Зачем в задании 3 инициализация в
__init__идёт через сеттер, а не присваиванием напрямую в_celsius? - В чём разница между
_balanceи__numberв задании 4? Как добраться до__numberизвне и почему это «мягкая» защита? - Почему свойство
balanceсделано только на чтение? Как это согласуется с принципом инкапсуляции? - Зачем
__eq__возвращаетNotImplementedдля объектов чужого типа, а неFalse? - В чём разница между
__str__и__repr__? Что выведетprint(obj), если определён только__repr__? - Почему
from_fahrenheitиfrom_intоформлены как@classmethod(сcls), а не как@staticmethod? Какое преимущество это даёт при наследовании? - Почему дробь из задания 5 разумно считать неизменяемой и возвращать новый
объект в
__add__/__mul__, а не менять текущий?