{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "3e9aa8bc-bdc1-4f86-a44b-c68b8072c4ef",
   "metadata": {},
   "source": [
    "## Decorators\n",
    "\n",
    "A common pattern in functional programs, are functions that are built to \"wrap\" other functions.\n",
    "\n",
    "Functions are in fact mutable."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "d8cecafd-c460-446b-8f8f-7a35a9f8c6e2",
   "metadata": {},
   "outputs": [],
   "source": [
    "def print_before_and_after(func):\n",
    "    def newfunc(*args, **kwargs):\n",
    "        print(\"BEFORE\", func)\n",
    "        func(*args, **kwargs)\n",
    "        print(\"AFTER\", func)\n",
    "    return newfunc"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b3dd55e9-33cb-41c9-840a-7fd2f137f2f0",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "f327b88e-d034-4bd3-9e63-9196ccbb3d84",
   "metadata": {},
   "outputs": [],
   "source": [
    "def inner(a, b, c):\n",
    "    print(\"inner function\", a, b, c)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "359d03fc-1d69-4574-a06c-45b55905cecb",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "BEFORE <function inner at 0x103591240>\n",
      "inner function 1 2 3\n",
      "AFTER <function inner at 0x103591240>\n"
     ]
    }
   ],
   "source": [
    "wrapped_inner = print_before_and_after(inner)\n",
    "\n",
    "wrapped_inner(1, 2, 3)\n",
    "#print(x)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "916c438d-f0f3-4b78-9846-f9bf2aab7dcc",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "BEFORE <function inner at 0x103591240>\n",
      "inner function 1 2 3\n",
      "AFTER <function inner at 0x103591240>\n"
     ]
    }
   ],
   "source": [
    "# often we want to just replace the function altogether\n",
    "# with the modified version\n",
    "inner = print_before_and_after(inner)\n",
    "inner(1, 2, 3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "508bbf73-1f9f-41dd-9665-549472056211",
   "metadata": {},
   "outputs": [],
   "source": [
    "# another way of writing this is to use decorator syntax\n",
    "@print_before_and_after\n",
    "@print_before_and_after\n",
    "def add_nums(a, b, c):\n",
    "    print(f\"{a} + {b} + {c} =\", a + b + c)\n",
    "# add_nums = print_before_and_after(add_nums)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "86ee63a9-ac5d-44f5-a06a-a89527358141",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "BEFORE <function print_before_and_after.<locals>.newfunc at 0x103591ea0>\n",
      "BEFORE <function add_nums at 0x1035923b0>\n",
      "1 + 2 + 3 = 6\n",
      "AFTER <function add_nums at 0x1035923b0>\n",
      "AFTER <function print_before_and_after.<locals>.newfunc at 0x103591ea0>\n"
     ]
    }
   ],
   "source": [
    "add_nums(1, 2, 3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "1e9d6428-8f80-4707-b508-c15878459d1f",
   "metadata": {},
   "outputs": [],
   "source": [
    "def cache(func):\n",
    "    inner_cache = {}\n",
    "    \n",
    "    def newfunc(*args, **kwargs):\n",
    "        if args not in inner_cache:\n",
    "            # will not work correctly if there are kwargs\n",
    "            inner_cache[args] = func(*args, **kwargs)\n",
    "        return inner_cache[args]\n",
    "    \n",
    "    return newfunc"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c50faf15-f5dc-4a1b-a114-c606a0edd0be",
   "metadata": {},
   "outputs": [],
   "source": [
    "@cache\n",
    "def expensive_calculation(a, b, *, c=0):\n",
    "    print(f\"doing expensive calculation on {a} {b}...\")\n",
    "    return a ** b\n",
    "#expensive_calculation = cache(expensive_calculation)\n",
    "\n",
    "@cache\n",
    "def cheap_calculation(a, b):\n",
    "    print(f\"doing cheap calculation on {a} {b}...\")\n",
    "    return a + b"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dadaf583-82fd-4273-870e-7d68ec67c537",
   "metadata": {},
   "outputs": [],
   "source": [
    "expensive_calculation(4, 10)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6caa9b63-bc83-44c7-b416-1b5f9c708469",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "expensive_calculation(4, 10)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c6e9a399-9c52-4b38-8fe0-427e0843804a",
   "metadata": {},
   "outputs": [],
   "source": [
    "cheap_calculation(4, 10)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8ed9065a-4fb1-4486-ab02-b858d031eb93",
   "metadata": {},
   "outputs": [],
   "source": [
    "expensive_calculation(5, 6)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "975f7f9e-74c4-4dcf-b396-ad85556e930a",
   "metadata": {},
   "outputs": [],
   "source": [
    "cheap_calculation(4, 10)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d3d8c310-4fa6-4b5e-b6e7-e0512adfe8c0",
   "metadata": {},
   "outputs": [],
   "source": [
    "expensive_calculation(5, 6)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "id": "b6da10be-562c-4968-86ed-c473db63eccf",
   "metadata": {},
   "outputs": [
    {
     "ename": "TypeError",
     "evalue": "repeatN() missing 1 required positional argument: 'n'",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mTypeError\u001b[0m                                 Traceback (most recent call last)",
      "Cell \u001b[0;32mIn[31], line 8\u001b[0m\n\u001b[1;32m      4\u001b[0m             func(a, b, c)\n\u001b[1;32m      5\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m newfunc\n\u001b[1;32m      7\u001b[0m \u001b[38;5;129;43m@repeatN\u001b[39;49m\n\u001b[0;32m----> 8\u001b[0m \u001b[38;5;28;43;01mdef\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;21;43mprint_sum\u001b[39;49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m)\u001b[49m\u001b[43m:\u001b[49m\n\u001b[1;32m      9\u001b[0m \u001b[43m    \u001b[49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[43m(\u001b[49m\u001b[38;5;28;43msum\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43margs\u001b[49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n",
      "\u001b[0;31mTypeError\u001b[0m: repeatN() missing 1 required positional argument: 'n'"
     ]
    }
   ],
   "source": [
    "def repeat5(func):                     # the decorator   \n",
    "    def newfunc(a, b, c):      # the inner function\n",
    "        return func(a, b, c, c, b, a)\n",
    "    return newfunc\n",
    "\n",
    "@repeatN\n",
    "def print_sum(*args):\n",
    "    print(sum(args))\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "7f43e771-92d9-48d3-96ed-b95b1c94e942",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "6\n",
      "6\n",
      "6\n",
      "6\n",
      "6\n"
     ]
    }
   ],
   "source": [
    "print_sum(1, 2, 3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "4327b95d-b9ab-49e3-a530-b34e0688cff6",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sdrawkcab\n",
      "sdrawkcab\n",
      "sdrawkcab\n",
      "sdrawkcab\n",
      "sdrawkcab\n",
      "sdrawkcab\n",
      "sdrawkcab\n",
      "sdrawkcab\n",
      "sdrawkcab\n",
      "sdrawkcab\n"
     ]
    }
   ],
   "source": [
    "# to make a decorator that takes additional arguments\n",
    "# you need to write a decorator factory function that returns decorators\n",
    "\n",
    "total = 123\n",
    "\n",
    "def repeat(start, end):  # factory: takes integer, returns decorator\n",
    "    def repeat_decorator(func):                  # decorator: takes function, returns function\n",
    "        def newfunc(*args, **kwargs):            # inner function: takes ?, returns ?\n",
    "            for i in range(total):\n",
    "                func(*args, **kwargs)\n",
    "        return newfunc\n",
    "    return repeat_decorator\n",
    "\n",
    "@repeat(10)\n",
    "def print_backwards(s):\n",
    "    print(s[::-1])\n",
    "\n",
    "print_backwards(\"backwards\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8b4219ac-7326-47e1-b3ad-68941349db1d",
   "metadata": {},
   "outputs": [],
   "source": [
    "repeat_10 = repeat(10)\n",
    "print(repeat_10)\n",
    "print_backwards = repeat_10(print_backwards)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a1d27a5c-ce00-4023-8efd-1900b6f876e5",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Example: writing our own `partial`\n",
    "# https://docs.python.org/3/library/functools.html#functools.partial"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "id": "fb9e74b4-a0a2-4463-add7-45b04430edcd",
   "metadata": {},
   "outputs": [],
   "source": [
    "import functools\n",
    "print_hello_names = functools.partial(print, \"Hello\", sep=\", \")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "id": "e64ec996-4085-4c03-a771-6c1073b04990",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Hello, Scott, Paul, Lauren\n"
     ]
    }
   ],
   "source": [
    "print_hello_names(\"Scott\", \"Paul\", \"Lauren\")\n",
    "# same as print(\"Hello\", \"Scott\", \"Paul\", \"Lauren\", sep=\", \")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "eb0d1ad3-e636-4430-9139-265740adc8a1",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "('Hello',)"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "print_hello_names.args"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "9e67124b-9014-49d9-af86-54b8a617521f",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'sep': ', '}"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "print_hello_names.keywords"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "f4939684-6c92-4c07-8d88-f7c4349d0f02",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<function print>"
      ]
     },
     "execution_count": 19,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "print_hello_names.func"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "id": "1ed27205-7691-46ee-bdc6-f75dae7d53da",
   "metadata": {},
   "outputs": [],
   "source": [
    "# since functions are objects, we can attach arbitrary values to them\n",
    "def wrapper(func):\n",
    "    def newfunc(*args, **kwargs):\n",
    "        return func(*args, **kwargs)\n",
    "    # we can do whatever we like after defining newfunc, but before returning it\n",
    "    newfunc.xyz = \"hello\"*2\n",
    "    return newfunc"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "id": "8e02bb23-7278-4799-85d6-9004af65565b",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 35,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# property is assigned to all wrapped functions\n",
    "@wrapper\n",
    "def our_function():\n",
    "    print(\"inside our function\")\n",
    "\n",
    "our_function.xyz"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "id": "93b1ad42-b4a2-4947-a4bb-19c818968e2c",
   "metadata": {},
   "outputs": [],
   "source": [
    "def our_partial(func, /, *args, **keywords):\n",
    "    def newfunc(*fargs, **fkeywords):\n",
    "        newkeywords = {**keywords, **fkeywords}\n",
    "        return func(*args, *fargs, **newkeywords)\n",
    "    # assign these properties from within the closure\n",
    "    newfunc.func = func\n",
    "    newfunc.args = args\n",
    "    newfunc.keywords = keywords\n",
    "    return newfunc"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "id": "092b1a1d-4300-4895-aa06-4d874bc61159",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Scott, Paul, Lauren, Hello!"
     ]
    }
   ],
   "source": [
    "print_hello_names2 = our_partial(print, \"Hello\", sep=\", \")\n",
    "print_hello_names2(\"Scott\", \"Paul\", \"Lauren\", end=\"!\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "id": "a0bd355b-63d5-41e2-8238-2e3dff22cb45",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Hello?Scott?Paul?Lauren!"
     ]
    }
   ],
   "source": [
    "#print_hello_names2 = our_partial(print, \"Hello\", sep=\", \")\n",
    "print_hello_names2(\"Scott\", \"Paul\", \"Lauren\", end=\"!\", sep=\"?\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "id": "c48a5dfb-0444-4f5e-a4bc-39f0770ff5a3",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "('Hello',)"
      ]
     },
     "execution_count": 25,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "print_hello_names2.args"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "f5d68812-918b-42dd-b23f-925a692668aa",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'sep': ', '}"
      ]
     },
     "execution_count": 26,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "print_hello_names2.keywords"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "id": "6b41b480-43e1-49b2-ade7-44d8aa484a09",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<function print>"
      ]
     },
     "execution_count": 27,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "print_hello_names2.func"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ca9e90da-65e6-4e92-91ae-a97340527b38",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "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.10.15"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
