<jupyter_start><jupyter_text>Домашнее задание №2 (курс "Практикум по программированию на языке Python")  Тема: Объектно-ориентированное программирование на языке Python. Преподаватель: <NAME> (<EMAIL>)**Выдана**:   28 марта 2020**Дедлайн**:   21:00 12 апреля 2020**Среда выполнения**: Jupyter Notebook (Python 3.7) Правила:Результат выполнения задания - Jupyter Notebook с кодом и подробными ответами в случае теоретических вопросов. __Максимальное число баллов за задание - 20__.Все ячейки должны быть "выполненными", при этом результат должен воспроизводиться при проверке (на Python 3.7). Если какой-то код не был запущен или отрабатывает с ошибками, то пункт не засчитывается. Задание, сданное после дедлайна, _не принимается_. Можно отправить недоделанное задание, выполненные пункты будут оценены.Готовое задание отправляется на почту преподавателя.Задание выполняется самостоятельно. Если какие-то студенты будут уличены в списывании, все они автоматически получат за эту работу 0 баллов. Если вы нашли в Интернете какой-то специфичный код, который собираетесь заимствовать, обязательно укажите это в задании: вполне вероятно, что вы не единственный, кто найдёт и использует эту информацию.Удалять фрагменты формулировок заданий запрещается.  Постановка задачи:- В данной работе нужно    - ответить на ряд теоретических вопросов;    - решить набор задач, проверяющих владение ООП-инструментами языка;    - описать архитектуру приложения (набор интерфейсов и классов).- Ответы на теоретические вопросы должны быть полными и обоснованными.- Каждая задача представляет собой написание функции или класса, а также набора тестов, проверяющих работу решения в общих и крайних случаях.- Отсутствие тестов автоматически уменьшает количество баллов за задание как минимум в два раза, некачественные тесты также будут штрафоваться.- Даже если это не указано явно в требованиях, код должен быть по возможности неизбыточным, работать с разумной сложностью и объёмом потребялемой памяти, проверяющие могут снизить балл за задание, выполненное без учёта этого требования.- Результирующий код должен быть читаемым, с единой системой отступов и адеквантными названиями переменных, проверяющие могут снизить балл за задание, выполненное без учёта этого требования.- Предлагаемая архитектура должна быть снабжена комментариями, описывающими причину использования того или иного подхода/шаблона. __Задание 1 (2 балла):__ Дайте подробные ответы на следующие вопросы:1. В чём смысл инкапсуляции? Приведите пример конкретной ситуации в коде, в которой нарушение инкапсуляции приводит к проблемам.2. Какой метод называется статическим? Что такое параметр `self`?3. В чём отличия методов `__new__` и `__init__`?4. Какие виды отношений классов вы знаете? Для каждого приведите примеры. Укажите взаимные различия.5. Приведите 2 примера обоснованного использования шаблона Singleton в реальной жизни.6. Зачем нужны фабрики? Опишите смысл использования фабричного метода, фабрики и абстрактной фабрики, а также их взаимные отличия.1. Инкапсуляция преследует две основных цели: сокрытие сложной логики компонента от постороннего вмешательства и группировка логически связанных методов, свойств и полей в один объект. В чем заключается удобство инкапсуляции: при использовании некой библиотеки, программисту совершенно не важно знать, что у нее там под капотом, он пользуется ей и все. В тоже время разработчики этой библиотеки могут вносить самые кординальные изменения в реализацию методов, однако, при условии, что старые сигнатуры методов будут сохранены, разработчику не придется даже менять код.2. Статическим метод, называется метод, который работает на уровне класса, а не объекта класса, то есть для того, чтобы им воспользоваться, не нужно создавать объект класса. Параметр 'self' - ссылка объект, от которого вызван метод. Аналог this в c++/c/java.3.4.5.6. __Задание 2 (1 балл):__ Описать класс комплексных чисел, поддерживающий представление в полярном виде. Произвести перегрузку магических методов для основных арифметических операций (+, -, \*, /). Допускается использование модуля `math`.<jupyter_code>import math

