<jupyter_start><jupyter_text>More Language Features  Contents- [More Language Features](More-Language-Features)    - [Overview](Overview)    - [Iterables and Iterators](Iterables-and-Iterators)    - [Names and Name Resolution](Names-and-Name-Resolution)    - [Handling Errors](Handling-Errors)    - [Decorators and Descriptors](Decorators-and-Descriptors)    - [Generators](Generators)    - [Recursive Function Calls](Recursive-Function-Calls)    - [Exercises](Exercises)    - [Solutions](Solutions)    OverviewWith this last lecture, our advice is to **skip it on first pass**, unless you have a burning desire to read it.It’s here1. as a reference, so we can link back to it when required, and  1. for those who have worked through a number of applications, and now want to learn more about the Python language  A variety of topics are treated in the lecture, including generators, exceptions and descriptors.  Iterables and IteratorsWe’ve [already said something](https://python-programming.quantecon.org/python_essentials.htmliterating-version-1) about iterating in Python.Now let’s look more closely at how it all works, focusing in Python’s implementation of the `for` loop.  IteratorsIterators are a uniform interface to stepping through elements in a collection.Here we’ll talk about using iterators—later we’ll learn how to build our own.Formally, an *iterator* is an object with a `__next__` method.For example, file objects are iterators .To see this, let’s have another look at the [US cities data](https://python-programming.quantecon.org/python_essentials.htmlus-cities-data),which is written to the present working directory in the following cell<jupyter_code>%%file us_cities.txt
new york: 8244910
los angeles: 3819702
chicago: 2707120
houston: 2145146
philadelphia: 1536471
phoenix: 1469471
san antonio: 1359758
san diego: 1326179
dallas: 1223229
f = open('us_cities.txt')
f.__next__()
f.__next__()<jupyter_output><empty_output><jupyter_text>We see that file objects do indeed have a `__next__` method, and that calling this method returns the next line in the file.The next method can also be accessed via the builtin function `next()`,which directly calls this method<jupyter_code>next(f)<jupyter_output><empty_output><jupyter_text>The objects returned by `enumerate()` are also iterators<jupyter_code>e = enumerate(['foo', 'bar'])
next(e)
next(e)<jupyter_output><empty_output><jupyter_text>as are the reader objects from the `csv` module .Let’s create a small csv file that contains data from the NIKKEI index<jupyter_code>%%file test_table.csv
Date,Open,High,Low,Close,Volume,Adj Close
2009-05-21,9280.35,9286.35,9189.92,9264.15,133200,9264.15
2009-05-20,9372.72,9399.40,9311.61,9344.64,143200,9344.64
2009-05-19,9172.56,9326.75,9166.97,9290.29,167000,9290.29
2009-05-18,9167.05,9167.82,8997.74,9038.69,147800,9038.69
2009-05-15,9150.21,9272.08,9140.90,9265.02,172000,9265.02
2009-05-14,9212.30,9223.77,9052.41,9093.73,169400,9093.73
2009-05-13,9305.79,9379.47,9278.89,9340.49,176000,9340.49
2009-05-12,9358.25,9389.61,9298.61,9298.61,188400,9298.61
2009-05-11,9460.72,9503.91,9342.75,9451.98,230800,9451.98
2009-05-08,9351.40,9464.43,9349.57,9432.83,220200,9432.83
from csv import reader

f = open('test_table.csv', 'r')
nikkei_data = reader(f)
next(nikkei_data)
next(nikkei_data)<jupyter_output><empty_output><jupyter_text>Iterators in For LoopsAll iterators can be placed to the right of the `in` keyword in `for` loop statements.In fact this is how the `for` loop works:  If we write ```python3for x in iterator:    ``` then the interpreter- calls `iterator.___next___()` and binds `x` to the result  - executes the code block  - repeats until a `StopIteration` error occurs  So now you know how this magical looking syntax works ```python3f = open('somefile.txt', 'r')for line in f:     do something``` The interpreter just keeps1. calling `f.__next__()` and binding `line` to the result  1. executing the body of the loop  This continues until a `StopIteration` error occurs.  IterablesYou already know that we can put a Python list to the right of `in` in a `for` loop<jupyter_code>for i in ['spam', 'eggs']:
    print(i)<jupyter_output><empty_output><jupyter_text>So does that mean that a list is an iterator?The answer is no<jupyter_code>x = ['foo', 'bar']
type(x)
next(x)<jupyter_output><empty_output><jupyter_text>So why can we iterate over a list in a `for` loop?The reason is that a list is *iterable* (as opposed to an iterator).Formally, an object is iterable if it can be converted to an iterator using the built-in function `iter()`.Lists are one such object<jupyter_code>x = ['foo', 'bar']
type(x)
y = iter(x)
type(y)
next(y)
next(y)
next(y)<jupyter_output><empty_output><jupyter_text>Many other objects are iterable, such as dictionaries and tuples.Of course, not all objects are iterable<jupyter_code>iter(42)<jupyter_output><empty_output><jupyter_text>To conclude our discussion of `for` loops- `for` loops work on either iterators or iterables.  - In the second case, the iterable is converted into an iterator before the loop starts.    Iterators and built-insSome built-in functions that act on sequences also work with iterables- `max()`, `min()`, `sum()`, `all()`, `any()`  For example<jupyter_code>x = [10, -10]
max(x)
y = iter(x)
type(y)
max(y)<jupyter_output><empty_output><jupyter_text>One thing to remember about iterators is that they are depleted by use<jupyter_code>x = [10, -10]
y = iter(x)
max(y)
max(y)<jupyter_output><empty_output><jupyter_text>Names and Name Resolution  Variable Names in PythonConsider the Python statement<jupyter_code>x = 42<jupyter_output><empty_output><jupyter_text>We now know that when this statement is executed, Python creates an object oftype `int` in your computer’s memory, containing- the value `42`  - some associated attributes  But what is `x` itself?In Python, `x` is called a *name*, and the statement `x = 42` *binds* the name `x` to the integer object we have just discussed.Under the hood, this process of binding names to objects is implemented as a dictionary—more about this in a moment.There is no problem binding two or more names to the one object, regardless of what that object is<jupyter_code>def f(string):      # Create a function called f
    print(string)   # that prints any string it's passed

g = f
id(g) == id(f)
g('test')<jupyter_output><empty_output><jupyter_text>In the first step, a function object is created, and the name `f` is bound to it.After binding the name `g` to the same object, we can use it anywhere we would use `f`.What happens when the number of names bound to an object goes to zero?Here’s an example of this situation, where the name `x` is first bound to one object and then rebound to another<jupyter_code>x = 'foo'
id(x)
x = 'bar'  # No names bound to the first object<jupyter_output><empty_output><jupyter_text>What happens here is that the first object is garbage collected.In other words, the memory slot that stores that object is deallocated, and returned to the operating system.  NamespacesRecall from the preceding discussion that the statement<jupyter_code>x = 42<jupyter_output><empty_output><jupyter_text>binds the name `x` to the integer object on the right-hand side.We also mentioned that this process of binding `x` to the correct object is implemented as a dictionary.This dictionary is called a *namespace*.**Definition:** A namespace is a symbol table that maps names to objects in memory.Python uses multiple namespaces, creating them on the fly as necessary .For example, every time we import a module, Python creates a namespace for that module.To see this in action, suppose we write a script `math2.py` with a single line<jupyter_code>%%file math2.py
pi = 'foobar'<jupyter_output><empty_output><jupyter_text>Now we start the Python interpreter and import it<jupyter_code>import math2<jupyter_output><empty_output><jupyter_text>Next let’s import the `math` module from the standard library<jupyter_code>import math<jupyter_output><empty_output><jupyter_text>Both of these modules have an attribute called `pi`<jupyter_code>math.pi
math2.pi<jupyter_output><empty_output><jupyter_text>These two different bindings of `pi` exist in different namespaces, each one implemented as a dictionary.We can look at the dictionary directly, using `module_name.__dict__`<jupyter_code>import math

math.__dict__.items()
import math2

math2.__dict__.items()<jupyter_output><empty_output><jupyter_text>As you know, we access elements of the namespace using the dotted attribute notation<jupyter_code>math.pi<jupyter_output><empty_output><jupyter_text>In fact this is entirely equivalent to `math.__dict__['pi']`<jupyter_code>math.__dict__['pi'] == math.pi<jupyter_output><empty_output><jupyter_text>Viewing NamespacesAs we saw above, the `math` namespace can be printed by typing `math.__dict__`.Another way to see its contents is to type `vars(math)`<jupyter_code>vars(math).items()<jupyter_output><empty_output><jupyter_text>If you just want to see the names, you can type<jupyter_code>dir(math)[0:10]<jupyter_output><empty_output><jupyter_text>Notice the special names `__doc__` and `__name__`.These are initialized in the namespace when any module is imported- `__doc__` is the doc string of the module  - `__name__` is the name of the module<jupyter_code>print(math.__doc__)
math.__name__<jupyter_output><empty_output><jupyter_text>Interactive SessionsIn Python, **all** code executed by the interpreter runs in some module.What about commands typed at the prompt?These are also regarded as being executed within a module — in this case, a module called `__main__`.To check this, we can look at the current module name via the value of `__name__` given at the prompt<jupyter_code>print(__name__)<jupyter_output><empty_output><jupyter_text>When we run a script using IPython’s `run` command, the contents of the file are executed as part of `__main__` too.To see this, let’s create a file `mod.py` that prints its own `__name__` attribute<jupyter_code>%%file mod.py
print(__name__)<jupyter_output><empty_output><jupyter_text>Now let’s look at two different ways of running it in IPython<jupyter_code>import mod  # Standard import
%run mod.py  # Run interactively<jupyter_output><empty_output><jupyter_text>In the second case, the code is executed as part of `__main__`, so `__name__` is equal to `__main__`.To see the contents of the namespace of `__main__` we use `vars()` rather than `vars(__main__)` .If you do this in IPython, you will see a whole lot of variables that IPythonneeds, and has initialized when you started up your session.If you prefer to see only the variables you have initialized, use `whos`<jupyter_code>x = 2
y = 3

import numpy as np

%whos<jupyter_output><empty_output><jupyter_text>The Global NamespacePython documentation often makes reference to the “global namespace”.The global namespace is *the namespace of the module currently being executed*.For example, suppose that we start the interpreter and begin making assignments .We are now working in the module `__main__`, and hence the namespace for `__main__` is the global namespace.Next, we import a module called `amodule` ```python3import amodule``` At this point, the interpreter creates a namespace for the module `amodule` and starts executing commands in the module.While this occurs, the namespace `amodule.__dict__` is the global namespace.Once execution of the module finishes, the interpreter returns to the module from where the import statement was made.In this case it’s `__main__`, so the namespace of `__main__` again becomes the global namespace.  Local NamespacesImportant fact: When we call a function, the interpreter creates a *local namespace* for that function, and registers the variables in that namespace.The reason for this will be explained in just a moment.Variables in the local namespace are called *local variables*.After the function returns, the namespace is deallocated and lost.While the function is executing, we can view the contents of the local namespace with `locals()`.For example, consider<jupyter_code>def f(x):
    a = 2
    print(locals())
    return a * x<jupyter_output><empty_output><jupyter_text>Now let’s call the function<jupyter_code>f(1)<jupyter_output><empty_output><jupyter_text>You can see the local namespace of `f` before it is destroyed.  The `__builtins__` NamespaceWe have been using various built-in functions, such as `max(), dir(), str(), list(), len(), range(), type()`, etc.How does access to these names work?- These definitions are stored in a module called `__builtin__`.  - They have there own namespace called `__builtins__`.<jupyter_code>dir()[0:10]
dir(__builtins__)[0:10]<jupyter_output><empty_output><jupyter_text>We can access elements of the namespace as follows<jupyter_code>__builtins__.max<jupyter_output><empty_output><jupyter_text>But `__builtins__` is special, because we can always access them directly as well<jupyter_code>max
__builtins__.max == max<jupyter_output><empty_output><jupyter_text>The next section explains how this works …  Name ResolutionNamespaces are great because they help us organize variable names.(Type `import this` at the prompt and look at the last item that’s printed)However, we do need to understand how the Python interpreter works with multiple namespaces .At any point of execution, there are in fact at least two namespaces that can be accessed directly.(“Accessed directly” means without using a dot, as in  `pi` rather than `math.pi`)These namespaces are- The global namespace (of the module being executed)  - The builtin namespace  If the interpreter is executing a function, then the directly accessible namespaces are- The local namespace of the function  - The global namespace (of the module being executed)  - The builtin namespace  Sometimes functions are defined within other functions, like so<jupyter_code>def f():
    a = 2
    def g():
        b = 4
        print(a * b)
    g()<jupyter_output><empty_output><jupyter_text>Here `f` is the *enclosing function* for `g`, and each function gets itsown namespaces.Now we can give the rule for how namespace resolution works:The order in which the interpreter searches for names is1. the local namespace (if it exists)  1. the hierarchy of enclosing namespaces (if they exist)  1. the global namespace  1. the builtin namespace  If the name is not in any of these namespaces, the interpreter raises a `NameError`.This is called the **LEGB rule** (local, enclosing, global, builtin).Here’s an example that helps to illustrate .Consider a script `test.py` that looks as follows<jupyter_code>%%file test.py
def g(x):
    a = 1
    x = x + a
    return x

a = 0
y = g(10)
print("a = ", a, "y = ", y)<jupyter_output><empty_output><jupyter_text>What happens when we run this script?<jupyter_code>%run test.py
x<jupyter_output><empty_output><jupyter_text>First,- The global namespace `{}` is created.  - The function object is created, and `g` is bound to it within the global namespace.  - The name `a` is bound to `0`, again in the global namespace.  Next `g` is called via `y = g(10)`, leading to the following sequence of actions- The local namespace for the function is created.  - Local names `x` and `a` are bound, so that the local namespace becomes `{'x': 10, 'a': 1}`.  - Statement `x = x + a` uses the local `a` and local `x` to compute `x + a`, and binds local name `x` to the result.  - This value is returned, and `y` is bound to it in the global namespace.  - Local `x` and `a` are discarded (and the local namespace is deallocated).  Note that the global `a` was not affected by the local `a`.  Mutable Versus Immutable ParametersThis is a good time to say a little more about mutable vs immutable objects.Consider the code segment<jupyter_code>def f(x):
    x = x + 1
    return x

x = 1
print(f(x), x)<jupyter_output><empty_output><jupyter_text>We now understand what will happen here: The code prints `2` as the value of `f(x)` and `1` as the value of `x`.First `f` and `x` are registered in the global namespace.The call `f(x)` creates a local namespace and adds `x` to it, bound to `1`.Next, this local `x` is rebound to the new integer object `2`, and this value is returned.None of this affects the global `x`.However, it’s a different story when we use a **mutable** data type such as a list<jupyter_code>def f(x):
    x[0] = x[0] + 1
    return x

x = [1]
print(f(x), x)<jupyter_output><empty_output><jupyter_text>This prints `[2]` as the value of `f(x)` and *same* for `x`.Here’s what happens- `f` is registered as a function in the global namespace  - `x` bound to `[1]` in the global namespace  - The call `f(x)`    - Creates a local namespace    - Adds `x` to local namespace, bound to `[1]`    - The list `[1]` is modified to `[2]`    - Returns the list `[2]`    - The local namespace is deallocated, and local `x` is lost  - Global `x` has been modified    Handling ErrorsSometimes it’s possible to anticipate errors as we’re writing code.For example, the unbiased sample variance of sample $ y_1, \ldots, y_n $is defined as$$s^2 := \frac{1}{n-1} \sum_{i=1}^n (y_i - \bar y)^2\qquad \bar y = \text{ sample mean}$$This can be calculated in NumPy using `np.var`.But if you were writing a function to handle such a calculation, you mightanticipate a divide-by-zero error when the sample size is one.One possible action is to do nothing — the program will just crash, and spit out an error message.But sometimes it’s worth writing your code in a way that anticipates and deals with runtime errors that you think might arise.Why?- Because the debugging information provided by the interpreter is often less useful than the information  on possible errors you have in your head when writing code.  - Because errors causing execution to stop are frustrating if you’re in the middle of a large computation.  - Because it’s reduces confidence in your code on the part of your users (if you are writing for others).    AssertionsA relatively easy way to handle checks is with the `assert` keyword.For example, pretend for a moment that the `np.var` function doesn’texist and we need to write our own<jupyter_code>def var(y):
    n = len(y)
    assert n > 1, 'Sample size must be greater than one.'
    return np.sum((y - y.mean())**2) / float(n-1)<jupyter_output><empty_output><jupyter_text>If we run this with an array of length one, the program will terminate andprint our error message<jupyter_code>var([1])<jupyter_output><empty_output><jupyter_text>The advantage is that we can- fail early, as soon as we know there will be a problem  - supply specific information on why a program is failing    Handling Errors During RuntimeThe approach used above is a bit limited, because it always leads totermination.Sometimes we can handle errors more gracefully, by treating special cases.Let’s look at how this is done.  ExceptionsHere’s an example of a common error type<jupyter_code>def f:<jupyter_output><empty_output><jupyter_text>Since illegal syntax cannot be executed, a syntax error terminates execution of the program.Here’s a different kind of error, unrelated to syntax<jupyter_code>1 / 0<jupyter_output><empty_output><jupyter_text>Here’s another<jupyter_code>x1 = y1<jupyter_output><empty_output><jupyter_text>And another<jupyter_code>'foo' + 6<jupyter_output><empty_output><jupyter_text>And another<jupyter_code>X = []
x = X[0]<jupyter_output><empty_output><jupyter_text>On each occasion, the interpreter informs us of the error type- `NameError`, `TypeError`, `IndexError`, `ZeroDivisionError`, etc.  In Python, these errors are called *exceptions*.  Catching ExceptionsWe can catch and deal with exceptions using `try` – `except` blocks.Here’s a simple example<jupyter_code>def f(x):
    try:
        return 1.0 / x
    except ZeroDivisionError:
        print('Error: division by zero.  Returned None')
    return None<jupyter_output><empty_output><jupyter_text>When we call `f` we get the following output<jupyter_code>f(2)
f(0)
f(0.0)<jupyter_output><empty_output><jupyter_text>The error is caught and execution of the program is not terminated.Note that other error types are not caught.If we are worried the user might pass in a string, we can catch that error too<jupyter_code>def f(x):
    try:
        return 1.0 / x
    except ZeroDivisionError:
        print('Error: Division by zero.  Returned None')
    except TypeError:
        print('Error: Unsupported operation.  Returned None')
    return None<jupyter_output><empty_output><jupyter_text>Here’s what happens<jupyter_code>f(2)
f(0)
f('foo')<jupyter_output><empty_output><jupyter_text>If we feel lazy we can catch these errors together<jupyter_code>def f(x):
    try:
        return 1.0 / x
    except (TypeError, ZeroDivisionError):
        print('Error: Unsupported operation.  Returned None')
    return None<jupyter_output><empty_output><jupyter_text>Here’s what happens<jupyter_code>f(2)
f(0)
f('foo')<jupyter_output><empty_output><jupyter_text>If we feel extra lazy we can catch all error types as follows<jupyter_code>def f(x):
    try:
        return 1.0 / x
    except:
        print('Error.  Returned None')
    return None<jupyter_output><empty_output><jupyter_text>In general it’s better to be specific.  Decorators and DescriptorsLet’s look at some special syntax elements that are routinely used by Python developers.You might not need the following concepts immediately, but you will see themin other people’s code.Hence you need to understand them at some stage of your Python education.  DecoratorsDecorators are a bit of syntactic sugar that, while easily avoided, have turned out to be popular.It’s very easy to say what decorators do.On the other hand it takes a bit of effort to explain *why* you might use them.  An ExampleSuppose we are working on a program that looks something like this<jupyter_code>import numpy as np

def f(x):
    return np.log(np.log(x))

def g(x):
    return np.sqrt(42 * x)

# Program continues with various calculations using f and g<jupyter_output><empty_output><jupyter_text>Now suppose there’s a problem: occasionally negative numbers get fed to `f` and `g` in the calculations that follow.If you try it, you’ll see that when these functions are called with negative numbers they return a NumPy object called `nan` .This stands for “not a number” (and indicates that you are trying to evaluatea mathematical function at a point where it is not defined).Perhaps this isn’t what we want, because it causes other problems that are hard to pick up later on.Suppose that instead we want the program to terminate whenever this happens, with a sensible error message.This change is easy enough to implement<jupyter_code>import numpy as np

def f(x):
    assert x >= 0, "Argument must be nonnegative"
    return np.log(np.log(x))

def g(x):
    assert x >= 0, "Argument must be nonnegative"
    return np.sqrt(42 * x)

# Program continues with various calculations using f and g<jupyter_output><empty_output><jupyter_text>Notice however that there is some repetition here, in the form of two identical lines of code.Repetition makes our code longer and harder to maintain, and hence issomething we try hard to avoid.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.This means we need to repeat the test logic (i.e., the `assert` line testing nonnegativity) 20 times.The situation is still worse if the test logic is longer and more complicated.In this kind of scenario the following approach would be neater<jupyter_code>import numpy as np

def check_nonneg(func):
    def safe_function(x):
        assert x >= 0, "Argument must be nonnegative"
        return func(x)
    return safe_function

def f(x):
    return np.log(np.log(x))

def g(x):
    return np.sqrt(42 * x)

f = check_nonneg(f)
g = check_nonneg(g)
# Program continues with various calculations using f and g<jupyter_output><empty_output><jupyter_text>This looks complicated so let’s work through it slowly.To unravel the logic, consider what happens when we say `f = check_nonneg(f)`.This calls the function `check_nonneg` with parameter `func` set equal to `f`.Now `check_nonneg` creates a new function called `safe_function` thatverifies `x` as nonnegative and then calls `func` on it (which is the same as `f`).Finally, the global name `f` is then set equal to `safe_function`.Now the behavior of `f` is as we desire, and the same is true of `g`.At the same time, the test logic is written only once.  Enter DecoratorsThe last version of our code is still not ideal.For example, if someone is reading our code and wants to know how`f` works, they will be looking for the function definition, which is<jupyter_code>def f(x):
    return np.log(np.log(x))<jupyter_output><empty_output><jupyter_text>They may well miss the line `f = check_nonneg(f)`.For this and other reasons, decorators were introduced to Python.With decorators, we can replace the lines<jupyter_code>def f(x):
    return np.log(np.log(x))

def g(x):
    return np.sqrt(42 * x)

f = check_nonneg(f)
g = check_nonneg(g)<jupyter_output><empty_output><jupyter_text>with<jupyter_code>@check_nonneg
def f(x):
    return np.log(np.log(x))

@check_nonneg
def g(x):
    return np.sqrt(42 * x)<jupyter_output><empty_output><jupyter_text>These two pieces of code do exactly the same thing.If they do the same thing, do we really need decorator syntax?Well, notice that the decorators sit right on top of the function definitions.Hence anyone looking at the definition of the function will see them and beaware that the function is modified.In the opinion of many people, this makes the decorator syntax a significant improvement to the language.  DescriptorsDescriptors solve a common problem regarding management of variables.To understand the issue, consider a `Car` class, that simulates a car.Suppose that this class defines the variables `miles` and `kms`, which give the distance traveled in milesand kilometers respectively.A highly simplified version of the class might look as follows<jupyter_code>class Car:

    def __init__(self, miles=1000):
        self.miles = miles
        self.kms = miles * 1.61

    # Some other functionality, details omitted<jupyter_output><empty_output><jupyter_text>One potential problem we might have here is that a user alters one of thesevariables but not the other<jupyter_code>car = Car()
car.miles
car.kms
car.miles = 6000
car.kms<jupyter_output><empty_output><jupyter_text>In the last two lines we see that `miles` and `kms` are out of sync.What we really want is some mechanism whereby each time a user sets one of these variables, *the other is automatically updated*.  A SolutionIn Python, this issue is solved using *descriptors*.A descriptor is just a Python object that implements certain methods.These methods are triggered when the object is accessed through dotted attribute notation.The best way to understand this is to see it in action.Consider this alternative version of the `Car` class<jupyter_code>class Car:

    def __init__(self, miles=1000):
        self._miles = miles
        self._kms = miles * 1.61

    def set_miles(self, value):
        self._miles = value
        self._kms = value * 1.61

    def set_kms(self, value):
        self._kms = value
        self._miles = value / 1.61

    def get_miles(self):
        return self._miles

    def get_kms(self):
        return self._kms

    miles = property(get_miles, set_miles)
    kms = property(get_kms, set_kms)<jupyter_output><empty_output><jupyter_text>First let’s check that we get the desired behavior<jupyter_code>car = Car()
car.miles
car.miles = 6000
car.kms<jupyter_output><empty_output><jupyter_text>Yep, that’s what we want — `car.kms` is automatically updated.  How it WorksThe names `_miles` and `_kms` are arbitrary names we are using to store the values of the variables.The objects `miles` and `kms` are *properties*, a common kind of descriptor.The methods `get_miles`, `set_miles`, `get_kms` and `set_kms` definewhat happens when you get (i.e. access) or set (bind) these variables- So-called “getter” and “setter” methods.  The builtin Python function `property` takes getter and setter methods and creates a property.For example, after `car` is created as an instance of `Car`, the object `car.miles` is a property.Being a property, when we set its value via `car.miles = 6000` its settermethod is triggered — in this case `set_miles`.  Decorators and PropertiesThese days its very common to see the `property` function used via a decorator.Here’s another version of our `Car` class that works as before but now usesdecorators to set up the properties<jupyter_code>class Car:

    def __init__(self, miles=1000):
        self._miles = miles
        self._kms = miles * 1.61

    @property
    def miles(self):
        return self._miles

    @property
    def kms(self):
        return self._kms

    @miles.setter
    def miles(self, value):
        self._miles = value
        self._kms = value * 1.61

    @kms.setter
    def kms(self, value):
        self._kms = value
        self._miles = value / 1.61<jupyter_output><empty_output><jupyter_text>We won’t go through all the details here.For further information you can refer to the [descriptor documentation](https://docs.python.org/3/howto/descriptor.html).  GeneratorsA generator is a kind of iterator (i.e., it works with a `next` function).We will study two ways to build generators: generator expressions and generator functions.  Generator ExpressionsThe easiest way to build generators is using *generator expressions*.Just like a list comprehension, but with round brackets.Here is the list comprehension:<jupyter_code>singular = ('dog', 'cat', 'bird')
type(singular)
plural = [string + 's' for string in singular]
plural
type(plural)<jupyter_output><empty_output><jupyter_text>And here is the generator expression<jupyter_code>singular = ('dog', 'cat', 'bird')
plural = (string + 's' for string in singular)
type(plural)
next(plural)
next(plural)
next(plural)<jupyter_output><empty_output><jupyter_text>Since `sum()` can be called on iterators, we can do this<jupyter_code>sum((x * x for x in range(10)))<jupyter_output><empty_output><jupyter_text>The function `sum()` calls `next()` to get the items, adds successive terms.In fact, we can omit the outer brackets in this case<jupyter_code>sum(x * x for x in range(10))<jupyter_output><empty_output><jupyter_text>Generator FunctionsThe most flexible way to create generator objects is to use generator functions.Let’s look at some examples.  Example 1Here’s a very simple example of a generator function<jupyter_code>def f():
    yield 'start'
    yield 'middle'
    yield 'end'<jupyter_output><empty_output><jupyter_text>It looks like a function, but uses a keyword `yield` that we haven’t met before.Let’s see how it works after running this code<jupyter_code>type(f)
gen = f()
gen
next(gen)
next(gen)
next(gen)
next(gen)<jupyter_output><empty_output><jupyter_text>The generator function `f()` is used to create generator objects (in this case `gen`).Generators are iterators, because they support a `next` method.The first call to `next(gen)`- Executes code in the body of `f()` until it meets a `yield` statement.  - Returns that value to the caller of `next(gen)`.  The second call to `next(gen)` starts executing *from the next line*<jupyter_code>def f():
    yield 'start'
    yield 'middle'  # This line!
    yield 'end'<jupyter_output><empty_output><jupyter_text>and continues until the next `yield` statement.At that point it returns the value following `yield` to the caller of `next(gen)`, and so on.When the code block ends, the generator throws a `StopIteration` error.  Example 2Our next example receives an argument `x` from the caller<jupyter_code>def g(x):
    while x < 100:
        yield x
        x = x * x<jupyter_output><empty_output><jupyter_text>Let’s see how it works<jupyter_code>g
gen = g(2)
type(gen)
next(gen)
next(gen)
next(gen)
next(gen)<jupyter_output><empty_output><jupyter_text>The call `gen = g(2)` binds `gen` to a generator.Inside the generator, the name `x` is bound to `2`.When we call `next(gen)`- The body of `g()` executes until the line `yield x`, and the value of `x` is returned.  Note that value of `x` is retained inside the generator.When we call `next(gen)` again, execution continues *from where it left off*<jupyter_code>def g(x):
    while x < 100:
        yield x
        x = x * x  # execution continues from here<jupyter_output><empty_output><jupyter_text>When `x < 100` fails, the generator throws a `StopIteration` error.Incidentally, the loop inside the generator can be infinite<jupyter_code>def g(x):
    while 1:
        yield x
        x = x * x<jupyter_output><empty_output><jupyter_text>Advantages of IteratorsWhat’s the advantage of using an iterator here?Suppose we want to sample a binomial(n,0.5).One way to do it is as follows<jupyter_code>import random
n = 10000000
draws = [random.uniform(0, 1) < 0.5 for i in range(n)]
sum(draws)<jupyter_output><empty_output><jupyter_text>But we are creating two huge lists here,  `range(n)` and `draws`.This uses lots of memory and is very slow.If we make `n` even bigger then this happens<jupyter_code>n = 100000000
draws = [random.uniform(0, 1) < 0.5 for i in range(n)]<jupyter_output><empty_output><jupyter_text>We can avoid these problems using iterators.Here is the generator function<jupyter_code>def f(n):
    i = 1
    while i <= n:
        yield random.uniform(0, 1) < 0.5
        i += 1<jupyter_output><empty_output><jupyter_text>Now let’s do the sum<jupyter_code>n = 10000000
draws = f(n)
draws
sum(draws)<jupyter_output><empty_output><jupyter_text>In summary, iterables- avoid the need to create big lists/tuples, and  - provide a uniform interface to iteration that can be used transparently in `for` loops    Recursive Function CallsThis is not something that you will use every day, but it is still useful — you should learn it at some stage.Basically, a recursive function is a function that calls itself.For example, consider the problem of computing $ x_t $ for some t when$$x_{t+1} = 2 x_t, \quad x_0 = 1 \tag{16.1}$$Obviously the answer is $ 2^t $.We can compute this easily enough with a loop<jupyter_code>def x_loop(t):
    x = 1
    for i in range(t):
        x = 2 * x
    return x<jupyter_output><empty_output><jupyter_text>We can also use a recursive solution, as follows<jupyter_code>def x(t):
    if t == 0:
        return 1
    else:
        return 2 * x(t-1)<jupyter_output><empty_output><jupyter_text>What happens here is that each successive call uses it’s own *frame* in the *stack*- a frame is where the local variables of a given function call are held  - stack is memory used to process function calls    - a First In Last Out (FILO) queue  This example is somewhat contrived, since the first (iterative) solution would usually be preferred to the recursive solution.We’ll meet less contrived applications of recursion later on.  Exercises  Exercise 1The Fibonacci numbers are defined by$$x_{t+1} = x_t + x_{t-1}, \quad x_0 = 0, \; x_1 = 1 \tag{16.2}$$The first few numbers in the sequence are $ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 $.Write a function to recursively compute the $ t $-th Fibonacci number for any $ t $.  Exercise 2Complete 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 ```python3def column_iterator(target_file, column_number):    """A generator function for CSV files.    When called with a file name target_file (string) and column number    column_number (integer), the generator function returns a generator    that steps through the elements of column column_number in file    target_file.    """     put your code heredates = column_iterator('test_table.csv', 1)for date in dates:    print(date)```  Exercise 3Suppose we have a text file `numbers.txt` containing the following lines ```textprices38721``` Using `try` – `except`, write a program to read in the contents of the file and sum the numbers, ignoring lines without numbers.  Solutions  Exercise 1Here’s the standard solution<jupyter_code>def x(t):
    if t == 0:
        return 0
    if t == 1:
        return 1
    else:
        return x(t-1) + x(t-2)<jupyter_output><empty_output><jupyter_text>Let’s test it<jupyter_code>print([x(i) for i in range(10)])<jupyter_output><empty_output><jupyter_text>Exercise 2One solution is as follows<jupyter_code>def column_iterator(target_file, column_number):
    """A generator function for CSV files.
    When called with a file name target_file (string) and column number
    column_number (integer), the generator function returns a generator
    which steps through the elements of column column_number in file
    target_file.
    """
    f = open(target_file, 'r')
    for line in f:
        yield line.split(',')[column_number - 1]
    f.close()

dates = column_iterator('test_table.csv', 1)

i = 1
for date in dates:
    print(date)
    if i == 10:
        break
    i += 1<jupyter_output><empty_output><jupyter_text>Exercise 3Let’s save the data first<jupyter_code>%%file numbers.txt
prices
3
8

7
21
f = open('numbers.txt')

total = 0.0
for line in f:
    try:
        total += float(line)
    except ValueError:
        pass

f.close()

print(total)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.08726603743401055, 'fraction_numerical': 0.02151655735082387, 'mean_word_length': 4.865968095089146, 'pattern_counts': {'":': 0, '<': 344, '<?xml version=': 0, '>': 341, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 3, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '84', 'dir': 'jupyter-structured-clean-dedup', 'id': '309858', 'license': 'BSD-3-Clause', 'n_tokens_mistral': 12002, 'n_tokens_neox': 11350, 'n_words': 4616, 'path': 'tests/project/ipynb/python_advanced_features.ipynb', 'repo_name': 'QuantEcon/sphinx-tojupyter', 'size': '37506'}