# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.15.2
#   kernelspec:
#     display_name: Python 3
#     language: python
#     name: python3
# ---

# + [markdown] slideshow={"slide_type": "slide"}
# # `Практикум по программированию на языке Python`
# <br>
#
# ## `Занятие 5: Основы ООП: проектирование кода, шаблоны проектирования`
# <br><br>
#
# ### `Мурат Апишев (mel-lain@yandex.ru)`
#
# #### `Москва, 2022`

# + [markdown] slideshow={"slide_type": "slide"}
# ### `Напоминание: принципы ООП`
#
# - **Абстракция** - выделение важных свойств объекта и игнорирование прочих<br><br>
#
# - **Инкапсуляция** - хранение данных и методов работы с ними внутри одного класса с доступом к данным только через методы<br><br>
#
# - **Наследование** - возможность создания наследников, получающих все свойства родителей с возможностью их переопределения и расширения<br><br>
#
# - **Полиморфизм** - возможность использования объектов разных типов с общим интерфейсом без информации об их внутреннем устройстве

# + [markdown] slideshow={"slide_type": "slide"}
# ### `Напоминание: класс, объект, интерфейс`
#
# - __Класс__ представляет собой тип данных (как int или str)
# - Это способ описания некоторой сущности, её состояния и возможного поведения
# - Поведение при этом зависит от состояния и может его изменять<br><br>
# - __Объект__ - это конретный представитель класса (как переменная этого типа)
# - У объекта своё состояние, изменяемое поведением
# - Поведение полностью определяется правилами, описанными в классе<br><br>
# - __Интерфейс__ - это класс, описывающий только поведение, без состояния
# - Создать объект типа интерфейса невозможно (если есть их поддержка на уровне языка)
# - Поведение полностью определяется правилами, описанными в классе
# - Вместо этого описываются классы, которые реализуют этот интерфейс и, в то же время, имеют состояние
# <br><br>

# + [markdown] slideshow={"slide_type": "slide"}
# ### `Виды отношений между классами`
#
# - __Наследование__ - класс наследует класс<br><br>
#     - __Реализация__ - класс реализует интерфейс<br><br>
# - __Ассоциация__ - горизонтальная связь между объектами двух классов (может быть "один ко многим")<br><br>
#     - __Композиция__ - вложенность объекта одного класса в другой (главный управляет жизненным циклом зависимого)<br><br>
#     - __Агрегация__ - вложенность объекта одного класса в другой (объекты остаются независимыми)

# + [markdown] slideshow={"slide_type": "slide"}
# ### `Объектно-ориентированное проектирование`
#
# - Проектирование - определение наборов интерфейсов, классов, функций, их свойств и взаимных отношений<br><br>
#
# - Система для решения одной и той же задачи может спроектирована многими способами<br><br>
#
# - Задача в том, чтобы спроектировать систему, которая будет<br><br>
#     - понятной, поддерживаемой
#     - неизбыточнойм
#     - несложно модифицируемой и расширяемой
#     - эффективной<br><br>
#
# - Для этого нужны собственный опыт, знания, основанные на опыте предшественников и владение возможностями языка<br><br>
#
# __ВАЖНО:__
# - шаблоны, завязанные на типизацию и интерфейсы, могут работать в Python и без строгой иерархии наследования за счёт duck-typing
# - при использовании аннотирования типов и статической проверки с номинальной типизацией это станет невозможным
# - по этой причине в коде везде определяются и наследуются корректные интерфейсы

# + [markdown] slideshow={"slide_type": "slide"}
# ### `Набор правил SOLID`
#
# - __Принцип единственной ответственности__ - объект класса отвечает за одну и только одну задачу<br><br>
#
# - __Принцип открытости/закрытости__ - класс должен быть открыт для расширения функциональности и закрыт для изменения<br><br>
#
# - __Принцип подстановки Барбары Лисков__ - наследник класса должен только дополнять родительский класс, а не менять в нём что-либо<br><br>
#
# - __Принцип разделения интерфейса__ - интерфейсы не должны покрывать много задач (класс не должен реализовывать методы, которые ему не нужны из-за слишком большого базового интерфейса)<br><br>
#
# - __Принцип инверсии зависимостей__ - нижестоящее в иерархии зависит от вышестоящего, а не наоборот<br><br>