class ComplexNumber:
    def __init__(self,real,imaginary):
        self.__real = real
        self.__imaginary = imaginary    
    def __repr__(self):
        return f'<{self.__real} + {self.__imaginary} * i>'
    def __neg__(self):
        return ComplexNumber(-self.__real,-self.__imaginary)
    def __module__(self):
        return self.__real ** 2 + self.__imaginary ** 2
    def __add__(self,other):
        return ComplexNumber(self.__real + other.__real, self.__imaginary + other.__imaginary)
    def __sub__(self,other):
        return self + - other
    def __mul__(self,other):
        real = self.__real * other.__real - self.__imaginary * other.__imaginary
        imaginary = self.__real * other.__imaginary + self.__imaginary * other.__real
        return ComplexNumber(real, imaginary)
    def __truediv__(self,other):
        denominator = other.__module__()
        real = (self.__real * other.__real + self.__imaginary * other.__imaginary) / denominator
        imaginary = (self.__imaginary * other.__real - self.__real * other.__imaginary) / denominator
        return ComplexNumber(real, imaginary)
    def polar(self):
        return (math.hypot(self.__real,self.__imaginary), math.atan2(self.__imaginary, self.__real))
    
nums = [ComplexNumber(5.0,3.0), ComplexNumber(-5.0,-3.0)]

print(nums[0])
print(nums[1])

sum_nums = nums[0] + nums[1]
sub_nums = nums[0] - nums[1]
mul_nums = nums[0] * nums[1]
div_nums = nums[0] / nums[1]

print(f'{nums[0]} + {nums[1]} = {sum_nums}')
print(f'{nums[0]} - {nums[1]} = {sub_nums}')
print(f'{nums[0]} * {nums[1]} = {mul_nums}')
print(f'{nums[0]} / {nums[1]} = {div_nums}')


polar = nums[0].polar()
print(polar)<jupyter_output><5.0 + 3.0 * i>
<-5.0 + -3.0 * i>
<5.0 + 3.0 * i> + <-5.0 + -3.0 * i> = <0.0 + 0.0 * i>
<5.0 + 3.0 * i> - <-5.0 + -3.0 * i> = <10.0 + 6.0 * i>
<5.0 + 3.0 * i> * <-5.0 + -3.0 * i> = <-16.0 + -30.0 * i>
<5.0 + 3.0 * i> / <-5.0 + -3.0 * i> = <-1.0 + 0.0 * i>
(5.8309518948453, 0.5404195002705842)<jupyter_text>__Задание 3 (1.5 балл):__ Составить описание класса для объектов-векторов, задаваемых координатами концов в N-мерном пространстве. Основой является список значений координат вектора, задаваемый списком. Обеспечить операции сложения и вычитания векторов с получением нового вектора (суммы или разности), вычисления скалярного произведения двух векторов, длины вектора, косинуса угла между векторами. Все операции, которые можно перегрузить с помощью магических методов, должны быть реализованы именно через них. Класс должен производить проверку консистентности аргументов для каждой операции и в случаях ошибок выбрасывать исключение `ValueError` с исчерпывающим объяснением ошибки.<jupyter_code>class Vector:
    def __init__(self, vector_values_list):
        if(type(vector_values_list) is not list):
            raise ValueError(f'Type of {vector_values_list} is not list')
        self.__values = vector_values_list
        self.__dimension = len(vector_values_list)
    def __getitem__(self,key):
        return self.__values[key]
    def __repr__(self):
        return f'{self.__values}'
    def module(self):
        return sum([self[i] ** 2 for i in range(0,self.__dimension)]) ** .5
    def __add__(self,other):
        if(self.__dimension != other.__dimension):
            raise ValueError(f'The dimension of the first vector({self.__dimension}) are not equal the dimension of the second vector({other.__dimension})')
        return [self[i] + other[i] for i in range(0,self.__dimension)]
    def __sub__(self,other):
        if(self.__dimension != other.__dimension):
            raise ValueError(f'The dimension of the first vector({self.__dimension}) are not equal the dimension of the second vector({other.__dimension})')
        return [self[i] - other[i] for i in range(0,self.__dimension)]
    def __mul__(self,other):
        if(self.__dimension != other.__dimension):
            raise ValueError(f'The dimension of the first vector({self.__dimension}) are not equal the dimension of the second vector({other.__dimension})')
        return sum([self[i] * other[i] for i in range(0,self.__dimension)])
    def cos(self, other):
        if(self.__dimension != other.__dimension):
            raise ValueError(f'The dimension of the first vector({self.__dimension}) are not equal the dimension of the second vector({other.__dimension})')
        return (self * other) / (self.module() * other.module())
    
first_vector = Vector([11,12,13])
second_vector = Vector([6,7,8])

print(first_vector)
print(second_vector)

