{ "cells": [ { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "# Using functional programming in Python like a boss: Generators, Iterators and Decorators\n", "\n", "" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "# Features of functions\n", "1. First-class functions are objects and thus:\n", " * Can be assigned to variables\n", " * Can be stored in data structures\n", " * Can be used as parameters\n", " * Can be used as a return value\n", "2. Higher order functions:\n", " * Accept a function as an argument and/or return a function as a value.\n", " * Create composite functions from simpler functions.\n", " * Modify the behavior of existing functions.\n", "3. Pure functions:\n", " * Do not depend on hidden state, or equivalently depend only on their input.\n", " * Evaluation of the function does not cause side effects\n" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "# A pure function\n", "def my_min(x, y):\n", " if x < y:\n", " return x\n", " else:\n", " return y\n", "\n", "\n", "# An impure function\n", "# 1) Depends on global variable, 2) Changes its input\n", "exponent = 2\n", "\n", "def my_powers(L):\n", " for i in range(len(L)):\n", " L[i] = L[i]**exponent\n", " return L" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true, "slideshow": { "slide_type": "slide" } }, "source": [ "# What can act as a function in Python?\n", "1. A function object, created with the `def` statement.\n", "2. A `lambda` anonymous function, restricted to an expression in single line.\n", "3. An instance of a class implementing the `__call__` function." ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "True\n" ] } ], "source": [ "def min_def(x, y):\n", " return x if x < y else y\n", "\n", "min_lambda = lambda x, y: x if x < y else y\n", "\n", "class MinClass:\n", " def __call__(self, x, y):\n", " return x if x < y else y\n", "\n", "min_class = MinClass()\n", "\n", "print(min_def(2,3) == min_lambda(2, 3) == min_class(2,3))" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "# Common gotchas 1: Mutable Default Arguments\n", "\n", "* Python's default arguments are evaluated once when the function is **defined** and **not each time the function is called** (like say, in Ruby).\n", "* If you use a mutable default argument and mutate it, you will and have mutated that object for all future calls to the function as well.\n", "* Sometimes you can specifically \"exploit\" (read: use as intended) this behavior to maintain state between calls of a function. This is often done when writing a caching function." ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "my_list: [12]\n", "my_other_list: [12, 42]\n", "my_list2: [12]\n", "my_other_list2: [42]\n" ] } ], "source": [ "def append_to(element, to=[]):\n", " to.append(element)\n", " return to\n", "\n", "my_list = append_to(12)\n", "print(\"my_list:\", my_list)\n", "my_other_list = append_to(42)\n", "print(\"my_other_list:\", my_other_list)\n", "\n", "def append_to2(element, to=None):\n", " if to is None:\n", " to = []\n", " to.append(element)\n", " return to\n", "\n", "my_list2 = append_to2(12)\n", "print(\"my_list2:\", my_list2)\n", "my_other_list2 = append_to2(42)\n", "print(\"my_other_list2:\", my_other_list2)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "# Common gotchas 2: Late Binding Closures\n", "\n", "1. A closure occurs when a function has access to a local variable from an enclosing scope that has finished its execution.\n", "2. Python's closures are *late binding*.\n", "3. Values of variables used in closures are looked up at the time the inner function is **called** and **not when it is defined**." ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "8\n", "8\n", "8\n", "8\n", "8\n" ] } ], "source": [ "def create_multipliers():\n", " multipliers = []\n", "\n", " for i in range(5):\n", " def multiplier(x):\n", " return i * x\n", " multipliers.append(multiplier)\n", "\n", " return multipliers\n", "\n", "for multiplier in create_multipliers():\n", " print(multiplier(2))" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "# Higher order functions and decorators\n", "\n", "* Python functions are objects.\n", "* Can be defined in functions.\n", "* Can be assigned to variables.\n", "* Can be used as function parameters or returned from functions.\n", "* Decorators are syntactic sugar for higher order functions." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "hello world\n", "hello world\n" ] } ], "source": [ "# Higher order functions\n", "\n", "def makebold(fn):\n", " def wrapped():\n", " return \"\" + fn() + \"\"\n", " return wrapped\n", "\n", "def hello():\n", " return \"hello world\"\n", "\n", "print(hello())\n", "hello = makebold(hello)\n", "print(hello())" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Hello. args: ('world', 'pythess'), kwargs: {'where': 'soho'}\n" ] } ], "source": [ "# Decorated function with *args and **kewargs\n", "\n", "def makebold(fn):\n", " def wrapped(*args, **kwargs):\n", " return \"\" + fn(*args, **kwargs) + \"\"\n", " return wrapped\n", "\n", "@makebold # hello = makebold(hello)\n", "def hello(*args, **kwargs):\n", " return \"Hello. args: {}, kwargs: {}\".format(args, kwargs)\n", "\n", "print(hello('world', 'pythess', where='soho'))" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Hello. args: ('world', 'pythess'), kwargs: {'where': 'soho'}\n" ] } ], "source": [ "# Decorators can be combined\n", "\n", "def makeitalic(fn):\n", " def wrapped(*args, **kwargs):\n", " return \"\" + fn(*args, **kwargs) + \"\"\n", " return wrapped\n", "\n", "def makebold(fn):\n", " def wrapped(*args, **kwargs):\n", " return \"\" + fn(*args, **kwargs) + \"\"\n", " return wrapped\n", "\n", "@makeitalic\n", "@makebold # hello = makeitalic(makebold(hello))\n", "def hello(*args, **kwargs):\n", " return \"Hello. args: {}, kwargs: {}\".format(args, kwargs)\n", "\n", "print(hello('world', 'pythess', where='soho'))" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Hello. args: ('world', 'pythess'), kwargs: {'where': 'soho'}\n" ] } ], "source": [ "# Decorators can be instances of callable classes\n", "\n", "class BoldMaker:\n", " def __init__(self, fn):\n", " self.fn = fn\n", " def __call__(self, *args, **kwargs):\n", " return \"\" + self.fn(*args, **kwargs) + \"\"\n", "\n", "@BoldMaker # hello = BoldMaker(hello)\n", "def hello(*args, **kwargs):\n", " return \"Hello. args: {}, kwargs: {}\".format(args, kwargs)\n", "\n", "# hello.__call__(*args, **kwargs)\n", "print(hello('world', 'pythess', where='soho'))" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "
hello world
\n", "hello world
\n" ] } ], "source": [ "# Decorators can take arguments\n", "\n", "def enclose_in_tags(opening_tag, closing_tag): # returns a decorator\n", " def make_with_tags(fn): # returns a decorated function\n", " def wrapped(): # the function to be decorated (modified)\n", " return opening_tag + fn() + closing_tag\n", " return wrapped\n", " return make_with_tags\n", "\n", "# decorator function make_with_tags with the arguments in closure\n", "heading_decorator = enclose_in_tags('', '
')\n", "\n", "def hello():\n", " return \"hello world\"\n", "\n", "h1_hello = heading_decorator(hello)\n", "p_hello = paragraph_decorator(hello)\n", "h1_p_hello = heading_decorator(paragraph_decorator(hello))\n", "print(h1_hello())\n", "print(p_hello())\n", "print(h1_p_hello())" ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "hello world
\n", "hello world
\n" ] } ], "source": [ "# Decorators with arguments combined\n", "\n", "def enclose_in_tags(opening_tag, closing_tag):\n", " def make_with_tags(fn):\n", " def wrapped():\n", " return opening_tag + fn() + closing_tag\n", " return wrapped\n", " return make_with_tags\n", "\n", "# hello = enclose_in_tags('', '
')(hello)\n", "@enclose_in_tags('', '
')\n", "def hello():\n", " return \"hello world\"\n", "\n", "print(hello())\n", "\n", "# hello = enclose_in_tags('', '
')(hello))\n", "@enclose_in_tags('', '
')\n", "def hello():\n", " return \"hello world\"\n", "\n", "print(hello())" ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "hello world
\n" ] } ], "source": [ "# Decorators with arguments as instances of callable classes\n", "\n", "class TagEncloser:\n", " def __init__(self, opening_tag, closing_tag):\n", " self.opening_tag = opening_tag\n", " self.closing_tag = closing_tag\n", " def __call__(self, fn):\n", " def wrapped():\n", " return self.opening_tag + fn() + self.closing_tag\n", " return wrapped\n", "\n", "tag_h1 = TagEncloser('', '
')\n", "\n", "@tag_h1\n", "@tag_p\n", "def hello(): # hello = tag_h1(tag_p(hello))\n", " return \"hello world\"\n", "\n", "print(hello())" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "# Iterables and Iterators\n", "\n", "1. **Iteration** is a general term for taking each item of something, one after another. Any time you use a loop, explicit or implicit, to go over a group of items, that is iteration.\n", "2. An **iterable** is an anything that can be looped over. It either:\n", " - Has an `__iter__` method which returns an **iterator** for that object when you call `iter()` on it, or implicitly in a for loop.\n", " - Defines a `__getitem__` method that can take sequential indexes starting from zero (and raises an `IndexError` when the indexes are no longer valid).\n", "3. An **iterator** is:\n", " * A stateful helper object which defines a `__next__` method and will produce the next value when you call ``next()`` on it. If there are no further items, it raises the `StopIteration` exception.\n", " * An object that is *self-iterable* (meaning that it has an `__iter__` method that returns `self`).\n", " \n", "Therefore: An *iterable* is an object from which we can get an *iterator*. An *iterator* is **always** an *iterable*. An *iterable* **is not always** an *iterator* but will always **return** an *iterator*." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "" ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1\n", "2\n", "1\n", "