# + [markdown] slideshow={"slide_type": "slide"}
# ### `Принцип единственной ответственности`
#
# Объект класса отвечает за одну и только одну задачу (имеет не более одной причины для изменения)
# -

class Analyzer:
    def read_data(self, input_path):
        pass
    
    def process_data(self):
        pass
    
    def print_results(self, output_path):
        pass


# - Класс состоит из трех независимых функциональностей - есть три причины для изменения.
# - Лучше разделить его на три класса, объединяемых на уровне интерфейса

# + [markdown] slideshow={"slide_type": "slide"}
# ### `Принцип открытости/закрытости`
#
# - Класс должен быть открыт для расширения функциональности и закрыт для изменения
#
# - Все последующие изменения должны производиться добавлением нового кода, а не переписыванием существующего
# -

class Croupier:
    def create_game(self):
        print('Deal two cards')
        print('Open five cards one by one')
        print('Count the winnings')


# - Данный крупье умеет обслуживать игру в техасский холдем
# - Если захотим использовать его для игры в преферанс, то придется меня логику `create_game`

# + [markdown] slideshow={"slide_type": "slide"}
# ### `Принцип открытости/закрытости`
#
# Исправить ситуацию можно по-разному, например, так:
# -

class Croupier:
    def create_game(self, game):
        game.startSteps()
        game.middleSteps()
        game.finishSteps()
    
    def startSteps(self): pass
    def middleSteps(self): pass
    def finishSteps(self): pass


class TexasHoldemGame(Croupier):
    def startSteps(self): print('Deal two cards')
    def middleSteps(self): print('Open three cards one by one')
    def finishSteps(self): print('Count the winnings')


class PreferansGame(Croupier):
    def startSteps(self): print('Deal ten cards')
    def middleSteps(self): print('Deal buy-in')
    def finishSteps(self): print('Count the winnings')


# + [markdown] slideshow={"slide_type": "slide"}
# ### `Принцип подстановки Лисков (LSP)`
#
# - Наследник класса должен только дополнять родительский класс, а не менять в нём что-либо<br><br>
# - Должна быть возможность вместо базового типа подставить любой его подтип<br><br>
# - Принцип не означает, что не нужно использовать перегрузку виртуальных методов<br><br>
#
# - Требуется, чтобы код наследника:
#     - не изменял состояния родительского класса
#     - не расширял предусловия
#     - не сужал постусловия

# +
class Base:
    def method(self, value):
        if not isinstance(value, int):
            raise Exception
        return abs(value)

class Deriv(Base):
    def method(self, value):
        if not isinstance(value, int) and value < 0:
            raise Exception  # 1st error (more pre-conditions)
        return value  # 2st error (less post-conditions)


# + [markdown] slideshow={"slide_type": "slide"}
# ### `Принцип разделения интерфейса`
#
# - Интерфейсы не должны покрывать много задач
# - Класс не должен реализовывать методы, которые ему не нужны из-за слишком большого базового интерфейса
# -

class CookingMachine:
    def prepare_ingredients(self): pass
    def heat_meal(self): pass


# - Такой подход работает для горячих блюд
# - Допустим, мы захотели приготовить салат - метод `heat_meal` на больше не нужен
# - Если же мы хотим сделать сок, то ингредиенты надо выжимать - нужен новый один метод `squeeze`, который лишний для блюд и салатов
#
# Проблему можно решить выстраиванием правильного набора интерфейсов - своего на каждый тип приготавливаемых блюд и напитков

# + [markdown] slideshow={"slide_type": "slide"}
# ### `Принцип инверсии зависимостей`
#
# - Нижестоящее в иерархии зависит от вышестоящего, а не наоборот<br><br>
#
# - Принцип состоит из двух пунктов:<br><br>
#     - Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций
#     - Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций<br><br>
#
# - Главное, что стоит вынести отсюда:<br><br>
#     - нужно программировать на уровне интерфейсов
#     - нужно избавляться от вложенных зависимостей
#     - объекты в системе должны быть слабосвязанными

# + [markdown] slideshow={"slide_type": "slide"}
# ### `Принцип инверсии зависимостей`

# +
class Printer:
    @staticmethod
    def print_text(text):
        print(text)

class Class:
    def __init__(self, value):
        self.value = value
        self.printer = Printer()
    
    def print_value(self):
        self.printer.print_text(self.value)