sum_vectors = first_vector + first_vector
sub_vectors = first_vector - first_vector
scalar_mult_vectors = first_vector * second_vector
cos_between_vectors = first_vector.cos(second_vector)
print(sub_vectors)
print(f'{first_vector.module()}')
print(scalar_mult_vectors)
print(cos_between_vectors)<jupyter_output>[11, 12, 13]
[6, 7, 8]
[0, 0, 0]
20.83266665599966
254
0.9988395208729043<jupyter_text>__Задание 4 (1.5 балл):__ Опишите декоратор, который принимает на вход функцию и при каждом её вызове печатает строку "This function was called N times", где N - число раз, которое это функция была вызвана на текущий момент (пока функция существует как объект, это число, очевидно, может только неубывать).<jupyter_code>def calls_counter(func):
    def inside_func():
        calls_counter.counter+=1
        print(f'This function was called {calls_counter.counter} times')
    return inside_func

calls_counter.counter = 0

@calls_counter
def  foo():
    pass

foo()
foo()
foo()<jupyter_output>This function was called 1 times
This function was called 2 times
This function was called 3 times<jupyter_text>__Задание 5 (2.5 балла):__ Опишите декоратор класса, который принимает на вход другой класс и снабжает декорируемый класс всеми атрибутами входного класса, названия которых НЕ обрамлены с обеих сторон "\_\_". При этом приватные атрибуты-поля (начинающиеся в объявлении с "\_\_") должны иметь в декорируемом классе корректное имя.В случае конфликтов имён импортируемый атрибут должен получить имя с суффиксом "\_new".<jupyter_code>def copy_class_attrs(decorator_class):
    def __decorate(cls):
        for attr_name in decorator_class.__dict__:
            if(not attr_name.startswith('__') and not attr_name.endswith('__')):
                new_attr_name = attr_name
                if (not callable(getattr(decorator_class, attr_name)) and attr_name.startswith(f'_{decorator_class.__name__}__')):
                    new_attr_name = attr_name.replace(f'_{decorator_class.__name__}__','__')
                elif(hasattr(cls, attr_name)):
                    new_attr_name = f'_new{attr_name}'
                setattr(cls,new_attr_name,getattr(decorator_class,attr_name))
        return cls
    return __decorate

class Example:
    a = 'str'
    __d = 10
    def __init__(self):
        self.__a = 0
        self.__str ='hello world'
    def __p(self):
        pass
    def func(self):
        print('hello world')
    def sum(self):
        print('sum')
       
@copy_class_attrs(Example)
class Example2:
    def __init__(self):
        pass
    def func():
        pass
    
example = Example2()    
for attr in Example2.__dict__:
    print(attr)<jupyter_output>__module__
__init__
func
__dict__
__weakref__
__doc__
a
__d
_Example__p
_newfunc
sum<jupyter_text>__Задание 6 (4.5 баллов):__ Опишите класс для хранения двумерных числовых матриц на основе списков. Реализуйте поддержку индексирования, итерирования по столбцам и строкам, по-элементные математические операции (с помощью магических методов), операцию умножения матрицы (как метод `mat_prod` класса), транспонирование, поиска следа матрицы, а также поиск значения её определителя, если он существует, в противном случае соответствующий метод должен выводить сообщение об ошибке и возвращать `None`.Матрицу должно быть возможным создать из списка (в этом случае у неё будет одна строка), списка списков, или же передав явно три числа: число строк, число столбцов и значение по-умолчанию (которое можно не задавать, в этом случае оно принимается равным нулю). Все операции должны проверять корректность входных данных и бросать исключение с информативным сообщением в случае ошибки.Матрица должна поддерживать методы сохранения на диск в текстовом и бинарном файле и методы обратной загрузки с диска для обоих вариантов. Также она должна поддерживать метод копирования.В реализации математических операций запрещается пользоваться любыми функциями, требующими использования оператора `import`.<jupyter_code>import pickle

