# Functions#

**Co-author**

**Prerequisites**

**Outcomes**

Economic Production Functions

Understand the basics of production functions in economics

Functions

Know how to define your own function

Know how to find and write your own function documentation

Know why we use functions

Understand scoping rules and blocks

## Application: Production Functions#

Production functions are useful when modeling the economics of firms producing goods or the aggregate output in an economy.

Though the term “function” is used in a mathematical sense here, we will be making tight connections between the programming of mathematical functions and Python functions.

### Factors of Production#

The factors of production are the inputs used in the production of some sort of output.

Some example factors of production include

Physical capital, e.g. machines, buildings, computers, and power stations.

Labor, e.g. all of the hours of work from different types of employees of a firm.

Human Capital, e.g. the knowledge of employees within a firm.

A production function maps a set of inputs to the output, e.g. the amount of wheat produced by a farm, or widgets produced in a factory.

As an example of the notation, we denote the total units of labor and physical capital used in a factory as \(L\) and \(K\) respectively.

If we denote the physical output of the factory as \(Y\), then a production function \(F\) that transforms labor and capital into output might have the form:

### An Example Production Function#

Throughout this lecture, we will use the Cobb-Douglas production function to help us understand how to create Python functions and why they are useful.

The Cobb-Douglas production function has appealing statistical properties when brought to data.

This function is displayed below.

The function is parameterized by:

A parameter \(\alpha \in [0,1]\), called the “output elasticity of capital”.

A value \(z\) called the Total Factor Productivity (TFP).

## What are (Python) Functions?#

In this class, we will often talk about `function`

s.

So what is a function?

We like to think of a function as a production line in a manufacturing plant: we pass zero or more things to it, operations take place in a set linear sequence, and zero or more things come out.

We use functions for the following purposes:

**Re-usability**: Writing code to do a specific task just once, and reuse the code by calling the function.**Organization**: Keep the code for distinct operations separated and organized.**Sharing/collaboration**: Sharing code across multiple projects or sharing pieces of code with collaborators.

## How to Define (Python) Functions?#

The basic syntax to create our own function is as follows:

```
def function_name(inputs):
# step 1
# step 2
# ...
return outputs
```

Here we see two new *keywords*: `def`

and `return`

.

`def`

is used to tell Python we would like to define a new function.`return`

is used to tell Python what we would like to**return**from a function.

Let’s look at an example and then discuss each part:

```
def mean(numbers):
total = sum(numbers)
N = len(numbers)
answer = total / N
return answer
```

Here we defined a function `mean`

that has one input (`numbers`

),
does three steps, and has one output (`answer`

).

Let’s see what happens when we call this function on the list of numbers
`[1, 2, 3, 4]`

.

```
x = [1, 2, 3, 4]
the_mean = mean(x)
the_mean
```

```
2.5
```

Additionally, as we saw in the control flow lecture, indentation controls blocks of code (along with the scope rules).

To see this, compare a function with no inputs or return values.

```
def f():
print("1")
print("2")
f()
```

```
1
2
```

With the following change of indentation…

```
def f():
print("1")
print("2")
f()
```

```
2
1
```

### Scope#

Notice that we named the input to the function `x`

and we called the output
`the_mean`

.

When we defined the function, the input was called `numbers`

and the output
`answer`

… what gives?

This is an example of a programming concept called variable scope.

In Python, functions define their own scope for variables.

In English, this means that regardless of what name we give an input variable (`x`

in this example),
the input will always be referred to as `numbers`

*inside* the body of the `mean`

function.

It also means that although we called the output `answer`

inside of the
function `mean`

, that this variable name was only valid inside of our
function.

To use the output of the function, we had to give it our own name (`the_mean`

in this example).

Another point to make here is that the intermediate variables we defined inside
`mean`

(`total`

and `N`

) are only defined inside of the `mean`

function
– we can’t access them from outside. We can verify this by trying to see what
the value of `total`

is:

```
def mean(numbers):
total = sum(numbers)
N = len(numbers)
answer = total / N
return answer # or directly return total / N
# uncomment the line below and execute to see the error
# total
```

This point can be taken even further: the same name can be bound to variables inside of blocks of code and in the outer “scope”.

```
x = 4
print(f"x = {x}")
def f():
x = 5 # a different "x"
print(f"x = {x}")
f() # calls function
print(f"x = {x}")
```

```
x = 4
x = 5
x = 4
```

The final point we want to make about scope is that function inputs and output don’t have to be given a name outside the function.

```
mean([10, 20, 30])
```

```
20.0
```

Notice that we didn’t name the input or the output, but the function was called successfully.