# -

# - `Class` зависит от `Printer`
# - `Class` может использовать для печати только `Printer`, т.е. стандартный вывод
#
# Принцип нарушен - связь между классами жесткая безо всякой нужды

# + [markdown] slideshow={"slide_type": "slide"}
# ### `Принцип инверсии зависимостей`
#
# Модифицируем код так, чтобы убрать зависимости:

# +
class PrinterInterface:
    @staticmethod
    def print_text(text):
        raise NotImplementedError

class StdoutPrinter(PrinterInterface):
    @staticmethod
    def print_text(text):
        print(text)

class Class:
    def __init__(self, value, printer):
        self.value = value
        self.printer = printer
    
    def print_value(self):
        self.printer.print_text(self.value)


# + [markdown] slideshow={"slide_type": "slide"}
# ### `Пример: отправитель отчётов`
#
# - Все принципы связаны между собой и нарушение одного часто приводит к нарушению сразу нескольких
# - Рассмотрим ниже пример (источник: https://blog.byndyu.ru/2009/12/blog-post.html)
# -

class Reporter:
    def send_reports():
        report_builder = ReportBuilder()
        reports = report_builder.create_reports()
        
        if len(reports) == 0:
            raise Exception

        report_sender = EmailReportSender()
        for report in reports:
            report_sender.send(report)


# Какие есть проблемы?

# + [markdown] slideshow={"slide_type": "slide"}
# ### `Пример: отправитель отчётов`
#
# Проблем в таком коде полно:
#
# - сложность тестирования: как проверить поведение `Reporter`, которое зависит от других классов?<br><br>
# - высокая связность, `Reporter`<br><br>
#     - требует, чтобы отчёты выдавал именно `ReportBuilder`
#     - требует, чтобы отправление производил именно `EmailReportSender`
#     - содаёт объекты обоих классов внутри себя<br><br>
#
# - Нарушаются сразу три принципа:<br><br>
#     - принцип единственной ответственности (класс должен просто отправлять отчёты)
#     - принцип открытости/закрытости (для отправки отчётов не по e-mail нам потребуется исправлять `Reporter`)
#     - принцип инверсии зависимостей (классы жёстко зависят друг от друга)

# + [markdown] slideshow={"slide_type": "slide"}
# ### `Пример: отправитель отчётов`
#
# Изменим код так, чтобы исправить эти проблемы:

# +
class ReportBuilderInterface:
    def create_reports(self):
        raise NotImplementedError

class ReportSenderInterface:
    def send(self, report):
        raise NotImplementedError

class Reporter:
    def __init__(self, report_builder, report_sender):
        self.report_builder = report_builder
        self.report_sender = report_sender

    def send_reports():
        reports = report_builder.create_reports()
        
        if len(reports) == 0:
            raise Exception

        for report in reports:
            report_sender.send(report)


# + [markdown] slideshow={"slide_type": "slide"}
# ### `Шаблоны ОО-проектирования`
#
# - Набор практик и рекомендаций по организации кода, полученных опытным путём<br><br>
# - Оптимизируют решения типовых задач, упрощают разработку и поддержание кода<br><br>
# - Существуют десятки шаблонов, кратко рассмотрим несколько основных:<br><br>
#     - Одиночка (Singleton)
#     - Стратегия
#     - Примесь (Mixin)
#     - Фасад
#     - Адаптер, DAO
#     - Простая фабрика
#     - Фабричный метод
#     - Абстрактная фабрика
#     - Декоратор

# + [markdown] slideshow={"slide_type": "slide"}
# ### `Шаблон Singleton`
#
# - Иногда возникает необходимость создать класс, который может иметь не более одного экземпляра (например, глобальный конфиг)
# - Есть несколько способов реализации такого поведения
#
# Один из вариантов:
# -

class Singleton:
    def __new__(cls):  # is called before init to create object
        if not hasattr(cls, 'instance'):
            cls.instance = super().__new__(cls)
        return cls.instance


# +
s = Singleton()
s.some_attr = 'some_attr'

s_1 = Singleton()
s_1.some_attr


# -

# Singleton также удобно реализовывать с помощью статического метода

