{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Decorators - Dynamically Alter The Functionality Of Your Functions\n",
    "\n",
    "- [Video](https://www.youtube.com/watch?v=kr0mpwqttM0&feature=youtu.be)\n",
    "- [Source code](https://github.com/CoreyMSchafer/code_snippets/blob/master/Decorators/snippets.txt)\n",
    "- http://ot-note.logdown.com/posts/67571/-decorator-with-without-arguments-in-function-class-form\n",
    "- https://www.programiz.com/python-programming/decorator"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Closures\n",
    "\n",
    "https://www.programiz.com/python-programming/closure\n",
    "\n",
    "A **Closure** is a function object that remembers values in enclosing scopes even if they are not present in memory.\n",
    "\n",
    "The criteria that must be met to create closure in Python are summarized in the following points.\n",
    "\n",
    "- We must have a nested function (function inside a function).\n",
    "- The nested function must refer to a value defined in the enclosing function.\n",
    "- The enclosing function must return the nested function.\n",
    "\n",
    "#### When to use closures?\n",
    "\n",
    "So what are closures good for?\n",
    "\n",
    "- Closures can avoid the use of global values and provides some form of **data hiding**. It can also provide an **object oriented solution** to the problem.\n",
    "- When there are few methods (one method in most cases) to be implemented in a class, closures can provide an alternate and more elegant solutions. But when the number of attributes and methods get larger, better implement a class.\n",
    "\n",
    "All function objects have a __closure__ attribute that returns a tuple of cell objects if it is a closure function."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2017-07-19T22:13:42.059269Z",
     "start_time": "2017-07-19T22:13:42.044588Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "hi\n",
      "bye\n"
     ]
    }
   ],
   "source": [
    "def outer_function(msg):\n",
    "    def inner_function():\n",
    "        print(msg)\n",
    "    return inner_function\n",
    "\n",
    "hi_func = outer_function('hi')\n",
    "bye_func = outer_function('bye')\n",
    "\n",
    "hi_func()\n",
    "bye_func()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Usage of closure in Decorators\n",
    "\n",
    "Decorators in Python make an extensive use of closures as well:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2017-07-19T22:22:27.458546Z",
     "start_time": "2017-07-19T22:22:27.433885Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "wrapper executed this before \"display\"\n",
      "display function ran\n",
      "wrapper executed this before \"display_info\"\n",
      "display_info ran with arguments (John, 35)\n"
     ]
    }
   ],
   "source": [
    "def decorator_function(original_function):\n",
    "    def wrapper_function(*args, **kwargs):\n",
    "        print('wrapper executed this before \"{}\"'.format(\n",
    "            original_function.__name__))\n",
    "        original_function(*args, **kwargs)\n",
    "    return wrapper_function\n",
    "\n",
    "\n",
    "def display():\n",
    "    print('display function ran')\n",
    "\n",
    "\n",
    "@decorator_function\n",
    "def display_info(name, age):\n",
    "    print('display_info ran with arguments ({name}, {age})'.format(\n",
    "        name=name, age=age))\n",
    "\n",
    "\n",
    "decorated_function = decorator_function(display)\n",
    "decorated_function()\n",
    "display_info('John', 35)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Class-style Decorators\n",
    "\n",
    "https://youtu.be/FsAPt_9Bf3U?t=13m25s"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2017-07-19T22:33:48.996538Z",
     "start_time": "2017-07-19T22:33:48.974714Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Enter DecoratorClass init\n",
      "Exit DecoratorClass init\n",
      "Call method before \"display_info\"\n",
      "\"display_info\" ran with arguments (John, 35)\n",
      "Call method after \"display_info\"\n"
     ]
    }
   ],
   "source": [
    "class DecoratorClass(object):\n",
    "\n",
    "    def __init__(self, original_function):\n",
    "        print('Enter {} init'.format(self.__class__.__name__))\n",
    "        # tie the original_function to the instance of Class\n",
    "        self.original_function = original_function\n",
    "        print('Exit {} init'.format(self.__class__.__name__))\n",
    "\n",
    "    def __call__(self, *args, **kwargs):\n",
    "        print('Call method before \"{}\"'.format(self.original_function.__name__))\n",
    "        self.original_function(*args, **kwargs)\n",
    "        print('Call method after \"{}\"'.format(self.original_function.__name__))\n",
    "\n",
    "@DecoratorClass\n",
    "def display_info(name, age):\n",
    "    print('\"display_info\" ran with arguments ({name}, {age})'.format(\n",
    "        name=name, age=age))\n",
    "\n",
    "\n",
    "display_info('John', 35)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Practical Examples"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2017-07-19T22:35:01.185895Z",
     "start_time": "2017-07-19T22:35:01.143134Z"
    },
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "from functools import wraps\n",
    "\n",
    "\n",
    "def my_logger(orig_func):\n",
    "    import logging\n",
    "    \n",
    "    logging.basicConfig(filename='{}.log'.format(\n",
    "        orig_func.__name__), level=logging.INFO)\n",
    "\n",
    "    @wraps(orig_func)\n",
    "    def wrapper(*args, **kwargs):\n",
    "        logging.info(\n",
    "            'Ran with args: {}, and kwargs: {}'.format(args, kwargs))\n",
    "        return orig_func(*args, **kwargs)\n",
    "\n",
    "    return wrapper\n",
    "\n",
    "\n",
    "def my_timer(orig_func):\n",
    "    import time\n",
    "\n",
    "    @wraps(orig_func)\n",
    "    def wrapper(*args, **kwargs):\n",
    "        t1 = time.time()\n",
    "        result = orig_func(*args, **kwargs)\n",
    "        t2 = time.time() - t1\n",
    "        print('{} ran in: {} sec'.format(orig_func.__name__, t2))\n",
    "        return result\n",
    "\n",
    "    return wrapper\n",
    "\n",
    "\n",
    "import time"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Mininal Example by Dan Bader\n",
    "\n",
    "https://github.com/dbader/schedule/blob/master/FAQ.rst"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "******************** Boom! ********************\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-8-52382694de5b>\", line 8, in wrapper\n",
      "    return job_func(*args, **kwargs)\n",
      "  File \"<ipython-input-8-52382694de5b>\", line 19, in bad_task\n",
      "    return 1 / 0\n",
      "ZeroDivisionError: division by zero\n",
      "\n"
     ]
    }
   ],
   "source": [
    "import functools\n",
    "\n",
    "def catch_exceptions(print_failure=False):\n",
    "    def catch_exceptions_decorator(job_func):\n",
    "        @functools.wraps(job_func)\n",
    "        def wrapper(*args, **kwargs):\n",
    "            try:\n",
    "                return job_func(*args, **kwargs)\n",
    "            except:\n",
    "                import traceback\n",
    "                if print_failure:\n",
    "                    print('*' * 20 + ' Boom! ' + '*' * 20)\n",
    "                    print(traceback.format_exc())\n",
    "        return wrapper\n",
    "    return catch_exceptions_decorator\n",
    "\n",
    "@catch_exceptions(print_failure=True)\n",
    "def bad_task():\n",
    "    return 1 / 0\n",
    "\n",
    "\n",
    "bad_task()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Python Decorator 四種寫法範例 Code"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 沒有參數的 Function 版本\n",
    "\n",
    "> 使用 closure 去處理 function"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "-- apple before call\n",
      "hello first time.\n",
      "-- apple after call\n"
     ]
    }
   ],
   "source": [
    "def decorate_apple(orig_fun):\n",
    "    def wrapper(*args, **kargs):\n",
    "        print(\"-- apple before call\")\n",
    "        result = orig_fun(*args, **kargs)\n",
    "        print(\"-- apple after call\")\n",
    "        return result\n",
    "    return wrapper\n",
    "\n",
    "@decorate_apple\n",
    "def print_hello():\n",
    "    print(\"hello first time.\")\n",
    "\n",
    "\n",
    "print_hello()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 有參數的 Decorator Function"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "-- new banana before call\n",
      "hello 2nd time.\n",
      "-- new banana after call\n",
      "\n",
      "-- 50% rot guava before call\n",
      "hello 3th time.\n",
      "-- 50% rot guava after call\n"
     ]
    }
   ],
   "source": [
    "from functools import wraps\n",
    "\n",
    "def fruit(fruit, rot_level):\n",
    "    def fruit_decorator(orig_fun):\n",
    "        @wraps(orig_fun)\n",
    "        def wrapper(*args, **kargs):\n",
    "            print(\"-- {} {} before call\".format(rot_level, fruit))\n",
    "            result = orig_fun(*args, **kargs)\n",
    "            print(\"-- {} {} after call\".format(rot_level, fruit))\n",
    "            return result\n",
    "        return wrapper\n",
    "    return fruit_decorator\n",
    "\n",
    "@fruit('banana', 'new')\n",
    "def print_hello2():\n",
    "    print(\"hello 2nd time.\")\n",
    "\n",
    "@fruit('guava', '50% rot')\n",
    "def print_hello3():\n",
    "    print(\"hello 3th time.\")\n",
    "\n",
    "\n",
    "print_hello2()\n",
    "print('')\n",
    "print_hello3()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 沒有參數版本的 Decorator Class\n",
    "\n",
    "- Python 會將被修飾的 function 傳入 `__init__()`，這時就將它存入到 decorator 的 data member `self.f` 。另外再直接覆寫 `__call__()` ，然後在呼叫 `self.f` 的前後作額外的修飾動作。\n",
    "- 這裡的 `__call__()` 的 signature ，就是跟 被修飾的 function 一樣，所以就加上動態的 `*args`, 跟 `**kargs` 就可以完整 delegate了。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "-- apple before call\n",
      "hello 4th time.\n",
      "-- apple after call\n"
     ]
    }
   ],
   "source": [
    "class DecorateAppleClass(object):\n",
    "    def __init__(self, orig_fun):\n",
    "        self.orig_fun = orig_fun\n",
    "    \n",
    "    def __call__(self, *args, **kargs):\n",
    "        print(\"-- apple before call\")\n",
    "        result = self.orig_fun(*args, **kargs)\n",
    "        print(\"-- apple after call\")\n",
    "        return result\n",
    "\n",
    "\n",
    "@DecorateAppleClass\n",
    "def print_hello4():\n",
    "    print(\"hello 4th time.\")\n",
    "\n",
    "\n",
    "print_hello4()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 帶有參數版本的 Decorator Class\n",
    "\n",
    "- 當有參數版本的 decorator class 在修飾 function 時，function 就不會像沒有參數版本的 decorator class 一樣，會將 function 傳入 `__init__`，而是會傳入 decorator 的參數。function 本體則是會在 call 中被傳入，這是一個比較不一樣的地方。\n",
    "- 這裡的 `__call__()` 的 signature 則是 `def __call__(self, f)`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "-- 80% rot guava before call\n",
      "hello 5th times.\n",
      "-- 80% rot guava after call\n",
      "\n",
      "-- 30% rot banana before call\n",
      "hello 6th times.\n",
      "-- 30% rot banana after call\n"
     ]
    }
   ],
   "source": [
    "class DecorateFruitClass(object):\n",
    "    def __init__(self, fruit, rot_level):\n",
    "        self.fruit = fruit\n",
    "        self.rot_level = rot_level\n",
    "\n",
    "    def __call__(self, orig_fun):\n",
    "        def wrapper(*args, **kargs):\n",
    "            print(\"-- {} {} before call\".format(self.rot_level, self.fruit))\n",
    "            result = orig_fun(*args, **kargs)\n",
    "            print(\"-- {} {} after call\".format(self.rot_level, self.fruit))\n",
    "            return result\n",
    "        return wrapper\n",
    "\n",
    "\n",
    "@DecorateFruitClass('guava', '80% rot')\n",
    "def print_hello5():\n",
    "    print(\"hello 5th times.\")\n",
    "\n",
    "@DecorateFruitClass('banana', '30% rot')\n",
    "def print_hello6():\n",
    "    print(\"hello 6th times.\")\n",
    "\n",
    "\n",
    "print_hello5()\n",
    "print('')\n",
    "print_hello6()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "anaconda-cloud": {},
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
