[ Сборник задач ]
Тема 13. Классы и объекты

[ Сборник задач ]
Тема 13. Классы и объекты

Python Workbook Cover T1
Операторы: class
Контент: Вопросы (5шт) + задачи (5шт)

Оглавление

1
Введение
Рассмотрим специфику ООП в Python, структуру класса и вызов его экземпляров. Определим особенности методов и свойств классов. Ознакомимся с основными магическими методами (__str__, __repr__, __gt__, __slots__ и др.). Научимся модифицировать атрибуты в плане приватности доступа к ним.
Перейти
2
Вопросы и ответы
5 вопросов по теме "Классы и объекты" + ответы
Перейти
3
Условия задач
5 задач по теме двух уровней сложности: Базовый и *Продвинутый
Перейти
4
Решения задач
Приводим код решений указанных выше задач
Перейти
1
One

Введение

Объектно-ориентированное программирование (ООП) – один из подходов к реализации программного кода для проецирования сущностей реального мира. Считается, что введение классов и объектов упрощает понимание кода человеком. В Питоне все является объектами.

Для решения заданий потребуется усвоение следующих тем:
  1. Принципы ООП и их реализация в Python;
  2. Свойства и методы классов;
  3. Инициализация экземпляров классов;
  4. Получение и задание свойств через декораторы;
  5. Классовые и статические методы;
  6. Магические (dunder или magic) методы классов;
  7. Модификаторы доступа к методам и свойствам;
  8. Дескрипторы.

Читайте также

2
Two

Вопросы по теме "Классы и объекты"

1. Как связаны классы и объекты?
Классы – шаблоны (blueprint) конкретных объектов, т.е. на их основе создаются экземпляры, наследующие свойства и методы родителей.

ООП в языке Python базируется на следующей иерархии:
1. Имеется головной класс object(), являющийся основой для всех других (обычно, его явно не указывают);
2. Уровнем ниже расположены метаклассы , классы и подклассы (как самого Питона, так и пользовательские);
3. В результате получаем возможность создавать любое количество экземпляров классов, т.е. объектов.

А вообще говоря - все в Питоне является объектом (даже класс). Это просто нужно запомнить.
2. Для чего необходимо ключевое слово self в классах?
Так как на основе классов создаются конкретные объекты, необходима возможность получения доступа к каждому из них. Ключевое слово self обозначает текущий объект класса. Это некая договоренность (так как self никто не запрещает заменить на любое другое слово).

Слово self применяется в следующий случаях:
  1. В качестве первого аргумента у методов экземпляра класса;
  2. Для доступа к свойству объекта внутри класса.

Пример – IDE
---
class Hello:
____# self - указание на экземпляр класса
____def __init__(self, name):
________# Свойство объекта
________self.name = name
________print(f'Привет, {self.name}')

greet_me = Hello('Дмитрий')



Результат выполнения
---
Привет, Дмитрий
3. Как создаются и для чего нужны статические методы?
Методы классов в Питоне делятся на 3 типа:
  1. Методы экземпляров (наиболее часто используемые, в качестве первого аргумента всегда принимают self);
  2. Классовые методы (здесь первым параметром передается cls. Привязаны к данному классу, а не к его экземплярам. Способны менять состояние класса, но не его экземпляров);
  3. Статические методы (не требуют наличия особого первого аргумента. Фактически, не принадлежат никакому классу, а представляют собой независимую функцию, которую мы решили по причинам бизнес-логики включить в класс).
Для создания статического метода используют декоратор @staticmethod. Его можно вызывать как от имени класса, так и экземпляра. Главная причина использования – инкапсуляция (изоляция некоторой логики внутри класса). Также, код становится более читабельным и удобным при импорте (не нужно импортировать множество отдельных функций). Статические методы можно вызывать как от имени класса, так и объекта.

Пример – IDE
---
class Person:

____def __init__(self, name):
________self.name = name

____@staticmethod
____def status(year_of_birth):
________if 2021 - year_of_birth >= 18:
____________print('Вы можете смотреть все страницы сайта')
________else:
____________print('Часть страниц вам не доступна')


student = Person('Петр')
# Тесты
student.status(1991)
Person.status(2011)



Результат выполнения
---
Вы можете смотреть все страницы сайта
Часть страниц вам не доступна


Фактические, метод status() никто не мешает нам вынести за рамки класса, но мы его тут разместили для изоляции и удобства.
4. Как реализуется наследование классов в Python?
Наследование – один из основных принципов ООП. Его суть выражается в следующем: на основании одних классов (базовых, суперклассов) можно создавать другие (подклассы), наследующие их свойства и методы.