# + [markdown] slideshow={"slide_type": "slide"}
# ### `Стратегия`
#
# - В системе может быть несколько различных алгоритмов, выполняющих одну задачу разными способами
# - __Неправильный__ способ: собрать все в один класс
# - __Правильный__ - _стратегия_:
#     - все алгоритмы реализуются своими классами, реализующими общий интерфейс
#     - общий контекст занимается подменой алгоритмов и вызовом общей процедуры выполнения

# +
class StrategyInterface:
    def execute(self, value): raise NotImplementedError

class StrategyA(StrategyInterface):
    def execute(self, value): return value

class StrategyB(StrategyInterface):
    def execute(self, value): return value * 2

class Context:
    def set_strategy(strategy):
        self.strategy = strategy

    def execute(value):
        return self.strategy.execute(value)


# + [markdown] slideshow={"slide_type": "slide"}
# ### `Шаблон Mixin`
#
# - Mixin - шаблон, основанный на множественном наследовании<br><br>
# - Используется в одном из двух основных случаев:<br><br>
#     - хочется иметь в одном классе много опциональных признаков
#     - хочется использовать один и тот же признак во многих классах<br><br>
#     
# - Реализуется в виде класса, содержащего необходимую функциональность<br><br>
#
# - Далее этот класс наследуется любым нужным классом, умеющим эту функциональность использовать<br><br>

# + [markdown] slideshow={"slide_type": "slide"}
# ### `Пример использования Mixin`

# +
class VersionMixin:
    def set_version(self, version):
        self._version = version
    
    @property  # One more example of decorator
    def version(self):
        return self._version if hasattr(self, '_version') else 'Unk. version'
    
class SomeBaseClass:
    def __init__(self, value):
        self.value = value

class SomeClass(SomeBaseClass, VersionMixin):
    def __init__(self, value):
        super().__init__(value)
        
sc = SomeClass(10)
print(sc.value)
sc.set_version(1.0)
print(sc.version)


# + [markdown] slideshow={"slide_type": "slide"}
# ### `Шаблон Фасад`
#
# Предназначен для сокрытия сложного поведения за простым внешним интерфейсом
# -

class ATMImpl:
    def check_pin(self, pin): pass
    def make_db_request(self, user): pass
    def check_money(self, user, amount): pass
    def send_money(self, amount): pass
    ...


class ATM:
    def __init__(self, atm_impl):
        self.atm_impl = atm_impl

    def get_money(self, pin, user, amount):
        pass  # use self.atm_impl methods to proceed operation


# + [markdown] slideshow={"slide_type": "slide"}
# ### `Шаблон Адаптер`
#
# - Предназначен для соединения двух систем, которые могут работать вместе, но имеют несовместимые интерфейсы
# - Особенно полезен при работе с внешними API

# +
class ValueProvider:
    def __init__(self, value):
        if not isinstance(value, int):
            raise ValueError
        self._value = value

    def get_value(self):
        return self._value

class ValueUser:
    def use_value(self, value_with_name):
        if not isinstance(value_with_name, tuple):
            raise ValueError

        print(value_with_name[0])


# +
class ValueAdapter:
    def transform_value(self, value):
        return (value, '')

vp = ValueProvider(10)
va = ValueAdapter()
vu = ValueUser()

vu.use_value(va.transform_value(vp.get_value()))


# + [markdown] slideshow={"slide_type": "slide"}
# ### `Шаблон Data Access Object`
#
# - Предоставление единого интерфейса доступа к источнику данных (как правило, к БД)
# - DAO скрывает детали уставления соединений, посылки и обработки результатов запросов и т.п.
# - По сути, это адаптер между клиентским приложением и базой данных

# +
class DbDao:
    def request(self, query): raise NotImplementedError


class DbOneDao(DbDao):
    def __init__(self, db_one_config):
        self._db_one_impl = DbOne(db_one_config)
    
    def request(self, query):
        return self._db_one_impl.request(query)


class DbTwoDao(DbDao):
    def __init__(self, db_two_config):
        self._db_two_impl = DbTwo(db_two_config)
    
    def request(self, query):
        return self._db_two_impl.proceed_request(query)


# + [markdown] slideshow={"slide_type": "slide"}
# ### `Фабрики`
#
# - Фабрика - это общая идея создания объекта с помощью какой-то другой сущности<br><br>
# - Создавать объекты могут<br><br>
#     - обычный функции
#     - методы этого же класса (в т.ч. и статические)
#     - методы других классов<br><br>
# - При это в зависимости от ситуации могут использоваться как примитивные решения, так и сложные фабрики<br><br>
# - Рассмотрим ниже три основных фабричных шаблона:<br><br>
#     - простая фабрика
#     - фабричный метод
#     - абстрактная фабрика

