{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 3.4 Advanced Topics on Functions\n",
    "\n",
    "## Topics\n",
    "- handle variable length arguments\n",
    "- lambda expressions\n",
    "- higher-order functions\n",
    "- nested functions\n",
    "- functions as returned values\n",
    "- currying\n",
    "- function decorators"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3.4.1 Variable length arguments\n",
    "- when not sure how many arguments will be passed to a function (e.g., print())\n",
    "- *args (non-keyworded variable length arguments)\n",
    "- *kwargs (keyworded variable length arguments)\n",
    "- e.g., built-in print function uses variable length arguments\n",
    "\n",
    "### print(*object, sep=' ', end='\\n', file=sys.stdout, flush=False)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "# variable length arguments demo\n",
    "def someFunction(a, b, c, *varargs, **kwargs):\n",
    "    print('a = ', a)\n",
    "    print('b = ', b)\n",
    "    print('c = ', c)\n",
    "    print('*args = ', varargs)\n",
    "    print('type of args = ', type(varargs))\n",
    "    print('**kwargs = ', kwargs)\n",
    "    print('type of kwargs = ', type(kwargs))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a =  1\n",
      "b =  Apple\n",
      "c =  4.5\n",
      "*args =  (5, [2.5, 'b'])\n",
      "type of args =  <class 'tuple'>\n",
      "**kwargs =  {'fname': 'Jake', 'num': 1}\n",
      "type of kwargs =  <class 'dict'>\n"
     ]
    }
   ],
   "source": [
    "# call someFunction with some arguments\n",
    "someFunction(1, 'Apple', 4.5, 5, [2.5, 'b'], fname='Jake', num=1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3.4.2 Lambda Functions/Expressions\n",
    "- anonymous functions (no name)\n",
    "- typically used in conjunction with higher order functions such as: map(), reduce(), filter()\n",
    "- Reference: http://www.secnetix.de/olli/Python/lambda_functions.hawk"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### lambda function properties and usage\n",
    "- single line simple functions\n",
    "- no explicit return keyword is used\n",
    "- always contains an expression that is implictly returned\n",
    "- can use a lambda definition anywere a function is expected without assigning to a variable\n",
    "- syntax: **lambda argument(s): expression**\n",
    "- see Ch08-2 Lists Advanced chapter for lambda applications on some higher order built-in functions"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### difference between lambda and regular function"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "# regular function\n",
    "def func(x): return x**2"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "16\n"
     ]
    }
   ],
   "source": [
    "print(func(4))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "g = lambda x: x**2 # no name, no parenthesis, and no return keyword\n",
    "# a function that takes x and returns x**2"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<function <lambda> at 0x7fc925e9dc80>\n"
     ]
    }
   ],
   "source": [
    "print(g)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "16"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "g(4)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3.4.3 Higher-order functions\n",
    "https://composingprograms.com/pages/16-higher-order-functions.html\n",
    "- functions that manipulate other functions are called higher order functions\n",
    "- functions take function(s) as argument(s)\n",
    "    - typically lambda expressions are passed\n",
    "- functions can return a function"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {},
   "outputs": [],
   "source": [
    "# computer summations of n natural numbers\n",
    "# func is a function applied to all the natural numbers between 1 and n inclusive\n",
    "def sum_naturals(func, n):\n",
    "    total, k = 0, 1\n",
    "    while k <= n:\n",
    "        total += func(k)\n",
    "        k += 1\n",
    "    return total\n",
    "    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sum of first 100 natural numbers = 5050\n"
     ]
    }
   ],
   "source": [
    "n = 100\n",
    "print(f'sum of first {n} natural numbers = {sum_naturals(lambda x: x, n)}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {},
   "outputs": [],
   "source": [
    "# of course you can pass regular function\n",
    "def even(n):\n",
    "    return n if n%2 == 0 else 0"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sum of even numbers from 1 to 100 = 2550\n"
     ]
    }
   ],
   "source": [
    "print(f'sum of even numbers from 1 to {n} = {sum_naturals(even, n)}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 49,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sum of odd numbers from 1 to 100 = 2500\n"
     ]
    }
   ],
   "source": [
    "# sum of odd numbers from 1 to 100\n",
    "print(f'sum of odd numbers from 1 to {n} = {sum_naturals(lambda x: x if x%2==1 else 0, n)}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3.4.4 Nested definitions\n",
    "- functions can be defined inside a function with local scope\n",
    "- locally defined functions also have access to the names in which they are defined\n",
    "    - this technique is called lexical scoping\n",
    "- helps keep the global frame clean and less cluter with functions that are only used inside some functions\n",
    "- let's redefine sum_natural function again with local functions\n",
    "\n",
    "### Visualize using [PythonTutor.com](http://pythontutor.com/visualize.html#code=def%20sum_naturals1%28n,%20number_type%3D%22all%22%29%3A%0A%20%20%20%20def%20even%28x%29%3A%0A%20%20%20%20%20%20%20%20return%20x%20if%20x%252%20%3D%3D%200%20else%200%0A%20%20%20%20%0A%20%20%20%20def%20odd%28x%29%3A%0A%20%20%20%20%20%20%20%20return%20x%20if%20x%252%20%3D%3D%201%20else%200%0A%20%20%20%20%0A%20%20%20%20def%20func%28x%29%3A%0A%20%20%20%20%20%20%20%20%23%20local%20function%20has%20access%20to%20global%20variables%20as%20well%20as%20parent%20frames%0A%20%20%20%20%20%20%20%20if%20number_type%20%3D%3D%20'even'%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20x%20if%20x%252%20%3D%3D%200%20else%200%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20x%20if%20x%252%20%3D%3D%201%20else%200%0A%20%20%20%20%20%20%20%20%20%20%20%20%0A%20%20%20%20total,%20k%20%3D%200,%201%0A%20%20%20%20while%20k%20%3C%3D%20n%3A%0A%20%20%20%20%20%20%20%20if%20number_type%20!%3D%20'all'%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20total%20%2B%3D%20func%28k%29%0A%20%20%20%20%20%20%20%20%23elif%20number_type%20%3D%3D%20'odd'%3A%0A%20%20%20%20%20%20%20%20%23%20%20%20%20total%20%2B%3D%20odd%28k%29%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20total%20%2B%3D%20k%0A%20%20%20%20%20%20%20%20k%20%2B%3D%201%0A%20%20%20%20return%20total%0A%20%20%20%20%0An%20%3D%2010%20%20%0Aprint%28f'sum%20of%20even%20numbers%20from%201%20to%20%7Bn%7D%20%3D%20%7Bsum_naturals1%28n,%20%22even%22%29%7D'%29&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 102,
   "metadata": {},
   "outputs": [],
   "source": [
    "# compute summations of n natural numbers\n",
    "# by default sum_natural1 finds sum of all the natural numbers between 1 and n inclusive\n",
    "def sum_naturals1(n, number_type=\"all\"):\n",
    "    def even(x):\n",
    "        return x if x%2 == 0 else 0\n",
    "    \n",
    "    def odd(x):\n",
    "        return x if x%2 == 1 else 0\n",
    "    \n",
    "    def func(x):\n",
    "        # local function has access to global variables as well as parent frames\n",
    "        if number_type == 'even':\n",
    "            return x if x%2 == 0 else 0\n",
    "        else:\n",
    "            return x if x%2 == 1 else 0\n",
    "            \n",
    "    total, k = 0, 1\n",
    "    while k <= n:\n",
    "        if number_type != 'all':\n",
    "            total += func(k)\n",
    "        #elif number_type == 'odd':\n",
    "        #    total += odd(k)\n",
    "        else:\n",
    "            total += k\n",
    "        k += 1\n",
    "    return total"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 45,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sum of first 100 natural numbers = 5050\n"
     ]
    }
   ],
   "source": [
    "n = 100\n",
    "print(f'sum of first {n} natural numbers = {sum_naturals1(n)}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sum of even numbers from 1 to 100 = 2550\n"
     ]
    }
   ],
   "source": [
    "print(f'sum of even numbers from 1 to {n} = {sum_naturals1(n, \"even\")}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 48,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sum of odd numbers from 1 to 100 = 2500\n"
     ]
    }
   ],
   "source": [
    "# sum of odd numbers from 1 to 100\n",
    "print(f'sum of odd numbers from 1 to {n} = {sum_naturals1(n, \"odd\")}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3.4.5 Functions as returned values\n",
    "- functions can return functions\n",
    "- locally defined functions maintain their parent environment when they are returned"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 52,
   "metadata": {},
   "outputs": [],
   "source": [
    "def number_type(ntype='all'):\n",
    "    def even(x):\n",
    "        return x if x%2 == 0 else 0\n",
    "    \n",
    "    def odd(x):\n",
    "        return x if x%2 == 1 else 0\n",
    "    \n",
    "    def _(x): # function to return x as it is; any()\n",
    "        return x\n",
    "    \n",
    "    if ntype == 'all':\n",
    "        return _\n",
    "    elif ntype == 'even':\n",
    "        return even\n",
    "    else:\n",
    "        return odd"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 55,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sum of first 100 natural numbers = 5050\n"
     ]
    }
   ],
   "source": [
    "n = 100\n",
    "print(f'sum of first {n} natural numbers = {sum_naturals(number_type(\"all\"), n)}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 58,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sum of even numbers from 1 to 100 = 2550\n"
     ]
    }
   ],
   "source": [
    "print(f'sum of even numbers from 1 to {n} = {sum_naturals(number_type(\"even\"), n)}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 59,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sum of odd numbers from 1 to 100 = 2500\n"
     ]
    }
   ],
   "source": [
    "# sum of odd numbers from 1 to 100\n",
    "print(f'sum of odd numbers from 1 to {n} = {sum_naturals(number_type(\"odd\"), n)}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3.4.6 Currying\n",
    "- functions that take multiple arguments can be converted into a chain of functions that each take a single argument using higher-order function\n",
    "- e.g., given a function **f(x, y)**, we can define a function **g(x)(y)** equivalent to **f(x, y)**\n",
    "- **g** is a higher-order function that takes in a single argument **x** and returns another function that takes in a single argument **y**\n",
    "    - this transformation is called **currying**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 86,
   "metadata": {},
   "outputs": [],
   "source": [
    "def curried_pow(x):\n",
    "    def g(y):\n",
    "        return pow(x, y)\n",
    "    return g"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 87,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "8"
      ]
     },
     "execution_count": 87,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# same as 2**3\n",
    "curried_pow(2)(3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 88,
   "metadata": {},
   "outputs": [],
   "source": [
    "# let's create a list of integers and map each to a different value\n",
    "nums = list(range(1, 11))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 89,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"
      ]
     },
     "execution_count": 89,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "nums"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 90,
   "metadata": {},
   "outputs": [],
   "source": [
    "def my_map(alist, func):\n",
    "    for i in range(len(alist)):\n",
    "        alist[i] = func(alist[i])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 91,
   "metadata": {},
   "outputs": [],
   "source": [
    "my_map(nums, curried_pow(2))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 92,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]"
      ]
     },
     "execution_count": 92,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "nums"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3.4.7 Function Decorators\n",
    "- https://realpython.com/primer-on-python-decorators/\n",
    "- decorators are higher order functions\n",
    "- decorators take another function and extends the behavior of the latter function without explictly modifying it\n",
    "- if the func being decorated takes arguments, provide arguments to wrapper\n",
    "- if the func being decorated returns a value call it with return statement\n",
    "- many frameworks such as Flask, Django provide lots of decorators\n",
    "    - e.g. @login_required; @app.route(\"/route_name\"), etc."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 118,
   "metadata": {},
   "outputs": [],
   "source": [
    "# a simple decorator example\n",
    "# my_decorator decorates func\n",
    "def my_decorator(func):\n",
    "    def wrapper():\n",
    "        print(\"Before the function is called...\")\n",
    "        # call the original function\n",
    "        func()\n",
    "        print(\"After the function is called.\")\n",
    "    return wrapper\n",
    "\n",
    "def say_hello():\n",
    "    print(\"Hello there!\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 119,
   "metadata": {},
   "outputs": [],
   "source": [
    "# say_hello is decorated now, without modifying the original function\n",
    "# just the behavior is modified by added extra print() before and after say_hello\n",
    "say_hello = my_decorator(say_hello)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 120,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Before the function is called...\n",
      "Hello there!\n",
      "After the function is called.\n"
     ]
    }
   ],
   "source": [
    "say_hello()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 100,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Python provides better syntax!\n",
    "# use @decorting_function\n",
    "@my_decorator\n",
    "def say_hi():\n",
    "    print(\"Hi there!\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 101,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Before the function is called...\n",
      "Hi there!\n",
      "After the function is called.\n"
     ]
    }
   ],
   "source": [
    "say_hi()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 107,
   "metadata": {},
   "outputs": [],
   "source": [
    "# a simple count down function\n",
    "def countDown(from_number):\n",
    "    if from_number <= 0:\n",
    "        print('Blast off!')\n",
    "    else:\n",
    "        print(from_number)\n",
    "        countDown(from_number-1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 112,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "10\n",
      "9\n",
      "8\n",
      "7\n",
      "6\n",
      "5\n",
      "4\n",
      "3\n",
      "2\n",
      "1\n",
      "Blast off!\n"
     ]
    }
   ],
   "source": [
    "# Doesn't slow down the countdown!\n",
    "countDown(10)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 109,
   "metadata": {},
   "outputs": [],
   "source": [
    "# let's write a slow_down wrapper\n",
    "import time\n",
    "\n",
    "def slow_down(func):\n",
    "    \"\"\"Sleep 1 second before calling the function\"\"\"\n",
    "    def wrapper_slow_down(*args, **kwargs):\n",
    "        time.sleep(1) # sleep for a second\n",
    "        return func(*args, **kwargs) # call and return the result from the func\n",
    "    return wrapper_slow_down"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 113,
   "metadata": {},
   "outputs": [],
   "source": [
    "countDownSlow = slow_down(countDown)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 114,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "10\n",
      "9\n",
      "8\n",
      "7\n",
      "6\n",
      "5\n",
      "4\n",
      "3\n",
      "2\n",
      "1\n",
      "Blast off!\n"
     ]
    }
   ],
   "source": [
    "countDownSlow(10)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "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.7.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}