{ "cells": [ { "cell_type": "markdown", "id": "62204c5b", "metadata": {}, "source": [ "\n", "\n", "
\n", " \n", " \"QuantEcon\"\n", " \n", "
" ] }, { "cell_type": "markdown", "id": "be60f287", "metadata": {}, "source": [ "# More Language Features" ] }, { "cell_type": "markdown", "id": "bcf8eb68", "metadata": {}, "source": [ "## Contents\n", "\n", "- [More Language Features](#More-Language-Features) \n", " - [Overview](#Overview) \n", " - [Iterables and Iterators](#Iterables-and-Iterators) \n", " - [`*` and `**` Operators](#*-and-**-Operators) \n", " - [Decorators and Descriptors](#Decorators-and-Descriptors) \n", " - [Generators](#Generators) \n", " - [Exercises](#Exercises) " ] }, { "cell_type": "markdown", "id": "a9249cb1", "metadata": {}, "source": [ "## Overview\n", "\n", "With this last lecture, our advice is to **skip it on first pass**, unless you have a burning desire to read it.\n", "\n", "It’s here\n", "\n", "1. as a reference, so we can link back to it when required, and \n", "1. for those who have worked through a number of applications, and now want to learn more about the Python language \n", "\n", "\n", "A variety of topics are treated in the lecture, including iterators, decorators and descriptors, and generators." ] }, { "cell_type": "markdown", "id": "30d0e0b2", "metadata": {}, "source": [ "## Iterables and Iterators\n", "\n", "\n", "\n", "We’ve [already said something](https://python-programming.quantecon.org/python_essentials.html#iterating-version-1) about iterating in Python.\n", "\n", "Now let’s look more closely at how it all works, focusing in Python’s implementation of the `for` loop.\n", "\n", "\n", "" ] }, { "cell_type": "markdown", "id": "90275ff3", "metadata": {}, "source": [ "### Iterators\n", "\n", "\n", "\n", "Iterators are a uniform interface to stepping through elements in a collection.\n", "\n", "Here we’ll talk about using iterators—later we’ll learn how to build our own.\n", "\n", "Formally, an *iterator* is an object with a `__next__` method.\n", "\n", "For example, file objects are iterators .\n", "\n", "To see this, let’s have another look at the [US cities data](https://python-programming.quantecon.org/python_essentials.html#us-cities-data),\n", "which is written to the present working directory in the following cell" ] }, { "cell_type": "code", "execution_count": null, "id": "85e1c6db", "metadata": { "hide-output": false }, "outputs": [], "source": [ "%%file us_cities.txt\n", "new york: 8244910\n", "los angeles: 3819702\n", "chicago: 2707120\n", "houston: 2145146\n", "philadelphia: 1536471\n", "phoenix: 1469471\n", "san antonio: 1359758\n", "san diego: 1326179\n", "dallas: 1223229" ] }, { "cell_type": "code", "execution_count": null, "id": "34ebabb4", "metadata": { "hide-output": false }, "outputs": [], "source": [ "f = open('us_cities.txt')\n", "f.__next__()" ] }, { "cell_type": "code", "execution_count": null, "id": "1b56ccde", "metadata": { "hide-output": false }, "outputs": [], "source": [ "f.__next__()" ] }, { "cell_type": "markdown", "id": "b294fd92", "metadata": {}, "source": [ "We see that file objects do indeed have a `__next__` method, and that calling this method returns the next line in the file.\n", "\n", "The next method can also be accessed via the builtin function `next()`,\n", "which directly calls this method" ] }, { "cell_type": "code", "execution_count": null, "id": "159bd9d2", "metadata": { "hide-output": false }, "outputs": [], "source": [ "next(f)" ] }, { "cell_type": "markdown", "id": "bba30eac", "metadata": {}, "source": [ "The objects returned by `enumerate()` are also iterators" ] }, { "cell_type": "code", "execution_count": null, "id": "ace1abd0", "metadata": { "hide-output": false }, "outputs": [], "source": [ "e = enumerate(['foo', 'bar'])\n", "next(e)" ] }, { "cell_type": "code", "execution_count": null, "id": "bb531ece", "metadata": { "hide-output": false }, "outputs": [], "source": [ "next(e)" ] }, { "cell_type": "markdown", "id": "46570d49", "metadata": {}, "source": [ "as are the reader objects from the `csv` module .\n", "\n", "Let’s create a small csv file that contains data from the NIKKEI index" ] }, { "cell_type": "code", "execution_count": null, "id": "9d58785a", "metadata": { "hide-output": false }, "outputs": [], "source": [ "%%file test_table.csv\n", "Date,Open,High,Low,Close,Volume,Adj Close\n", "2009-05-21,9280.35,9286.35,9189.92,9264.15,133200,9264.15\n", "2009-05-20,9372.72,9399.40,9311.61,9344.64,143200,9344.64\n", "2009-05-19,9172.56,9326.75,9166.97,9290.29,167000,9290.29\n", "2009-05-18,9167.05,9167.82,8997.74,9038.69,147800,9038.69\n", "2009-05-15,9150.21,9272.08,9140.90,9265.02,172000,9265.02\n", "2009-05-14,9212.30,9223.77,9052.41,9093.73,169400,9093.73\n", "2009-05-13,9305.79,9379.47,9278.89,9340.49,176000,9340.49\n", "2009-05-12,9358.25,9389.61,9298.61,9298.61,188400,9298.61\n", "2009-05-11,9460.72,9503.91,9342.75,9451.98,230800,9451.98\n", "2009-05-08,9351.40,9464.43,9349.57,9432.83,220200,9432.83" ] }, { "cell_type": "code", "execution_count": null, "id": "f5b064e3", "metadata": { "hide-output": false }, "outputs": [], "source": [ "from csv import reader\n", "\n", "f = open('test_table.csv', 'r')\n", "nikkei_data = reader(f)\n", "next(nikkei_data)" ] }, { "cell_type": "code", "execution_count": null, "id": "3fa4ee4c", "metadata": { "hide-output": false }, "outputs": [], "source": [ "next(nikkei_data)" ] }, { "cell_type": "markdown", "id": "ef69b789", "metadata": {}, "source": [ "### Iterators in For Loops\n", "\n", "\n", "\n", "All iterators can be placed to the right of the `in` keyword in `for` loop statements.\n", "\n", "In fact this is how the `for` loop works: If we write" ] }, { "cell_type": "markdown", "id": "2d7b4f7f", "metadata": { "hide-output": false }, "source": [ "```python3\n", "for x in iterator:\n", " \n", "```\n" ] }, { "cell_type": "markdown", "id": "ff450ee0", "metadata": {}, "source": [ "then the interpreter\n", "\n", "- calls `iterator.___next___()` and binds `x` to the result \n", "- executes the code block \n", "- repeats until a `StopIteration` error occurs \n", "\n", "\n", "So now you know how this magical looking syntax works" ] }, { "cell_type": "markdown", "id": "71b4fd19", "metadata": { "hide-output": false }, "source": [ "```python3\n", "f = open('somefile.txt', 'r')\n", "for line in f:\n", " # do something\n", "```\n" ] }, { "cell_type": "markdown", "id": "953fded2", "metadata": {}, "source": [ "The interpreter just keeps\n", "\n", "1. calling `f.__next__()` and binding `line` to the result \n", "1. executing the body of the loop \n", "\n", "\n", "This continues until a `StopIteration` error occurs." ] }, { "cell_type": "markdown", "id": "7840e482", "metadata": {}, "source": [ "### Iterables\n", "\n", "\n", "\n", "You already know that we can put a Python list to the right of `in` in a `for` loop" ] }, { "cell_type": "code", "execution_count": null, "id": "b60c9bb8", "metadata": { "hide-output": false }, "outputs": [], "source": [ "for i in ['spam', 'eggs']:\n", " print(i)" ] }, { "cell_type": "markdown", "id": "20ee077f", "metadata": {}, "source": [ "So does that mean that a list is an iterator?\n", "\n", "The answer is no" ] }, { "cell_type": "code", "execution_count": null, "id": "e5f60bb7", "metadata": { "hide-output": false }, "outputs": [], "source": [ "x = ['foo', 'bar']\n", "type(x)" ] }, { "cell_type": "code", "execution_count": null, "id": "8d12cbec", "metadata": { "hide-output": false }, "outputs": [], "source": [ "next(x)" ] }, { "cell_type": "markdown", "id": "39a87450", "metadata": {}, "source": [ "So why can we iterate over a list in a `for` loop?\n", "\n", "The reason is that a list is *iterable* (as opposed to an iterator).\n", "\n", "Formally, an object is iterable if it can be converted to an iterator using the built-in function `iter()`.\n", "\n", "Lists are one such object" ] }, { "cell_type": "code", "execution_count": null, "id": "5dc101fd", "metadata": { "hide-output": false }, "outputs": [], "source": [ "x = ['foo', 'bar']\n", "type(x)" ] }, { "cell_type": "code", "execution_count": null, "id": "dbf33e30", "metadata": { "hide-output": false }, "outputs": [], "source": [ "y = iter(x)\n", "type(y)" ] }, { "cell_type": "code", "execution_count": null, "id": "14e4e95f", "metadata": { "hide-output": false }, "outputs": [], "source": [ "next(y)" ] }, { "cell_type": "code", "execution_count": null, "id": "9f2ee7f9", "metadata": { "hide-output": false }, "outputs": [], "source": [ "next(y)" ] }, { "cell_type": "code", "execution_count": null, "id": "961c1fb4", "metadata": { "hide-output": false }, "outputs": [], "source": [ "next(y)" ] }, { "cell_type": "markdown", "id": "f2eece84", "metadata": {}, "source": [ "Many other objects are iterable, such as dictionaries and tuples.\n", "\n", "Of course, not all objects are iterable" ] }, { "cell_type": "code", "execution_count": null, "id": "c8816fa2", "metadata": { "hide-output": false }, "outputs": [], "source": [ "iter(42)" ] }, { "cell_type": "markdown", "id": "9aba6b24", "metadata": {}, "source": [ "To conclude our discussion of `for` loops\n", "\n", "- `for` loops work on either iterators or iterables. \n", "- In the second case, the iterable is converted into an iterator before the loop starts. " ] }, { "cell_type": "markdown", "id": "774dac08", "metadata": {}, "source": [ "### Iterators and built-ins\n", "\n", "\n", "\n", "Some built-in functions that act on sequences also work with iterables\n", "\n", "- `max()`, `min()`, `sum()`, `all()`, `any()` \n", "\n", "\n", "For example" ] }, { "cell_type": "code", "execution_count": null, "id": "22e57ee9", "metadata": { "hide-output": false }, "outputs": [], "source": [ "x = [10, -10]\n", "max(x)" ] }, { "cell_type": "code", "execution_count": null, "id": "b012187c", "metadata": { "hide-output": false }, "outputs": [], "source": [ "y = iter(x)\n", "type(y)" ] }, { "cell_type": "code", "execution_count": null, "id": "8b02a0eb", "metadata": { "hide-output": false }, "outputs": [], "source": [ "max(y)" ] }, { "cell_type": "markdown", "id": "6a7edc83", "metadata": {}, "source": [ "One thing to remember about iterators is that they are depleted by use" ] }, { "cell_type": "code", "execution_count": null, "id": "1b43f212", "metadata": { "hide-output": false }, "outputs": [], "source": [ "x = [10, -10]\n", "y = iter(x)\n", "max(y)" ] }, { "cell_type": "code", "execution_count": null, "id": "09bf798d", "metadata": { "hide-output": false }, "outputs": [], "source": [ "max(y)" ] }, { "cell_type": "markdown", "id": "08320385", "metadata": {}, "source": [ "## `*` and `**` Operators\n", "\n", "`*` and `**` are convenient and widely used tools to unpack lists and tuples and to allow users to define functions that take arbitrarily many arguments as input.\n", "\n", "In this section, we will explore how to use them and distinguish their use cases." ] }, { "cell_type": "markdown", "id": "4766cfed", "metadata": {}, "source": [ "### Unpacking Arguments\n", "\n", "When we operate on a list of parameters, we often need to extract the content of the list as individual arguments instead of a collection when passing them into functions.\n", "\n", "Luckily, the `*` operator can help us to unpack lists and tuples into [*positional arguments*](https://python-programming.quantecon.org/functions.html#pos-args) in function calls.\n", "\n", "To make things concrete, consider the following examples:\n", "\n", "Without `*`, the `print` function prints a list" ] }, { "cell_type": "code", "execution_count": null, "id": "0145c74b", "metadata": { "hide-output": false }, "outputs": [], "source": [ "l1 = ['a', 'b', 'c']\n", "\n", "print(l1)" ] }, { "cell_type": "markdown", "id": "051c49ad", "metadata": {}, "source": [ "While the `print` function prints individual elements since `*` unpacks the list into individual arguments" ] }, { "cell_type": "code", "execution_count": null, "id": "f9250757", "metadata": { "hide-output": false }, "outputs": [], "source": [ "print(*l1)" ] }, { "cell_type": "markdown", "id": "3b234f82", "metadata": {}, "source": [ "Unpacking the list using `*` into positional arguments is equivalent to defining them individually when calling the function" ] }, { "cell_type": "code", "execution_count": null, "id": "7ac64911", "metadata": { "hide-output": false }, "outputs": [], "source": [ "print('a', 'b', 'c')" ] }, { "cell_type": "markdown", "id": "f515f671", "metadata": {}, "source": [ "However, `*` operator is more convenient if we want to reuse them again" ] }, { "cell_type": "code", "execution_count": null, "id": "77aac77b", "metadata": { "hide-output": false }, "outputs": [], "source": [ "l1.append('d')\n", "\n", "print(*l1)" ] }, { "cell_type": "markdown", "id": "bee7ed6d", "metadata": {}, "source": [ "Similarly, `**` is used to unpack arguments.\n", "\n", "The difference is that `**` unpacks *dictionaries* into *keyword arguments*.\n", "\n", "`**` is often used when there are many keyword arguments we want to reuse.\n", "\n", "For example, assuming we want to draw multiple graphs using the same graphical settings,\n", "it may involve repetitively setting many graphical parameters, usually defined using keyword arguments.\n", "\n", "In this case, we can use a dictionary to store these parameters and use `**` to unpack dictionaries into keyword arguments when they are needed.\n", "\n", "Let’s walk through a simple example together and distinguish the use of `*` and `**`" ] }, { "cell_type": "code", "execution_count": null, "id": "876dbb06", "metadata": { "hide-output": false }, "outputs": [], "source": [ "import numpy as np\n", "import matplotlib.pyplot as plt\n", "\n", "# Set up the frame and subplots\n", "fig, ax = plt.subplots(2, 1)\n", "plt.subplots_adjust(hspace=0.7)\n", "\n", "# Create a function that generates synthetic data\n", "def generate_data(β_0, β_1, σ=30, n=100):\n", " x_values = np.arange(0, n, 1)\n", " y_values = β_0 + β_1 * x_values + np.random.normal(size=n, scale=σ)\n", " return x_values, y_values\n", "\n", "# Store the keyword arguments for lines and legends in a dictionary\n", "line_kargs = {'lw': 1.5, 'alpha': 0.7}\n", "legend_kargs = {'bbox_to_anchor': (0., 1.02, 1., .102), \n", " 'loc': 3, \n", " 'ncol': 4,\n", " 'mode': 'expand', \n", " 'prop': {'size': 7}}\n", "\n", "β_0s = [10, 20, 30]\n", "β_1s = [1, 2, 3]\n", "\n", "# Use a for loop to plot lines\n", "def generate_plots(β_0s, β_1s, idx, line_kargs, legend_kargs):\n", " label_list = []\n", " for βs in zip(β_0s, β_1s):\n", " \n", " # Use * to unpack tuple βs and the tuple output from the generate_data function\n", " # Use ** to unpack the dictionary of keyword arguments for lines\n", " ax[idx].plot(*generate_data(*βs), **line_kargs)\n", "\n", " label_list.append(f'$β_0 = {βs[0]}$ | $β_1 = {βs[1]}$')\n", "\n", " # Use ** to unpack the dictionary of keyword arguments for legends\n", " ax[idx].legend(label_list, **legend_kargs)\n", "\n", "generate_plots(β_0s, β_1s, 0, line_kargs, legend_kargs)\n", "\n", "# We can easily reuse and update our parameters\n", "β_1s.append(-2)\n", "β_0s.append(40)\n", "line_kargs['lw'] = 2\n", "line_kargs['alpha'] = 0.4\n", "\n", "generate_plots(β_0s, β_1s, 1, line_kargs, legend_kargs)\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "83d14fd8", "metadata": {}, "source": [ "In this example, `*` unpacked the zipped parameters `βs` and the output of `generate_data` function stored in tuples,\n", "while `**` unpacked graphical parameters stored in `legend_kargs` and `line_kargs`.\n", "\n", "To summarize, when `*list`/`*tuple` and `**dictionary` are passed into *function calls*, they are unpacked into individual arguments instead of a collection.\n", "\n", "The difference is that `*` will unpack lists and tuples into *positional arguments*, while `**` will unpack dictionaries into *keyword arguments*." ] }, { "cell_type": "markdown", "id": "f3c9e48a", "metadata": {}, "source": [ "### Arbitrary Arguments\n", "\n", "When we *define* functions, it is sometimes desirable to allow users to put as many arguments as they want into a function.\n", "\n", "You might have noticed that the `ax.plot()` function could handle arbitrarily many arguments.\n", "\n", "If we look at the [documentation](https://github.com/matplotlib/matplotlib/blob/v3.6.2/lib/matplotlib/axes/_axes.py#L1417-L1669) of the function, we can see the function is defined as" ] }, { "cell_type": "code", "execution_count": null, "id": "2b26a650", "metadata": { "hide-output": false }, "outputs": [], "source": [ "Axes.plot(*args, scalex=True, scaley=True, data=None, **kwargs)" ] }, { "cell_type": "markdown", "id": "0d8a75c4", "metadata": {}, "source": [ "We found `*` and `**` operators again in the context of the *function definition*.\n", "\n", "In fact, `*args` and `**kargs` are ubiquitous in the scientific libraries in Python to reduce redundancy and allow flexible inputs.\n", "\n", "`*args` enables the function to handle *positional arguments* with a variable size" ] }, { "cell_type": "code", "execution_count": null, "id": "2871063e", "metadata": { "hide-output": false }, "outputs": [], "source": [ "l1 = ['a', 'b', 'c']\n", "l2 = ['b', 'c', 'd']\n", "\n", "def arb(*ls):\n", " print(ls)\n", "\n", "arb(l1, l2)" ] }, { "cell_type": "markdown", "id": "1bd8d011", "metadata": {}, "source": [ "The inputs are passed into the function and stored in a tuple.\n", "\n", "Let’s try more inputs" ] }, { "cell_type": "code", "execution_count": null, "id": "2a19d5e2", "metadata": { "hide-output": false }, "outputs": [], "source": [ "l3 = ['z', 'x', 'b']\n", "arb(l1, l2, l3)" ] }, { "cell_type": "markdown", "id": "ab081411", "metadata": {}, "source": [ "Similarly, Python allows us to use `**kargs` to pass arbitrarily many *keyword arguments* into functions" ] }, { "cell_type": "code", "execution_count": null, "id": "70acbaeb", "metadata": { "hide-output": false }, "outputs": [], "source": [ "def arb(**ls):\n", " print(ls)\n", "\n", "# Note that these are keyword arguments\n", "arb(l1=l1, l2=l2)" ] }, { "cell_type": "markdown", "id": "67ea8c5d", "metadata": {}, "source": [ "We can see Python uses a dictionary to store these keyword arguments.\n", "\n", "Let’s try more inputs" ] }, { "cell_type": "code", "execution_count": null, "id": "32d21329", "metadata": { "hide-output": false }, "outputs": [], "source": [ "arb(l1=l1, l2=l2, l3=l3)" ] }, { "cell_type": "markdown", "id": "a641fce4", "metadata": {}, "source": [ "Overall, `*args` and `**kargs` are used when *defining a function*; they enable the function to take input with an arbitrary size.\n", "\n", "The difference is that functions with `*args` will be able to take *positional arguments* with an arbitrary size, while `**kargs` will allow functions to take arbitrarily many *keyword arguments*." ] }, { "cell_type": "markdown", "id": "781f3c24", "metadata": {}, "source": [ "## Decorators and Descriptors\n", "\n", "\n", "\n", "Let’s look at some special syntax elements that are routinely used by Python developers.\n", "\n", "You might not need the following concepts immediately, but you will see them\n", "in other people’s code.\n", "\n", "Hence you need to understand them at some stage of your Python education." ] }, { "cell_type": "markdown", "id": "45486100", "metadata": {}, "source": [ "### Decorators\n", "\n", "\n", "\n", "Decorators are a bit of syntactic sugar that, while easily avoided, have turned out to be popular.\n", "\n", "It’s very easy to say what decorators do.\n", "\n", "On the other hand it takes a bit of effort to explain *why* you might use them." ] }, { "cell_type": "markdown", "id": "5ac4ea5a", "metadata": {}, "source": [ "#### An Example\n", "\n", "Suppose we are working on a program that looks something like this" ] }, { "cell_type": "code", "execution_count": null, "id": "f8432881", "metadata": { "hide-output": false }, "outputs": [], "source": [ "import numpy as np\n", "\n", "def f(x):\n", " return np.log(np.log(x))\n", "\n", "def g(x):\n", " return np.sqrt(42 * x)\n", "\n", "# Program continues with various calculations using f and g" ] }, { "cell_type": "markdown", "id": "5c09ecf0", "metadata": {}, "source": [ "Now suppose there’s a problem: occasionally negative numbers get fed to `f` and `g` in the calculations that follow.\n", "\n", "If you try it, you’ll see that when these functions are called with negative numbers they return a NumPy object called `nan` .\n", "\n", "This stands for “not a number” (and indicates that you are trying to evaluate\n", "a mathematical function at a point where it is not defined).\n", "\n", "Perhaps this isn’t what we want, because it causes other problems that are hard to pick up later on.\n", "\n", "Suppose that instead we want the program to terminate whenever this happens, with a sensible error message.\n", "\n", "This change is easy enough to implement" ] }, { "cell_type": "code", "execution_count": null, "id": "cf630934", "metadata": { "hide-output": false }, "outputs": [], "source": [ "import numpy as np\n", "\n", "def f(x):\n", " assert x >= 0, \"Argument must be nonnegative\"\n", " return np.log(np.log(x))\n", "\n", "def g(x):\n", " assert x >= 0, \"Argument must be nonnegative\"\n", " return np.sqrt(42 * x)\n", "\n", "# Program continues with various calculations using f and g" ] }, { "cell_type": "markdown", "id": "d4e0042f", "metadata": {}, "source": [ "Notice however that there is some repetition here, in the form of two identical lines of code.\n", "\n", "Repetition makes our code longer and harder to maintain, and hence is\n", "something we try hard to avoid.\n", "\n", "Here it’s not a big deal, but imagine now that instead of just `f` and `g`, we have 20 such functions that we need to modify in exactly the same way.\n", "\n", "This means we need to repeat the test logic (i.e., the `assert` line testing nonnegativity) 20 times.\n", "\n", "The situation is still worse if the test logic is longer and more complicated.\n", "\n", "In this kind of scenario the following approach would be neater" ] }, { "cell_type": "code", "execution_count": null, "id": "9b2b5140", "metadata": { "hide-output": false }, "outputs": [], "source": [ "import numpy as np\n", "\n", "def check_nonneg(func):\n", " def safe_function(x):\n", " assert x >= 0, \"Argument must be nonnegative\"\n", " return func(x)\n", " return safe_function\n", "\n", "def f(x):\n", " return np.log(np.log(x))\n", "\n", "def g(x):\n", " return np.sqrt(42 * x)\n", "\n", "f = check_nonneg(f)\n", "g = check_nonneg(g)\n", "# Program continues with various calculations using f and g" ] }, { "cell_type": "markdown", "id": "6b84fc9b", "metadata": {}, "source": [ "This looks complicated so let’s work through it slowly.\n", "\n", "To unravel the logic, consider what happens when we say `f = check_nonneg(f)`.\n", "\n", "This calls the function `check_nonneg` with parameter `func` set equal to `f`.\n", "\n", "Now `check_nonneg` creates a new function called `safe_function` that\n", "verifies `x` as nonnegative and then calls `func` on it (which is the same as `f`).\n", "\n", "Finally, the global name `f` is then set equal to `safe_function`.\n", "\n", "Now the behavior of `f` is as we desire, and the same is true of `g`.\n", "\n", "At the same time, the test logic is written only once." ] }, { "cell_type": "markdown", "id": "b0721f09", "metadata": {}, "source": [ "#### Enter Decorators\n", "\n", "\n", "\n", "The last version of our code is still not ideal.\n", "\n", "For example, if someone is reading our code and wants to know how\n", "`f` works, they will be looking for the function definition, which is" ] }, { "cell_type": "code", "execution_count": null, "id": "cc22ac1a", "metadata": { "hide-output": false }, "outputs": [], "source": [ "def f(x):\n", " return np.log(np.log(x))" ] }, { "cell_type": "markdown", "id": "312eabd4", "metadata": {}, "source": [ "They may well miss the line `f = check_nonneg(f)`.\n", "\n", "For this and other reasons, decorators were introduced to Python.\n", "\n", "With decorators, we can replace the lines" ] }, { "cell_type": "code", "execution_count": null, "id": "541e8427", "metadata": { "hide-output": false }, "outputs": [], "source": [ "def f(x):\n", " return np.log(np.log(x))\n", "\n", "def g(x):\n", " return np.sqrt(42 * x)\n", "\n", "f = check_nonneg(f)\n", "g = check_nonneg(g)" ] }, { "cell_type": "markdown", "id": "9589001f", "metadata": {}, "source": [ "with" ] }, { "cell_type": "code", "execution_count": null, "id": "f877556a", "metadata": { "hide-output": false }, "outputs": [], "source": [ "@check_nonneg\n", "def f(x):\n", " return np.log(np.log(x))\n", "\n", "@check_nonneg\n", "def g(x):\n", " return np.sqrt(42 * x)" ] }, { "cell_type": "markdown", "id": "cb81e231", "metadata": {}, "source": [ "These two pieces of code do exactly the same thing.\n", "\n", "If they do the same thing, do we really need decorator syntax?\n", "\n", "Well, notice that the decorators sit right on top of the function definitions.\n", "\n", "Hence anyone looking at the definition of the function will see them and be\n", "aware that the function is modified.\n", "\n", "In the opinion of many people, this makes the decorator syntax a significant improvement to the language.\n", "\n", "\n", "" ] }, { "cell_type": "markdown", "id": "8cef9838", "metadata": {}, "source": [ "### Descriptors\n", "\n", "\n", "\n", "Descriptors solve a common problem regarding management of variables.\n", "\n", "To understand the issue, consider a `Car` class, that simulates a car.\n", "\n", "Suppose that this class defines the variables `miles` and `kms`, which give the distance traveled in miles\n", "and kilometers respectively.\n", "\n", "A highly simplified version of the class might look as follows" ] }, { "cell_type": "code", "execution_count": null, "id": "7d32e7bb", "metadata": { "hide-output": false }, "outputs": [], "source": [ "class Car:\n", "\n", " def __init__(self, miles=1000):\n", " self.miles = miles\n", " self.kms = miles * 1.61\n", "\n", " # Some other functionality, details omitted" ] }, { "cell_type": "markdown", "id": "71cc5b1a", "metadata": {}, "source": [ "One potential problem we might have here is that a user alters one of these\n", "variables but not the other" ] }, { "cell_type": "code", "execution_count": null, "id": "3fee3e6f", "metadata": { "hide-output": false }, "outputs": [], "source": [ "car = Car()\n", "car.miles" ] }, { "cell_type": "code", "execution_count": null, "id": "9caea3f7", "metadata": { "hide-output": false }, "outputs": [], "source": [ "car.kms" ] }, { "cell_type": "code", "execution_count": null, "id": "738e84be", "metadata": { "hide-output": false }, "outputs": [], "source": [ "car.miles = 6000\n", "car.kms" ] }, { "cell_type": "markdown", "id": "fef798e2", "metadata": {}, "source": [ "In the last two lines we see that `miles` and `kms` are out of sync.\n", "\n", "What we really want is some mechanism whereby each time a user sets one of these variables, *the other is automatically updated*." ] }, { "cell_type": "markdown", "id": "7b258102", "metadata": {}, "source": [ "#### A Solution\n", "\n", "In Python, this issue is solved using *descriptors*.\n", "\n", "A descriptor is just a Python object that implements certain methods.\n", "\n", "These methods are triggered when the object is accessed through dotted attribute notation.\n", "\n", "The best way to understand this is to see it in action.\n", "\n", "Consider this alternative version of the `Car` class" ] }, { "cell_type": "code", "execution_count": null, "id": "db8249da", "metadata": { "hide-output": false }, "outputs": [], "source": [ "class Car:\n", "\n", " def __init__(self, miles=1000):\n", " self._miles = miles\n", " self._kms = miles * 1.61\n", "\n", " def set_miles(self, value):\n", " self._miles = value\n", " self._kms = value * 1.61\n", "\n", " def set_kms(self, value):\n", " self._kms = value\n", " self._miles = value / 1.61\n", "\n", " def get_miles(self):\n", " return self._miles\n", "\n", " def get_kms(self):\n", " return self._kms\n", "\n", " miles = property(get_miles, set_miles)\n", " kms = property(get_kms, set_kms)" ] }, { "cell_type": "markdown", "id": "344661bd", "metadata": {}, "source": [ "First let’s check that we get the desired behavior" ] }, { "cell_type": "code", "execution_count": null, "id": "feb9b6a6", "metadata": { "hide-output": false }, "outputs": [], "source": [ "car = Car()\n", "car.miles" ] }, { "cell_type": "code", "execution_count": null, "id": "df677698", "metadata": { "hide-output": false }, "outputs": [], "source": [ "car.miles = 6000\n", "car.kms" ] }, { "cell_type": "markdown", "id": "bb025bd0", "metadata": {}, "source": [ "Yep, that’s what we want — `car.kms` is automatically updated." ] }, { "cell_type": "markdown", "id": "4a2387cd", "metadata": {}, "source": [ "#### How it Works\n", "\n", "The names `_miles` and `_kms` are arbitrary names we are using to store the values of the variables.\n", "\n", "The objects `miles` and `kms` are *properties*, a common kind of descriptor.\n", "\n", "The methods `get_miles`, `set_miles`, `get_kms` and `set_kms` define\n", "what happens when you get (i.e. access) or set (bind) these variables\n", "\n", "- So-called “getter” and “setter” methods. \n", "\n", "\n", "The builtin Python function `property` takes getter and setter methods and creates a property.\n", "\n", "For example, after `car` is created as an instance of `Car`, the object `car.miles` is a property.\n", "\n", "Being a property, when we set its value via `car.miles = 6000` its setter\n", "method is triggered — in this case `set_miles`." ] }, { "cell_type": "markdown", "id": "d60b6a3b", "metadata": {}, "source": [ "#### Decorators and Properties\n", "\n", "\n", "\n", "These days its very common to see the `property` function used via a decorator.\n", "\n", "Here’s another version of our `Car` class that works as before but now uses\n", "decorators to set up the properties" ] }, { "cell_type": "code", "execution_count": null, "id": "5bfc140a", "metadata": { "hide-output": false }, "outputs": [], "source": [ "class Car:\n", "\n", " def __init__(self, miles=1000):\n", " self._miles = miles\n", " self._kms = miles * 1.61\n", "\n", " @property\n", " def miles(self):\n", " return self._miles\n", "\n", " @property\n", " def kms(self):\n", " return self._kms\n", "\n", " @miles.setter\n", " def miles(self, value):\n", " self._miles = value\n", " self._kms = value * 1.61\n", "\n", " @kms.setter\n", " def kms(self, value):\n", " self._kms = value\n", " self._miles = value / 1.61" ] }, { "cell_type": "markdown", "id": "361a3efa", "metadata": {}, "source": [ "We won’t go through all the details here.\n", "\n", "For further information you can refer to the [descriptor documentation](https://docs.python.org/3/howto/descriptor.html).\n", "\n", "\n", "" ] }, { "cell_type": "markdown", "id": "e2cb917d", "metadata": {}, "source": [ "## Generators\n", "\n", "\n", "\n", "A generator is a kind of iterator (i.e., it works with a `next` function).\n", "\n", "We will study two ways to build generators: generator expressions and generator functions." ] }, { "cell_type": "markdown", "id": "ef7c2aab", "metadata": {}, "source": [ "### Generator Expressions\n", "\n", "The easiest way to build generators is using *generator expressions*.\n", "\n", "Just like a list comprehension, but with round brackets.\n", "\n", "Here is the list comprehension:" ] }, { "cell_type": "code", "execution_count": null, "id": "202531d9", "metadata": { "hide-output": false }, "outputs": [], "source": [ "singular = ('dog', 'cat', 'bird')\n", "type(singular)" ] }, { "cell_type": "code", "execution_count": null, "id": "5f904dc0", "metadata": { "hide-output": false }, "outputs": [], "source": [ "plural = [string + 's' for string in singular]\n", "plural" ] }, { "cell_type": "code", "execution_count": null, "id": "d3584397", "metadata": { "hide-output": false }, "outputs": [], "source": [ "type(plural)" ] }, { "cell_type": "markdown", "id": "3efcbfdf", "metadata": {}, "source": [ "And here is the generator expression" ] }, { "cell_type": "code", "execution_count": null, "id": "f773fa84", "metadata": { "hide-output": false }, "outputs": [], "source": [ "singular = ('dog', 'cat', 'bird')\n", "plural = (string + 's' for string in singular)\n", "type(plural)" ] }, { "cell_type": "code", "execution_count": null, "id": "9fa39cfa", "metadata": { "hide-output": false }, "outputs": [], "source": [ "next(plural)" ] }, { "cell_type": "code", "execution_count": null, "id": "1c959038", "metadata": { "hide-output": false }, "outputs": [], "source": [ "next(plural)" ] }, { "cell_type": "code", "execution_count": null, "id": "c50b1c3e", "metadata": { "hide-output": false }, "outputs": [], "source": [ "next(plural)" ] }, { "cell_type": "markdown", "id": "d722b722", "metadata": {}, "source": [ "Since `sum()` can be called on iterators, we can do this" ] }, { "cell_type": "code", "execution_count": null, "id": "aa865488", "metadata": { "hide-output": false }, "outputs": [], "source": [ "sum((x * x for x in range(10)))" ] }, { "cell_type": "markdown", "id": "21bcb3b6", "metadata": {}, "source": [ "The function `sum()` calls `next()` to get the items, adds successive terms.\n", "\n", "In fact, we can omit the outer brackets in this case" ] }, { "cell_type": "code", "execution_count": null, "id": "72df6444", "metadata": { "hide-output": false }, "outputs": [], "source": [ "sum(x * x for x in range(10))" ] }, { "cell_type": "markdown", "id": "dae109ce", "metadata": {}, "source": [ "### Generator Functions\n", "\n", "\n", "\n", "The most flexible way to create generator objects is to use generator functions.\n", "\n", "Let’s look at some examples." ] }, { "cell_type": "markdown", "id": "c93f128e", "metadata": {}, "source": [ "#### Example 1\n", "\n", "Here’s a very simple example of a generator function" ] }, { "cell_type": "code", "execution_count": null, "id": "1f47575c", "metadata": { "hide-output": false }, "outputs": [], "source": [ "def f():\n", " yield 'start'\n", " yield 'middle'\n", " yield 'end'" ] }, { "cell_type": "markdown", "id": "bde70d16", "metadata": {}, "source": [ "It looks like a function, but uses a keyword `yield` that we haven’t met before.\n", "\n", "Let’s see how it works after running this code" ] }, { "cell_type": "code", "execution_count": null, "id": "8d9114c1", "metadata": { "hide-output": false }, "outputs": [], "source": [ "type(f)" ] }, { "cell_type": "code", "execution_count": null, "id": "631a7616", "metadata": { "hide-output": false }, "outputs": [], "source": [ "gen = f()\n", "gen" ] }, { "cell_type": "code", "execution_count": null, "id": "9b2299fa", "metadata": { "hide-output": false }, "outputs": [], "source": [ "next(gen)" ] }, { "cell_type": "code", "execution_count": null, "id": "b448e598", "metadata": { "hide-output": false }, "outputs": [], "source": [ "next(gen)" ] }, { "cell_type": "code", "execution_count": null, "id": "ccf7511d", "metadata": { "hide-output": false }, "outputs": [], "source": [ "next(gen)" ] }, { "cell_type": "code", "execution_count": null, "id": "227f9321", "metadata": { "hide-output": false }, "outputs": [], "source": [ "next(gen)" ] }, { "cell_type": "markdown", "id": "6ba1f072", "metadata": {}, "source": [ "The generator function `f()` is used to create generator objects (in this case `gen`).\n", "\n", "Generators are iterators, because they support a `next` method.\n", "\n", "The first call to `next(gen)`\n", "\n", "- Executes code in the body of `f()` until it meets a `yield` statement. \n", "- Returns that value to the caller of `next(gen)`. \n", "\n", "\n", "The second call to `next(gen)` starts executing *from the next line*" ] }, { "cell_type": "code", "execution_count": null, "id": "2d87f840", "metadata": { "hide-output": false }, "outputs": [], "source": [ "def f():\n", " yield 'start'\n", " yield 'middle' # This line!\n", " yield 'end'" ] }, { "cell_type": "markdown", "id": "02c434a4", "metadata": {}, "source": [ "and continues until the next `yield` statement.\n", "\n", "At that point it returns the value following `yield` to the caller of `next(gen)`, and so on.\n", "\n", "When the code block ends, the generator throws a `StopIteration` error." ] }, { "cell_type": "markdown", "id": "beb90805", "metadata": {}, "source": [ "#### Example 2\n", "\n", "Our next example receives an argument `x` from the caller" ] }, { "cell_type": "code", "execution_count": null, "id": "8bfcf1d1", "metadata": { "hide-output": false }, "outputs": [], "source": [ "def g(x):\n", " while x < 100:\n", " yield x\n", " x = x * x" ] }, { "cell_type": "markdown", "id": "acae6b66", "metadata": {}, "source": [ "Let’s see how it works" ] }, { "cell_type": "code", "execution_count": null, "id": "30fe533f", "metadata": { "hide-output": false }, "outputs": [], "source": [ "g" ] }, { "cell_type": "code", "execution_count": null, "id": "2c3c260a", "metadata": { "hide-output": false }, "outputs": [], "source": [ "gen = g(2)\n", "type(gen)" ] }, { "cell_type": "code", "execution_count": null, "id": "ea8c5edb", "metadata": { "hide-output": false }, "outputs": [], "source": [ "next(gen)" ] }, { "cell_type": "code", "execution_count": null, "id": "f8f3f8e0", "metadata": { "hide-output": false }, "outputs": [], "source": [ "next(gen)" ] }, { "cell_type": "code", "execution_count": null, "id": "7c0bb48e", "metadata": { "hide-output": false }, "outputs": [], "source": [ "next(gen)" ] }, { "cell_type": "code", "execution_count": null, "id": "42469272", "metadata": { "hide-output": false }, "outputs": [], "source": [ "next(gen)" ] }, { "cell_type": "markdown", "id": "a5e1b077", "metadata": {}, "source": [ "The call `gen = g(2)` binds `gen` to a generator.\n", "\n", "Inside the generator, the name `x` is bound to `2`.\n", "\n", "When we call `next(gen)`\n", "\n", "- The body of `g()` executes until the line `yield x`, and the value of `x` is returned. \n", "\n", "\n", "Note that value of `x` is retained inside the generator.\n", "\n", "When we call `next(gen)` again, execution continues *from where it left off*" ] }, { "cell_type": "code", "execution_count": null, "id": "271f0d60", "metadata": { "hide-output": false }, "outputs": [], "source": [ "def g(x):\n", " while x < 100:\n", " yield x\n", " x = x * x # execution continues from here" ] }, { "cell_type": "markdown", "id": "eb3a5b3d", "metadata": {}, "source": [ "When `x < 100` fails, the generator throws a `StopIteration` error.\n", "\n", "Incidentally, the loop inside the generator can be infinite" ] }, { "cell_type": "code", "execution_count": null, "id": "3192394f", "metadata": { "hide-output": false }, "outputs": [], "source": [ "def g(x):\n", " while 1:\n", " yield x\n", " x = x * x" ] }, { "cell_type": "markdown", "id": "50364861", "metadata": {}, "source": [ "### Advantages of Iterators\n", "\n", "What’s the advantage of using an iterator here?\n", "\n", "Suppose we want to sample a binomial(n,0.5).\n", "\n", "One way to do it is as follows" ] }, { "cell_type": "code", "execution_count": null, "id": "ff45c765", "metadata": { "hide-output": false }, "outputs": [], "source": [ "import random\n", "n = 10000000\n", "draws = [random.uniform(0, 1) < 0.5 for i in range(n)]\n", "sum(draws)" ] }, { "cell_type": "markdown", "id": "104b967b", "metadata": {}, "source": [ "But we are creating two huge lists here, `range(n)` and `draws`.\n", "\n", "This uses lots of memory and is very slow.\n", "\n", "If we make `n` even bigger then this happens" ] }, { "cell_type": "code", "execution_count": null, "id": "22d5c80b", "metadata": { "hide-output": false }, "outputs": [], "source": [ "n = 100000000\n", "draws = [random.uniform(0, 1) < 0.5 for i in range(n)]" ] }, { "cell_type": "markdown", "id": "db79e1c6", "metadata": {}, "source": [ "We can avoid these problems using iterators.\n", "\n", "Here is the generator function" ] }, { "cell_type": "code", "execution_count": null, "id": "d886efc3", "metadata": { "hide-output": false }, "outputs": [], "source": [ "def f(n):\n", " i = 1\n", " while i <= n:\n", " yield random.uniform(0, 1) < 0.5\n", " i += 1" ] }, { "cell_type": "markdown", "id": "36f95ec3", "metadata": {}, "source": [ "Now let’s do the sum" ] }, { "cell_type": "code", "execution_count": null, "id": "7cda99b9", "metadata": { "hide-output": false }, "outputs": [], "source": [ "n = 10000000\n", "draws = f(n)\n", "draws" ] }, { "cell_type": "code", "execution_count": null, "id": "09d26326", "metadata": { "hide-output": false }, "outputs": [], "source": [ "sum(draws)" ] }, { "cell_type": "markdown", "id": "a14a3094", "metadata": {}, "source": [ "In summary, iterables\n", "\n", "- avoid the need to create big lists/tuples, and \n", "- provide a uniform interface to iteration that can be used transparently in `for` loops " ] }, { "cell_type": "markdown", "id": "fd6904ed", "metadata": {}, "source": [ "## Exercises" ] }, { "cell_type": "markdown", "id": "86b10fac", "metadata": {}, "source": [ "## Exercise 20.1\n", "\n", "Complete the following code, and test it using [this csv file](https://raw.githubusercontent.com/QuantEcon/lecture-python-programming/master/source/_static/lecture_specific/python_advanced_features/test_table.csv), which we assume that you’ve put in your current working directory" ] }, { "cell_type": "markdown", "id": "71de7905", "metadata": { "hide-output": false }, "source": [ "```python3\n", "def column_iterator(target_file, column_number):\n", " \"\"\"A generator function for CSV files.\n", " When called with a file name target_file (string) and column number\n", " column_number (integer), the generator function returns a generator\n", " that steps through the elements of column column_number in file\n", " target_file.\n", " \"\"\"\n", " # put your code here\n", "\n", "dates = column_iterator('test_table.csv', 1)\n", "\n", "for date in dates:\n", " print(date)\n", "```\n" ] }, { "cell_type": "markdown", "id": "7ff96a7f", "metadata": {}, "source": [ "## Solution to[ Exercise 20.1](https://python-programming.quantecon.org/#paf_ex1)\n", "\n", "One solution is as follows" ] }, { "cell_type": "code", "execution_count": null, "id": "9d5b20ba", "metadata": { "hide-output": false }, "outputs": [], "source": [ "def column_iterator(target_file, column_number):\n", " \"\"\"A generator function for CSV files.\n", " When called with a file name target_file (string) and column number\n", " column_number (integer), the generator function returns a generator\n", " which steps through the elements of column column_number in file\n", " target_file.\n", " \"\"\"\n", " f = open(target_file, 'r')\n", " for line in f:\n", " yield line.split(',')[column_number - 1]\n", " f.close()\n", "\n", "dates = column_iterator('test_table.csv', 1)\n", "\n", "i = 1\n", "for date in dates:\n", " print(date)\n", " if i == 10:\n", " break\n", " i += 1" ] } ], "metadata": { "date": 1710455932.6708055, "filename": "python_advanced_features.md", "kernelspec": { "display_name": "Python", "language": "python3", "name": "python3" }, "title": "More Language Features" }, "nbformat": 4, "nbformat_minor": 5 }