# + [markdown] slideshow={"slide_type": "slide"}
# ### `Шаблон Простая фабрика`
#
# - Есть объект класса `VendingMachine`, описывающий торговый автомат
# - Объект имеет метод `get_item`, который по имени товара возвращает объект этого товара
# -

class VendingMachine:
    def get_item(self, item):
        if item == 'cola':
            return Cola()  # skip ammount checks
        elif item == 'chocolate':
            return Сhocolate()
        else:
            raise ValueError('Unknown item')


# - Создаваемые продукты могут быть никак не связаны друг с другом
# - Класс `VendingMachine` является простой фабрикой

# + [markdown] slideshow={"slide_type": "slide"}
# ### `Шаблон Фабричный метод`
#
# - Есть объект класса `CottageBuilder`, который производит объекты-коттеджи `Cottage`<br><br>
# - Весь наш программный комплекс заточен под обработку производства коттеджей<br><br>
# - В некоторый момент было принято решение начать производство многоэтажных домов (`MultiStoreyBuilding`)<br><br>
# - Неправильная архитектура приведёт к необходимости переписывания кода во многих местах<br><br>
# - Фабричный метод позволяет избежать этого

# + [markdown] slideshow={"slide_type": "slide"}
# ### `Шаблон Фабричный метод: общая схема`
#
# - Имеется класс-производитель `ConcreteFactory` и класс-продукт `ConcreteProduct`
# - Вводим интерфейс производителя `FactoryInterface` и интерфейс продукта `ProductInterface`
# - Каждый конкретный производитель получает возможность создавать конкретный продукт с помощью одного и того же метода
#
# Для нашего примера код выглядит так:

# +
class BuilderInterface:
    def build(self):
        raise NotImplementedError

class BuildingInterface:
    def open_door(self, door_number): raise NotImplementedError
    def open_window(self, window_number): raise NotImplementedError
    ...


# +
class CottageBuilder(BuilderInterface):
    def build(self):
        return Cottage()

class MultiStoreyBuilder(BuilderInterface):
    def build(self):
        return MultiStoreyBuilding()


# + [markdown] slideshow={"slide_type": "slide"}
# ### `Шаблон Фабричный метод: общая схема`
#
# Осталось описать код классов продуктов (то есть зданий):
# -

class Cottage(BuildingInterface):
    def open_door(self, door_number): self._open_door(door_number)
    def open_window(self, window_number): self._open_window(window_number)
    ...


class MultiStoreyBuilder(BuildingInterface):
    def open_door(self, door_number): self._open_door(door_number)
    def open_window(self, window_number): self._open_window(window_number)
    ...


# - теперь в коде нет разницы, с каким зданием мы работаем, интерфейс у них всех общий
# - тип здания, который нам надо построить, определяется только внутри конкретного строителя

# + [markdown] slideshow={"slide_type": "slide"}
# ### `Шаблон Абстрактная фабрика`
#
# - Шаблон предназначен для создания систем взаимосвязанных объектов без указания их конкретных классов<br><br>
# - Проще всего разобрать на примере (источник: https://refactoring.guru/ru/design-patterns/abstract-factory)<br><br>
# - Задача:<br><br>
#     - есть набор одних и тех же элементов графического интерфейса (CheckBox, Button, TextField и прочие)
#     - в каждой операционной системе должны отображаться все эти элементы
#     - при этом в каждой OS собственный стиль отображения<br><br>
#
# - Опишем использование абстрактной фабрики для такого случая

# + [markdown] slideshow={"slide_type": "slide"}
# ### `Интерфейсы фабрик и продуктов`
# -

class AbstractGui:  # more declarative than necessary, remember about duck-typing
    def create_check_box(self): raise NotImplementedError
    def create_button(self): raise NotImplementedError
    def create_text_field(self): raise NotImplementedError
    ...


class AbstractCheckBox:
    def set_state(self): raise NotImplementedError
    ...


class AbstractButton:
    def on_press(self): raise NotImplementedError
    ...