class Matrix:
    def __init__(self, values_list):
        self.__values = [[column for column in row] for row in values_list]

    @classmethod
    def init_from_list(cls, values_list):
        self.__values = values_list
        pass
    @classmethod
    def init_from_list_lists(cls, values_list):
        pass
    @classmethod
    def init_from_three_numbers(cls, rows_count,columns_count,default_value = 0):
        return cls([[default_value for _ in range(columns_count)] for _ in range(rows_count)])

    @property
    def rows(self):
        return len(self.__values)
    
    @property
    def columns(self):
        return len(self.__values[0])
        
    def __getitem__(self, row):
        return self.__values[row]
    
    def __iter__(self):
        return self.__values.__iter__()
    
    def __neg__(self):
        negative_matrix = Matrix.init_from_three_numbers(self.rows, self.columns)
        negative_matrix.__values = [[-self[i][j] for j in range(self.columns)] for i in range(self.rows)]
        return negative_matrix
    
    def __add__(self,other):
        if(self.rows != other.rows):
            raise ValueError(f'Rows in the first matrix ({self.rows}) is not equal to rows in second matrix ({other.rows})')
        if(self.columns != other.columns):
            raise ValueError(f'Columns in the first matrix ({self.columns}) is not equal to columns in second matrix ({other.columns})')    
        sum_matrix = Matrix.init_from_three_numbers(self.rows,self.columns)
        for i in range(sum_matrix.rows):
            for j in range(sum_matrix.columns):
                sum_matrix[i][j] = self[i][j] + other [i][j]
        return sum_matrix
    
    def __sub__(self, other):
        return self + - other
    
    def print(self):
        for row in self.__values:
            print(row)
    
    def mat_prod(self,other):
        if(self.columns != other.rows):
            raise ValueError(f'Columns in the first matrix ({self.columns}) is not equal to rows in second matrix ({other.rows})')
        mult = lambda a,b : sum(x * y for (x, y) in zip(a, b))
        mult_matrix = Matrix.init_from_three_numbers(self.rows,other.columns)
        other_transposed = other.transpose()
        for i in range(mult_matrix.rows):
            for j in range(mult_matrix.columns):
                mult_matrix[i][j] = mult(self[i], other_transposed[j])
        return mult_matrix
            
    def transpose(self):
        transposed_matrix = Matrix.init_from_three_numbers(self.columns,self.rows)
        transposed_matrix.__values = [[ self[i][j] for i in range(self.rows)] for j in range(self.columns)]
        return transposed_matrix
    
    def minor(self, row_number, column_number):
        minor_matrix = Matrix.init_from_three_numbers(self.rows - 1,self.columns - 1)
        minor_matrix.__values = [[ self[i][j] for j in range(self.columns) if j != column_number] for i in range(self.rows) if i != row_number]
        return minor_matrix
    
    def det(self):
        if(self.rows != self.columns):
            raise ValueError(f'Matrix is not square: count rows = {self.rows}, count columns = {self.columns}')
        matrix_size = self.rows    
        if(matrix_size == 1):
            return self[0][0]
        if(matrix_size == 2):
            return self[0][0] * self [1][1] - self[0][1] * self[1][0]
        determinant_value = sum((-1) ** i * self[0][i] * self.minor(0,i).det() for i in range(matrix_size))
        if(determinant_value == 0):
            print('Error: this matrix is degenerate ')
            return None
        return determinant_value
    
    def trace(self):
        if(self.rows != self.columns):
            raise ValueError(f'Matrix is not square: count rows = {self.rows}, count columns = {self.columns}')
        return [self[i][j] for i in range(self.rows) for j in range(self.columns) if i == j]
    
    def save_to_text_file(self,file_name):
        with open(file_name,'w') as text_file:
            text_file.write(f'{self}')
            
    def save_to_binary_file(self,file_name):
        with open(file_name,'wb') as binary_file:
            pickle.dump(self.__values, binary_file)
        
    def load_from_text_file(self,file_name):
        with open(file_name,'r') as text_file:
            self = pickle.load(text_file)
            
    def load_from_binary_file(self,file_name):
        with open(file_name,'rb') as binary_file:
            self.__values = pickle.load(binary_file)        
                    

matrix_from_lists = Matrix([[3, 6, 11,20], [2, 5, 8,2], [1, 4, 7,3],[5,11,3,4]])
matrix_from_lists.save_to_binary_file('./matrix')
new_matrix = Matrix.init_from_three_numbers(4,4)
new_matrix.load_from_binary_file('./matrix')
new_matrix.print()
print(Matrix.det(matrix_from_lists))
print(matrix_from_lists.trace())
degenerate_matrix = Matrix([[1,2,3],[4,5,6],[7,8,9]])
print(degenerate_matrix.det())
matrix_transposed = matrix_from_lists.transpose()
multed_matrix = matrix_from_lists.mat_prod(matrix_transposed)
multed_matrix.print()
for row in multed_matrix:
    print(row)