Такой подход существенно снижает дублирование кода:
  1. Нет нужды переписывать одни и те же методы и свойства у разных объектов;
  2. Методы и свойства наследников не запрещено дополнять или модифицировать.
Как уже упоминалось выше, принцип наследования упрощает представление объектов как сущностей в реальном мире. Другими словами, человеческое сознание работает сходным образом: окружающие нас вещи мы легко может выстроить иерархически. Например, воробей относится к птицам, а птицы являются представителями животного царства. Аналогичным образом поступают и при создании классов: сначала определяют общий, а от него создаются частные, со своими особенностями.

Пример – IDE
---
class Bird:

____def __init__(self, age, fly_distance):
________self.age = age
________self.fly_distance = fly_distance

____def fly(self):
________print(f"Птица может пролететь за раз километров: {self.fly_distance}")

____def human_age(self):
________print(f'Этой птице {self.age * 6} человеческих лет')


class Sparrow(Bird):

____def __init__(self, age, fly_distance, sound):
________super().__init__(age, fly_distance)
________self.sound = sound

____def human_age(self):
________print(f'Данному воробью {self.age * 25} человеческих лет')

____def sing(self):
________print(self.sound)


# Тесты
crow = Bird(11, 5)
crow.fly()
crow.human_age()
young_sparrow = Sparrow(1, 2, 'чик-чирик')
old_sparrow = Sparrow(3, 1, 'чирик-чирик')
young_sparrow.fly()
young_sparrow.sing()
young_sparrow.human_age()
old_sparrow.human_age()



Результат выполнения
---
Птица пролетела километров: 5
Этой птице 66 человеческих лет
Птица пролетела километров: 2
чик-чирик
Данному воробью 25 человеческих лет
Данному воробью 75 человеческих лет