class AbstractTextField:
    def is_empty(self): raise NotImplementedError
    ...


# + [markdown] slideshow={"slide_type": "slide"}
# ### `Конкретные классы фабрики и продуктов`
# -

class WindowsGui(AbstractGui):
    def create_check_box(self): return WindowsCheckBox()
    def create_button(self): return WindowsButton()
    def create_text_field(self): return WindowsTextField()
    ...


class WindowsCheckBox(CheckBox):
    def set_state(self): self._get_state_impl()
    ...


class WindowsButton(AbstractButton):
    def on_press(self): self._on_press_impl()
    ...


class WindowsTextField(AbstractTextField):
    def is_empty(self): self._is_empty_impl()
    ...


# Аналогично определим классы для Mac OS и Ubuntu (Unity)

# + [markdown] slideshow={"slide_type": "slide"}
# ### `Использование абстрактной фабрики`
# -

class GuiApplication:
    def __init__(self):
        import sys
        
        self.gui = None
        if sys.platform == 'win32':
            self.gui = WindowsGui()
        elif sys.platform == 'linux':
            self.gui = LinuxGui()
        elif sys.platform == 'darwin':
            self.gui = MacOsGui()
        else:
            raise SystemError(f'Unsupported OS type {sys.platform}')

    def draw_window(self):
        # write cross-platform code, based on interface, not implementations
        self.gui.create_button()
        self.gui.create_check_box()
        self.gui.create_text_field()
        ...


# + [markdown] slideshow={"slide_type": "slide"}
# ### `Шаблон Декоратор`
#
# - Шаблон проектирования, позволяющий динамически наделить объект дополнительными свойствами<br><br>
# - Реализация включает в себя интерфейс, реализующие его объекты-компоненты и объекты-декораторы<br><br>
# - Задача из жизни: сбор набора метрик для отправки в сервис мониторинга<br><br>
#     - собирается несколько сотен различных метрик
#     - метрики могут агрегироваться несколькими способами (mean/median/mode/max/min по интервалу)
#     - метрики могут преобразовываться несколькими способами (положительная срезка, фильтрация значений)<br><br>
# - Модификаторы могут применяться в произвольных сочетаниях, количествах и порядках<br><br>
#
# - Как можно реализовать подобную систему без необходимости расписывать кучу условий?

# + [markdown] slideshow={"slide_type": "slide"}
# ### `Декоратор идеален для такой задачи`
#
# Опишем интерфейс метрики и реализующие его классы:
# -

class MetricInterface():  # again for Python it's unnecessary declaration
    def calculate(self): raise NotImplementedError


class MetricA(MetricInterface):
    def calculate(self):
        self.values = self._calculate_a()  # returns list


class MetricB(MetricInterface):
    def calculate(self):
        self.values = self._calculate_b()  # returns list


# + [markdown] slideshow={"slide_type": "slide"}
# ### `Собственно декораторы`
#
# Теперь опишем классы операций над метриками, которые тоже реализуют интерфейс метрики:
# -

class CalculateMeanDecorator(MetricInterface):
    def __init__(self, metric):
        self.metric = metric
    
    def calculate(self):
        self.values = [self._calculate_mean(self.metric.calculate())]  #[scalar] == list


class FilterZerosDecorator(MetricInterface):
    def __init__(self, metric):
        self.metric = metric
    
    def calculate(self):
        self.values = list(filter(lambda x: x != 0.0, self.metric.calculate()))


# + [markdown] slideshow={"slide_type": "slide"}
# ### `Использование декоратора`
#
# Теперь можно легко создавать метрики и тут же снабжать их нужными свойствами:

# +
metric_1 = MetricA()
metric_2 = MetricB()
metric_3 = FilterZerosDecorator(MetricA())
metric_4 = CalculateMeanDecorator(FilterZerosDecorator(MetricB()))

for metric in [metric_1, metric_2, metric_3, metric_4]:
    metric.calculate()
    MetricSender.send(metric.values)


# + [markdown] slideshow={"slide_type": "slide"}
# ### `Снова о декораторах в синтаксисе языка Python`
#
# - Шаблон "Декоратор" и декоратор в Python - это разные вещи!
# - Шаблон используется обычно в языках со статической типизацией для динамического добавления объектам новых свойств
# - Декораторы в Python - это инструмент языка, предназначенный для добавления новых свойств функциям, классам и методам на этапе их определения
# - В Python это особенно просто за счёт duck-typing
# - Этот тот случай, когда нужно вспомнить о замыканиях (функциях, возвращающих функции)