Now, we’ll use our new knowledge to define a function which computes the output from a Cobb-Douglas production function with parameters \(z = 1\) and \(\alpha = 0.33\) and takes inputs \(K\) and \(L\).

```
def cobb_douglas(K, L):
# Create alpha and z
z = 1
alpha = 0.33
return z * K**alpha * L**(1 - alpha)
```

We can use this function as we did the mean function.

```
cobb_douglas(1.0, 0.5)
```

```
0.6285066872609142
```

### Re-using Functions#

Economists are often interested in this question: how much does output change if we modify our inputs?

For example, take a production function \(Y_1 = F(K_1,L_1)\) which produces \(Y_1\) units of the goods.

If we then multiply the inputs each by \(\gamma\), so that \(K_2 = \gamma K_1\) and \(L_2 = \gamma L_1\), then the output is

How does \(Y_1\) compare to \(Y_2\)?

Answering this question involves something called *returns to scale*.

Returns to scale tells us whether our inputs are more or less productive as we have more of them.

For example, imagine that you run a restaurant. How would you expect the amount of food you could produce would change if you could build an exact replica of your restaurant and kitchen and hire the same number of cooks and waiters? You would probably expect it to double.

If, for any \(K, L\), we multiply \(K, L\) by a value \(\gamma\) then

If \(\frac{Y_2}{Y_1} < \gamma\) then we say the production function has decreasing returns to scale.

If \(\frac{Y_2}{Y_1} = \gamma\) then we say the production function has constant returns to scale.

If \(\frac{Y_2}{Y_1} > \gamma\) then we say the production function has increasing returns to scale.

Let’s try it and see what our function is!

```
y1 = cobb_douglas(1.0, 0.5)
print(y1)
y2 = cobb_douglas(2*1.0, 2*0.5)
print(y2)
```

```
0.6285066872609142
1.2570133745218284
```

How did \(Y_1\) and \(Y_2\) relate?

```
y2 / y1
```

```
2.0
```

\(Y_2\) was exactly double \(Y_1\)!

Let’s write a function that will compute the returns to scale for different values of \(K\) and \(L\).

This is an example of how writing functions can allow us to re-use code
in ways we might not originally anticipate. (You didn’t know we’d be
writing a `returns_to_scale`

function when we wrote `cobb_douglas`

.)

```
def returns_to_scale(K, L, gamma):
y1 = cobb_douglas(K, L)
y2 = cobb_douglas(gamma*K, gamma*L)
y_ratio = y2 / y1
return y_ratio / gamma
```

```
returns_to_scale(1.0, 0.5, 2.0)
```

```
1.0
```

Exercise

See exercise 1 in the exercise list.

It turns out that with a little bit of algebra, we can check that this will always hold for our Cobb-Douglas example above.

To show this, take an arbitrary \(K, L\) and multiply the inputs by an arbitrary \(\gamma\).

For an example of a production function that is not CRS, look at a generalization of the Cobb-Douglas production function that has different “output elasticities” for the 2 inputs.

Note that if \(\alpha_2 = 1 - \alpha_1\), this is our Cobb-Douglas production function.

Exercise

See exercise 2 in the exercise list.

### Multiple Returns#

Another valuable element to analyze on production functions is how output changes as we change only one of the inputs. We will call this the marginal product.

For example, compare the output using \(K, L\) units of inputs to that with an \(\epsilon\) units of labor.

Then the marginal product of labor (MPL) is defined as

This tells us how much additional output is created relative to the additional input. (Spoiler alert: This should look like the definition for a partial derivative!)

If the input can be divided into small units, then we can use calculus to take this limit, using the partial derivative of the production function relative to that input.

In this case, we define the marginal product of labor (MPL) and marginal product of capital (MPK) as

In the Cobb-Douglas example above, this becomes

Let’s test it out with Python! We’ll also see that we can actually return multiple things in a Python function.

The syntax for a return statement with multiple items is return item1, item2, ….

In this case, we’ll compute both the MPL and the MPK and then return both.

```
def marginal_products(K, L, epsilon):
mpl = (cobb_douglas(K, L + epsilon) - cobb_douglas(K, L)) / epsilon
mpk = (cobb_douglas(K + epsilon, L) - cobb_douglas(K, L)) / epsilon
return mpl, mpk
```

```
tup = marginal_products(1.0, 0.5, 1e-4)
print(tup)
```

```
(0.8421711708284096, 0.20740025904131265)
```

Instead of using the tuple, these can be directly unpacked to variables.

```
mpl, mpk = marginal_products(1.0, 0.5, 1e-4)
print(f"mpl = {mpl}, mpk = {mpk}")
```