matrix = Matrix.init_from_three_numbers(4,6,4)
print(matrix[0][0])
print(matrix.print())
sum_matrix = matrix - matrix
sum_matrix.print()
negative_matrix = - matrix
negative_matrix.print()
negative_matrix.transpose()<jupyter_output>[3, 6, 11, 20]
[2, 5, 8, 2]
[1, 4, 7, 3]
[5, 11, 3, 4]
804
[3, 5, 7, 4]
Error: this matrix is degenerate 
None
[566, 164, 164, 194]
[164, 97, 84, 97]
[164, 84, 75, 82]
[194, 97, 82, 171]
[566, 164, 164, 194]
[164, 97, 84, 97]
[164, 84, 75, 82]
[194, 97, 82, 171]
4
[4, 4, 4, 4, 4, 4]
[4, 4, 4, 4, 4, 4]
[4, 4, 4, 4, 4, 4]
[4, 4, 4, 4, 4, 4]
None
[0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0]
[-4, -4, -4, -4, -4, -4]
[-4, -4, -4, -4, -4, -4]
[-4, -4, -4, -4, -4, -4]
[-4, -4, -4, -4, -4, -4]<jupyter_text>__Задание 7 (7 баллов):__ Спроектируйте программную архитектуру для кофе-машины исходя из следующих требований:- поддержка нескольких видов кофе;- возможность налития кофе в стаканы разных размеров (обычная порция, двойная порция);- возможность добавления в кофе разнообразных добавок в произвольных сочетаниях (одна добавка на одну обычную порцию, две - на двойную);- подсчёт стоимости заказа исходя из выбранного типа кофе и видов добавок;- учёт вносимых денежных средств (средства вносятся на депозит, после чего возможен заказ кофе);- возможность возвращения оставшихся на депозите средств по требованию пользователя;- учёт запасов кофе и добавок (в обычных порциях), информирование о невозможности выдачи продукта при его отсутствии;- возможность пополнения запасов кофе и выгрузки денег;- обеспечение консистентности (нельзя ни в какой ситуации выдать продукт без оплаты и не вернуть оплату в случае невыдачи продукта);- возможность установления и изменения цены на кофе, его различные порции и на добавки;- в случае выбора кофе без добавок машина должна перед подготовкой и выдачей продукта предложить пользователю рекламу добавки, подходящую именно для этого вида кофе, но только в том случае, если денег на депозите достаточно для оплаты кофе с добавкой (реализуется вызовом метода `suggest_add_on`, который возвращает в код True (согласие) или False (отказ));- для каждого вида кофе есть одна единственная максимально подходящая добавка, которая используется в рекламе в предыдущем пункте (у нескольких видов кофе добавка может быть одинаковой);- метод `suggest_add_on` печатает предложение добавить в указанный вид кофе подходящую для него добавку с указанием её стоимости (x2 для двойной порции кофе), после чего ожидает от пользователя ввод ответа с клавиатуры в виде "да" или "нет", на любой другой ввод функция должна запросить повторный ввод, указав на ошибку ввода и предложив снова ввести один из двух валидных ответов;В рамках решения задачи нужно спроектировать и описать набор интерфейсов и классов, реализующих указанную логику, подробно описать причины использования того или иного архитектурного решения, покрыть получившийся набор классов тестами. Архитектура должна получиться, с одной стороны, не избыточной, а с другой - гибкой и расширяемой.Следующие шаблоны проектирования и инструменты языка могут оказаться полезными для решения этой задачи (рекомендация не является строгой, можно использовать всё, что известно, главное - по делу):- декоратор- абстрактная фабрика- фасад- mixin- исключения- менеджер контекстаОбратите внимание, что использование любого средства должно быть обоснованным, оценка ставится за качество и продуманность архитектуры решения, а не за использование как можно большего числа шаблонов. Допускается использование приёмов, не описанных в рамках курса.Дополнительно можно получить __до 5 бонусных баллов__ за описание получившейся архитектуры в виде [UML-диаграммы](https://habr.com/ru/post/458680/).<jupyter_code>class CoffeeMachine:
    pass<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.09417290610612472, 'fraction_numerical': 0.02283402315955913, 'mean_word_length': 3.9858567122652446, 'pattern_counts': {'":': 0, '<': 37, '<?xml version=': 0, '>': 37, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 1, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 5, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '6', 'dir': 'jupyter-structured-clean-dedup', 'id': '575054', 'license': 'MIT', 'n_tokens_mistral': 8032, 'n_tokens_neox': 8771, 'n_words': 2197, 'path': 'tasks/mel-lain-mipt-python-task-oop.ipynb', 'repo_name': 'dep-810b-studs/mipt-python', 'size': '21520'}