# + [markdown] slideshow={"slide_type": "slide"}
# ### `Пример декоратора функции`
#
# Опишем декоратор, замеряющий время работы функции:
# -

def timed(callable_obj):
    import time

    def __timed(*args, **kw):
        time_start = time.time()
        result = callable_obj(*args, **kw)
        time_end = time.time()
        
        print('{}  {:.3f} ms'.format(callable_obj.__name__,
                                     (time_end - time_start) * 1000))
        return result

    return __timed


@timed
def func():
    for i in range(1000000):
        pass
func()

# + [markdown] slideshow={"slide_type": "slide"}
# ### `Пример декоратора функции`
#
# Посмотрим, что это за функция на самом деле:
# -

import inspect
lines = inspect.getsource(func)
print(lines)


# + [markdown] slideshow={"slide_type": "slide"}
# ### `Пример декоратора класса`
#
# Опишем декоратор класса, который получает на вход декоратор функции и оборачивает его вокруг каждого публичного метода класса:
# -

def decorate_class(decorator):
    def _decorate(cls):
        for f in cls.__dict__:
            if callable(getattr(cls, f)) and not f.startswith("_"):
                setattr(cls, f, decorator(getattr(cls, f)))
        return cls
    return _decorate


# +
@decorate_class(timed)
class Cls:
    a = 10
    def method(self): pass
    def _method_2(self): pass

Cls().method()
Cls().a  # not callable
Cls()._method_2()  # not public
# -

# При определении класса он производится вызов `decorate_class._decorate`, которая возвращает обновлённый класс.

# + [markdown] slideshow={"slide_type": "slide"}
# ### `Мета-классы в Python`
#
# - Вспомним, что в Python всё, включая классы, является объектами <br><br>
# - Это позволяет создавать классы и менять их свойства (ещё один способ помимо декораторов)<br><br>
# - Описание класса определяет свойства объекта<br><br>
# - Описание мета-класса определяет свойства класса<br><br>
# - В Python есть один класс, не являющийся объектом - это мета-класс `type`

# + [markdown] slideshow={"slide_type": "slide"}
# ### `Мета-классы в Python`
#
# `type` можно использовать напрямую для создания классов:

# +
Class = type('MyClass', (object, ), {'field': lambda self: 'value'})
c = Class()

print(type(c))
print(c.field())


# + [markdown] slideshow={"slide_type": "slide"}
# ### `Мета-классы в Python`
#
# - А можно на основе `type` описывать мета-классы
#
# - Напишем простой мета-класс, который добавляет метод `hello` в создаваемый класс
#
# - Источник примера: https://gitjournal.tech/metaklassy-i-metaprogrammirovanie-v-python/ 

# +
class HelloMeta(type):  # always inherit type or it's childs
    def hello(cls):
        print("greetings from %s, a HelloMeta type class" % (type(cls())))

    # call meta-class
    def __call__(self, *args, **kwargs):
        cls = type.__call__(self, *args, **kwargs)  # create class as usual

        setattr(cls, "hello", self.hello)  # add 'hello' attribute

        return cls

class TryHello(object, metaclass=HelloMeta):  
    def greet(self):
        self.hello()

greeter = TryHello()  
greeter.greet()

# + [markdown] slideshow={"slide_type": "slide"}
# ### `Зачем нужно работать с мета-классами?`
#
# - На самом деле, это не нужно почти никогда, лучше использовать декораторы<br><br>
# - Бывает полезно в тех случаях, когда нужно штамповать классы с заданными свойствами или при отсутствии информации о деталях класса до момента выполнения кода<br><br>
# - **Пример**:
#     - вы определили класс данных с методами обработки, зависящими от формата
#     - класс имеет метакласс, который определяется аргументами программы
#     - в аргументах передаются различные форматы и методы обработки
#     - мета-класс в зависимости от них переопределяет методы вашего класса<br><br>
#
# - **Пример**: генерация API, в текущем контексте может требоваться, чтобы все методы ваших классов были в верхнем регистре<br><br>

# + [markdown] slideshow={"slide_type": "slide"}
# ### `Спасибо за внимание!`