```
mpl = 0.8421711708284096, mpk = 0.20740025904131265
```

We can use this to calculate the marginal products for different `K`

, fixing `L`

using a comprehension.

```
Ks = [1.0, 2.0, 3.0]
[marginal_products(K, 0.5, 1e-4) for K in Ks] # create a tuple for each K
```

```
[(0.8421711708284096, 0.20740025904131265),
(1.058620425367085, 0.13035463304111872),
(1.2101811517950534, 0.09934539767386674)]
```

### Documentation#

In a previous exercise, we asked you to find help for the `cobb_douglas`

and
`returns_to_scale`

functions using `?`

.

It didn’t provide any useful information.

To provide this type of help information, we need to add what Python programmers call a “docstring” to our functions.

This is done by putting a string (not assigned to any variable name) as
the first line of the *body* of the function (after the line with
`def`

).

Below is a new version of the template we used to define functions.

```
def function_name(inputs):
"""
Docstring
"""
# step 1
# step 2
# ...
return outputs
```

Let’s re-define our `cobb_douglas`

function to include a docstring.

```
def cobb_douglas(K, L):
"""
Computes the production F(K, L) for a Cobb-Douglas production function
Takes the form F(K, L) = z K^{\alpha} L^{1 - \alpha}
We restrict z = 1 and alpha = 0.33
"""
return 1.0 * K**(0.33) * L**(1.0 - 0.33)
```

Now when we have Jupyter evaluate `cobb_douglas?`

, our message is
displayed (or use the Contextual Help window with Jupyterlab and `Ctrl-I`

or `Cmd-I`

).

```
cobb_douglas?
```

We recommend that you always include at least a very simple docstring for nontrivial functions.

This is in the same spirit as adding comments to your code — it makes it easier for future readers/users (including yourself) to understand what the code does.

Exercise

See exercise 3 in the exercise list.

### Default and Keyword Arguments#

Functions can have optional arguments.

To accomplish this, we must these arguments a *default value* by saying
`name=default_value`

instead of just `name`

as we list the arguments.

To demonstrate this functionality, let’s now make \(z\) and \(\alpha\) arguments to our cobb_douglas function!

```
def cobb_douglas(K, L, alpha=0.33, z=1):
"""
Computes the production F(K, L) for a Cobb-Douglas production function
Takes the form F(K, L) = z K^{\alpha} L^{1 - \alpha}
"""
return z * K**(alpha) * L**(1.0 - alpha)
```

We can now call this function by passing in just K and L. Notice that it will
produce same result as earlier because `alpha`

and `z`

are the same as earlier.

```
cobb_douglas(1.0, 0.5)
```

```
0.6285066872609142
```

However, we can also set the other arguments of the function by passing more than just K/L.

```
cobb_douglas(1.0, 0.5, 0.35, 1.6)
```

```
1.0196485018554098
```

In the example above, we used `alpha = 0.35`

, `z = 1.6`

.

We can also refer to function arguments by their name, instead of only their position (order).

To do this, we would write `func_name(arg=value)`

for as many of the arguments
as we want.

Here’s how to do that with our `cobb_douglas`

example.

```
cobb_douglas(1.0, 0.5, z = 1.5)
```

```
0.9427600308913713
```

Exercise

See exercise 4 in the exercise list.

In terms of variable scope, the `z`

name within the function is
different from any other `z`

in the outer scope.

To be clear,

```
x = 5
def f(x):
return x
f(x) # "coincidence" that it has the same name
```

```
5
```

This is also true with named function arguments, above.

```
z = 1.5
cobb_douglas(1.0, 0.5, z = z) # no problem!
```

```
0.9427600308913713
```

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 with them.

Different types of variables have different functions or operations defined for them.

For example, I can divide one number by another or make a string uppercase.

It wouldn’t make sense to divide one string by another or make a number uppercase.

When certain functionality is closely tied to the type of an object, it
is often implemented as a special kind of function known as a **method**.

For now, you only need to know two things about methods:

We call them by doing

`variable.method_name(other_arguments)`

instead of`function_name(variable, other_arguments)`

.A method is a function, even though we call it using a different notation.

When we introduced the core data types, we saw many methods defined on these types.

Let’s revisit them for the `str`

, or string type.

Notice that we call each of these functions using the `dot`

syntax
described above.

```
s = "This is my handy string!"
```

```
s.upper()
```

```
'THIS IS MY HANDY STRING!'
```

```
s.title()
```

```
'This Is My Handy String!'
```

### 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:

```
class MyType:
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.

```
a = MyType(1, 2)
b = MyType(3, 4)
# Notice that these are different objects, even though they are the same type!
a == b
```

```
False
```

You can see that `a`

and `b`

are both instances of the `MyType`

class by using the `type`

function.

```
type(a)
```

```
__main__.MyType
```

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`

.

```
print(f"a.x = {a.x} and a.y = {a.y}")
```

```
a.x = 1 and a.y = 2
```

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.

```
class MyAdder:
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 `MyAdder`

and call the `add`

method, in the same way that we called methods on built-in types (like the `.upper()`

method on a string.)

```
b = MyAdder(1, 2)
print(b.add())
```

```
3
```

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.)

Exercise

See exercise 5 in the exercise list.

## More on Scope (Optional)#

Keep in mind that with mathematical functions, the arguments are just dummy names that can be interchanged.

That is, the following are identical.

The same concept applies to Python functions, where the arguments are just
placeholder names, and our `cobb_douglas`

function is identical to

```
def cobb_douglas2(K2, L2): # changed dummy variable names
# Create alpha and z
z = 1
alpha = 0.33
return z * K2**alpha * L2**(1 - alpha)
cobb_douglas2(1.0, 0.5)
```

```
0.6285066872609142
```

This is an appealing feature of functions for avoiding coding errors: names of variables within the function are localized and won’t clash with those on the outside (with more examples in scope).

Importantly, when Python looks for variables matching a particular name, it begins in the most local scope.

That is, note that having an `alpha`

in the outer scope does not impact the local one.

```
def cobb_douglas3(K, L, alpha): # added new argument
# Create alpha and z
z = 1
return z * K**alpha * L**(1 - alpha) # sees local argument alpha
print(cobb_douglas3(1.0, 0.5, 0.2))
print("Setting alpha, does the result change?")
alpha = 0.5 # in the outer scope
print(cobb_douglas3(1.0, 0.5, 0.2))
```

```
0.5743491774985174
Setting alpha, does the result change?
0.5743491774985174
```

A crucial element of the above function is that the `alpha`

variable
was available in the local scope of the function.

Consider the alternative where it is not. We have removed the `alpha`

function parameter as well as the local definition of `alpha`

.

```
def cobb_douglas4(K, L): # added new argument
# Create alpha and z
z = 1
# there are no local alpha in scope!
return z * K**alpha * L**(1 - alpha)
alpha = 0.2 # in the outer scope
print(f"alpha = {alpha} gives {cobb_douglas4(1.0, 0.5)}")
alpha = 0.3
print(f"alpha = {alpha} gives {cobb_douglas4(1.0, 0.5)}")
```

```
alpha = 0.2 gives 0.5743491774985174
alpha = 0.3 gives 0.6155722066724582
```

The intuition of scoping does not apply only for the “global” vs. “function” naming of variables, but also for nesting.

For example, we can define a version of `cobb_douglas`

which
is also missing a `z`

in its inner-most scope, then put the function
inside of another function.

```
z = 1
def output_given_alpha(alpha):
# Scoping logic:
# 1. local function name doesn't clash with global one
# 2. alpha comes from the function parameter
# 3. z comes from the outer global scope
def cobb_douglas(K, L):
return z * K**alpha * L**(1 - alpha)
# using this function
return cobb_douglas(1.0, 0.5)
alpha = 100 # ignored
alphas = [0.2, 0.3, 0.5]
# comprehension variables also have local scope
# and don't clash with the alpha = 100
[output_given_alpha(alpha) for alpha in alphas]
```

```
[0.5743491774985174, 0.6155722066724582, 0.7071067811865476]
```

## Exercises#

### Exercise 1#

What happens if we try different inputs in our Cobb-Douglas production function?

```
# Compute returns to scale with different values of `K` and `L` and `gamma`
```

### Exercise 2#

Define a function named `var`

that takes a list (call it `x`

) and
computes the variance. This function should use the mean function that we
defined earlier.

Hint

\(\text{variance} = \frac{1}{N-1} \sum_i (x_i - \text{mean}(x))^2\)

```
# Your code here.
```

### Exercise 3#

Redefine the `returns_to_scale`

function and add a docstring.

Confirm that it works by running the cell containing `returns_to_scale?`

below.

*Note*: You do not need to change the actual code in the function — just
copy/paste and add a docstring in the correct line.

```
# re-define the `returns_to_scale` function here
```

```
# test it here
returns_to_scale?
```

### Exercise 4#

Experiment with the `sep`

and `end`

arguments to the `print`

function.

These can *only* be set by name.

```
# Your code here.
```

### 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.

```
# 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`

.

```
# Your code here.
```