Функция super() позволяет ссылаться на родительский суперкласс. Класс Sparrow унаследовал от Bird метод fly(), затем дополнительно мы ему создали собственный метод sound() и изменили метод предка human_age().
5*. Что такое дескрипторы данных?
Очень часто переменные, инициализируемые в классе, являются однотипными. Например, есть класс Employee (сотрудник), принимающий параметры: имя, фамилия, отчество, должность. Все они являются строками. Следовательно, прежде чем создать экземпляр класса, нужно проверить, что пользователь ввел строки. А для этого потребуются сеттеры, проверяющие тип вводимых параметров. В итоге, мы 4 раза повторим код проверки. Нарушается принцип DRY (don't repeat yourself).

Для таких ситуаций удобно использовать дескрипторы (они, к слову, широко применяются во фреймворке Django при создании моделей).

Дескриптор - такой атрибут объекта, поведение которого переопределяется специальными методами. Проще говоря, доступ к какому-то свойству экземпляра можно переопределить с учетом дополнительных проверок. Если делать эти верификации без дескрипторов, то один и тот же код начнет повторяться.

Существует 4 метода протокола дескрипторов:
  1. __get__() - получить значение свойства;
  2. __set__() - задать значение;
  3. __delete__() - удалить атрибут;
  4. __set_name__() - присвоить имя свойству (появился в Питоне версии 3.6).

Если применяется только метод __get__(), то мы имеем дело с дескриптором без данных, а если есть еще и __set__(), то речь будет идти о дескрипторе данных.

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

Пример – IDE
---
# Создаем класс с протоколами дескриптора
class StringChecker:

____# Получаем доступ к свойству
____def __get__(self, instance, owner):
________if instance is None:
____________return self
________return instance.__dict__[self.name]

____# Меняем свойство
____def __set__(self, instance, str_value):
________if not isinstance(str_value, str):
____________raise ValueError('Нужно предоставить строку')
________elif len(str_value) < 2:
____________raise ValueError('Необходимо минимум 2 буквы')
________instance.__dict__[self.name] = str_value

____# Задаем имя свойства
____def __set_name__(self, owner, name):
________self.name = name


class Employee:

____# Определяем атрибуты (их может быть любое количество)
____name = StringChecker()
____surname = StringChecker()
____patronymic = StringChecker()
____post = StringChecker()

____# Инициализируем свойства с учетом требуемых проверок
____def __init__(self, name, surname, patronymic, post):
________self.name = name
________self.surname = surname
________self.patronymic = patronymic
________self.post = post


# Тесты
director = Employee('Иван', 'Николаевич', 'Прогин', 'Директор')
print(director.__dict__)
director.name = 1
director.name = 'A'



Результат выполнения
---
{'name': 'Иван', 'surname': 'Николаевич', 'patronymic': 'Прогин', 'post': 'Директор'}
ValueError: Нужно предоставить строку
ValueError: Минимум две буквы в атрибуте требуется

3
Three

Задание по теме "Классы и объекты"

4
Two

Решение

Задача 1. Базовый уровень

Условие
Создайте класс Soda (для определения типа газированной воды), принимающий 1 аргумент при инициализации (отвечающий за добавку к выбираемому лимонаду). 
В этом классе реализуйте метод show_my_drink(), выводящий на печать «Газировка и {ДОБАВКА}» в случае наличия добавки, а иначе отобразится следующая фраза: «Обычная газировка».
При решении задания можно дополнительно проверить тип передаваемого аргумента: принимается только строка.
Решение - IDE
class Soda:
    def __init__(self, ingredient):
        if isinstance(ingredient, str):
            self.ingredient = ingredient
        else:
            self.ingredient = None

    def show_my_drink(self):
        if self.ingredient:
            print(f'Газировка и {self.ingredient}')
        else:
            print('Обычная газировка')

 
# Тесты
drink1 = Soda()
drink2 = Soda('малина')
drink3 = Soda(5)
drink1.show_my_drink()
drink2.show_my_drink()
drink3.show_my_drink()
Результат выполнения
Обычная газировка
Газировка и малина
Обычная газировка

Задача 2. Базовый уровень

Условие
Николаю требуется проверить, возможно ли из представленных отрезков условной длины сформировать треугольник. 
Для этого он решил создать класс TriangleChecker, принимающий только положительные числа. 
С помощью метода is_triangle() возвращаются следующие значения (в зависимости от ситуации):
– Ура, можно построить треугольник!;
– С отрицательными числами ничего не выйдет!;
– Нужно вводить только числа!;
– Жаль, но из этого треугольник не сделать.
Построить треугольник из отрезков можно лишь в одном случае: сумма длин двух любых сторон всегда больше третьей.
Решение - IDE
class TriangleChecker:
    def __init__(self, sides):
        self.sides = sides

    def is_triangle(self):
        if all(isinstance(side, (int, float)) for side in self.sides):
            if all(side > 0 for side in self.sides):
                sorted_sides = sorted(self.sides)
                if sorted_sides[0] + sorted_sides[1] > sorted_sides[2]:
                    return 'Ура, можно построить треугольник!'
                return 'Жаль, но из этого треугольник не сделать'
            return 'С отрицательными числами ничего не выйдет!'
        return 'Нужно вводить только числа!'
 
 
# Тесты
triangle1 = TriangleChecker([2, 3, 4])
print(triangle1.is_triangle())
triangle2 = TriangleChecker([77, 3, 4])
print(triangle2.is_triangle())
triangle3 = TriangleChecker([77, 3, 'Сторона3'])
print(triangle3.is_triangle())
triangle4 = TriangleChecker([77, -3, 4])
print(triangle4.is_triangle())
Результат выполнения
Ура, можно построить треугольник!
Жаль, но из этого треугольник не сделать
Нужно вводить только числа!
С отрицательными числами ничего не выйдет!

Задача 3. Базовый уровень

Условие
Евгения создала класс KgToPounds с параметром kg, куда передается определенное количество килограмм, а с помощью метода to_pounds() они переводятся в фунты. Чтобы закрыть доступ к переменной “kg” она реализовала методы set_kg() - для задания нового значения килограммов, get_kg()  - для вывода текущего значения кг. Из-за этого возникло неудобство: нам нужно теперь использовать эти 2 метода для задания и вывода значений. Помогите ей переделать класс с использованием функции property() и свойств-декораторов. Код приведен ниже.
 

class KgToPounds:

    def __init__(self, kg):
        self.__kg = kg

    def to_pounds(self):
        return self.__kg * 2.205

    def set_kg(self, new_kg):
        if isinstance(new_kg, (int, float)):
            self.__kg = new_kg
        else:
            raise ValueError('Килограммы задаются только числами')
    
    def get_kg(self):
        return self.__kg
Чтобы не задавать новые значения или не получать к ним доступ через два метода, можно реализовать предложенный класс через функцию property() или свойства-декораторы.
Вариант решения 1. Функция property()
Решение - IDE
class KgToPounds:
    def __init__(self, kg):
        self.__kg = kg

    def to_pounds(self):
        return self.__kg * 2.205

    def __set_kg(self, new_kg):
        if isinstance(new_kg, (int, float)):
            self.__kg = new_kg
        else:
            raise ValueError('Килограммы задаются только числами')

    def __get_kg(self):
        return self.__kg

    kg = property(__get_kg, __set_kg)
Вариант решения 2. Свойства-декораторы
Решение - IDE
class KgToPounds:
    def __init__(self, kg):
        self.__kg = kg

    def to_pounds(self):
        return self.__kg * 2.205
   
    @property
    def kg(self):
        return self.__kg
    
    @kg.setter
    def kg(self, new_kg):
        if isinstance(new_kg, (int, float)):
            self.__kg = new_kg
        else:
            raise ValueError('Килограммы задаются только числами')
 
 
# Тесты
weight = KgToPounds(12)
print(weight.to_pounds())
print(weight.kg)
weight.kg = 41
print(weight.kg)
weight.kg = 'десять'
Результат выполнения
26.46
12
41
ValueError: Килограммы задаются только числами

Задача 4. Базовый уровень

Условие
Николай – оригинальный человек. 
Он решил создать класс Nikola, принимающий при инициализации 2 параметра: имя и возраст. Но на этом он не успокоился. 
Не важно, какое имя передаст пользователь при создании экземпляра, оно всегда будет содержать “Николая”. 
В частности - если пользователя на самом деле зовут Николаем, то с именем ничего не произойдет, а если его зовут, например, Максим, то оно преобразуется в “Я не Максим, а Николай”.
Более того, никаких других атрибутов и методов у экземпляра не может быть добавлено, даже если кто-то и вздумает так поступить (т.е. если некий пользователь решит прибавить к экземпляру свойство «отчество» или метод «приветствие», то ничего у такого хитреца не получится).
Для ограничения количества наборов свойств и методов в экземпляре применяется специальный магический атрибут __slots__.
Решение - IDE
class Nikola:
	__slots__ = ['name', 'age']
 
	def __init__(self, name, age):
    	if name == 'Николай':
            self.name = name
        else:
            self.name = f'Я не {name}, а Николай'
    	self.age = age
 
 
# Тесты
person1 = Nikola('Иван', 31)
person2 = Nikola('Николай', 14)
print(person1.name)
print(person2.name)
person2.surname = 'Петров'
Результат выполнения
Я не Иван, а Николай
Николай
AttributeError: 'Nikola' object has no attribute 'surname'

Задача 5*. Продвинутый уровень

Условие
Строки в Питоне сравниваются на основании значений символов. 
Т.е. если мы захотим выяснить, что больше: «Apple» или «Яблоко», – то «Яблоко» окажется бОльшим. 
А все потому, что английская буква «A» имеет значение 65 (берется из таблицы кодировки), а русская буква «Я» – 1071 (с помощью функции ord() это можно выяснить). 
Такое положение дел не устроило Анну. 
Она считает, что строки нужно сравнивать по количеству входящих в них символов.
Для этого девушка создала класс RealString и реализовала озвученный инструментарий. Сравнивать между собой можно как объекты класса, так и обычные строки с экземплярами класса RealString. 
К слову, Анне понадобилось только 3 метода внутри класса (включая __init__()) для воплощения задуманного.
В общем случае для создания такого класса понадобится 4 метода, так как в Питоне реализованы «богатые» сравнения. Это значит, что если имеется сравнение «больше», то автоматом появится возможность осуществлять сравнение «меньше».
Решение - IDE
class RealString:
	def __init__(self, some_str):
    	self.some_str = str(some_str)
 
	def __eq__(self, other):
    	if not isinstance(other, RealString):
        	other = RealString(other)
    	return len(self.some_str) == len(other.some_str)
 
	def __lt__(self, other):
    	if not isinstance(other, RealString):
        	other = RealString(other)
    	return len(self.some_str) < len(other.some_str)
 
	def __le__(self, other):
    	return self == other or self < other
Чтобы повторить класс, придуманный Анной (с тремя методами), требуется воспользоваться декоратором @total_ordering из модуля functools (упрощает реализацию сравнений. Требует лишь 2 дополняющих варианта сравнения - например, больше и равно - чтобы автоматически "дописать" остальные).
Решение - IDE
from functools import total_ordering
 
 
@total_ordering
class RealString:
	def __init__(self, some_str):
    	self.some_str = str(some_str)
 
	def __eq__(self, other):
    	if not isinstance(other, RealString):
        	other = RealString(other)
    	return len(self.some_str) == len(other.some_str)
 
	def __lt__(self, other):
    	if not isinstance(other, RealString):
        	other = RealString(other)
    	return len(self.some_str) < len(other.some_str)
 
 
# Тесты
str1 = RealString('Молоко')
str2 = RealString('Абрикосы растут')
str3 = 'Золото'
str4 = [1, 2, 3]
print(str1 < str4)
print(str1 >= str2)
print(str1 == str3)
Результат выполнения
True
False
True
Как вам материал?

Читайте также