Skip to content

Commit

Permalink
Merge pull request #233 from QuantEcon/classes-randomness
Browse files Browse the repository at this point in the history
Added example for creating clasess to the randomness.md lecture
  • Loading branch information
doctor-phil authored Jul 19, 2023
2 parents 98956d6 + a7a940c commit 9af3667
Show file tree
Hide file tree
Showing 2 changed files with 190 additions and 0 deletions.
86 changes: 86 additions & 0 deletions lectures/python_fundamentals/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,8 @@ In that example, the `z` on the left hand side of `z = z` refers
to the local variable name in the function whereas the `z` on the
right hand side refers to the `z` in the outer scope.



### Aside: Methods

As we learned earlier, all variables in Python have a type associated
Expand Down Expand Up @@ -625,6 +627,74 @@ s.upper()
s.title()
```


### Creating Custom Types

Python allows for Object-Oriented Programming (OOP), allowing you to define your own custom types and merge together some sets of parameters with custom methods. This can help you streamline your code by making it more modular.

We are used to defining variables like `x = dict("a": 1, "b": 2)` and then using notation like `x["a"]` to access the value of `1`. We can also define our own custom types and use them in similar ways.

For example, a simple class that stores two variables would look like this:

```{code-cell} python
class A:
def __init__(self, x, y):
self.x = x
self.y = y
```

Used both internal and external to classes, the `__init__` method is a special method that is called when an object is created. It is used to initialize the object's state. The `self` argument refers to the object itself. The `self` argument is always the first argument of any method in a class. The `self` argument is not passed in when the method is called, but Python will pass in the object itself when the method is called.

A class, defined by the `class` keyword, is a blueprint for an object. It defines the attributes and methods that an object will have. An object is an instance of a class that has been created and assigned to a variable. It is created by calling the class name as if it were a function. When you call the class name, the object is created and the `__init__` method is called by default.

```{code-cell} python
a = A(1, 2)
b = A(3, 4)
# Notice that these are different objects
a == b
```

You can see that `a` and `b` are both instances of the `A` class by using the `type` function.

```{code-cell} python
type(a)
```
Point at the debugger to see the `a.x` etc. fields
You can access the attributes of an object using the dot notation. For example, to access the `x` attribute of the `a` object, you would use `a.x`.

```{code-cell} python
print(f"a.x = {a.x} and a.y = {a.y}")
```

In addition to attributes, objects can also have methods. Methods are functions that are defined inside of a class. They are accessed using the dot notation as well. For example, let's define a method that adds the `x` and `y` attributes of an object.


```{code-cell} python
class B:
def __init__(self, x, y):
self.x = x
self.y = y
def add(self):
return self.x + self.y
```

We can now create an object of type `B` and call the `add` method, in the same way that we called methods on built-in types (like the `.upper()` method on a string.)

```{code-cell} python
b = B(1, 2)
print(b.add())
```

Using custom classes can often be a helpful way to organize your code and make it more modular, by grouping together related variables and functions. Understanding how to create and use custom classes is also a key part of understanding how Python works under the hood, and can be crucial to using some of the more advanced Python packages (like [PyTorch](https://pytorch.org/tutorials/beginner/basics/quickstart_tutorial.html).)

````{admonition} Exercise
:name: dir2-4-5
See exercise 5 in the {ref}`exercise list <ex2-4>`.
````


## More on Scope (Optional)

Keep in mind that with mathematical functions, the arguments are just dummy names
Expand Down Expand Up @@ -789,3 +859,19 @@ These can *only* be set by name.
```

({ref}`back to text <dir2-4-4>`)

### Exercise 5

Define a custom class called `CobbDouglas` that collects the parameters `z` and `alpha` as attributes, and has a method called `produce` that takes `K` and `L` as arguments and returns the output from the Cobb-Douglas production function.

```{code-cell} python
# Your code here.
```

Now create an instance of the `CobbDouglas` class called `cobb_douglas1` with `z = 1` and `alpha = 0.33`. Use the `produce` method to compute the output when `K = 1` and `L = 0.5`.

```{code-cell} python
# Your code here.
```

({ref}`back to text <dir2-4-5>`)
104 changes: 104 additions & 0 deletions lectures/scientific/randomness.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,110 @@ element at a time.
For more information see the
[QuantEcon lecture on performance Python](https://python-programming.quantecon.org/numba.html) code.


### Aside: Using Class to Hold Parameters

We have been using objects and classes both internal to python (e.g. `list`) from external libraries (e.g. `numpy.array`). Sometimes it is convenient to create your own classes to organize parameter, data, and functions.

In this section we will reimplement our function using new classes to hold parameters.

First, we rewrite `simulate_loan_repayments` so that instead of a collection of individual parameters, it takes in an object (titles `params`).

```{code-cell} python
def simulate_loan_repayments_2(N, params):
# Extract fields from params object
r = params.r
repayment_part = params.repayment_part
repayment_full = params.repayment_full
random_numbers = np.random.rand(N)
# start as 0 -- no repayment
repayment_sims = np.zeros(N)
# adjust for full and partial repayment
partial = random_numbers <= 0.20
repayment_sims[partial] = repayment_part
full = ~partial & (random_numbers <= 0.95)
repayment_sims[full] = repayment_full
repayment_sims = (1 / (1 + r)) * repayment_sims
return repayment_sims
```

Any object which fulfills `params.r, params.replayment_part` and `params.repayment_full` will work, so we will create a few versions of this to explore features of custom classes in Python.

The most important function in a class is the `__init__` function which determines how it is constructed and creates an object of that type. This function has the special argument `self` which refers to the new object being created, and with which you can easily add new fields. For example,

```{code-cell} python
class LoanRepaymentParams:
# A special function 'constructor'
def __init__(self, r, repayment_full, repayment_part):
self.r = r
self.repayment_full = repayment_full
self.repayment_part = repayment_part
# Create an instance of the class
params = LoanRepaymentParams(0.05, 50_000.0, 25_000)
print(params.r)
```

The inside of the `__init__` function simply takes the arguments and assigns them as new fields in the `self`. Calling the `LoanRepaymentParams(...)` implicitly calls the `__init__` function and returns the new object.

We can then use the new object to call the function `simulate_loan_repayments_2` as before.

```{code-cell} python
N = 1000
params = LoanRepaymentParams(0.05, 50_000.0, 25_000)
print(np.mean(simulate_loan_repayments_2(N, params)))
```

One benefit of using a class is that you can do calculations in the constructor. For example, instead of passing in the partial repayment amount, we could pass in the fraction of the full repayment that is paid.

```{code-cell} python
class LoanRepaymentParams2:
def __init__(self, r, repayment_full, partial_fraction = 0.3):
self.r = r
self.repayment_full = repayment_full
# This does a calculation and sets a new value
self.repayment_part = repayment_full * partial_fraction
# Create an instance of the class
params = LoanRepaymentParams2(0.05, 50_000.0, 0.5)
print(params.repayment_part) # Acccess the calculation
print(np.mean(simulate_loan_repayments_2(N, params)))
```

This setup a default value for the `partial_fraction` so that we could also have called this with `LoanRepaymentParams2(0.05, 50_000)`.


Finally, there are some special features we can use to create classes in python which automatically create the `__init__` function, allow for more easily setting default values. The easiest is to create a `dataclass` (see [documentation](https://docs.python.org/3/library/dataclasses.html)).

```{code-cell} python
from dataclasses import dataclass
@dataclass
class LoanRepaymentParams3:
r: float = 0.05
repayment_full: float = 50_000
repayment_part: float = 25_000
params = LoanRepaymentParams3() # uses all defaults
params2 = LoanRepaymentParams3(repayment_full= 60_000) # changes the full repayment amount
# show the objects
print(params)
print(params2)
# simulate using the new object
print(np.mean(simulate_loan_repayments_2(N, params2)))
```

The `@dataclass` is an example of a python decorator (see [documentation](https://docs.python.org/3/glossary.html#term-decorator)). Decorators take in a class (or function) and return a new class (or function) with some additional features. In this case, it automatically creates the `__init__` function, allows for default values, and adds a new `__repr__` function which determines how the object is printed.

#### Profitability Threshold

Rather than looking for the break even point, we might be interested in the largest loan size that
Expand Down

2 comments on commit 9af3667

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.