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:

\[ Y = F(K, L) \]

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.

\[ Y = z K^{\alpha} L^{1-\alpha} \]

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

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

\[ Y_2 = F(K_2, L_2) = F(\gamma K_1, \gamma L_1) \]

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

\[\begin{split} \begin{aligned} F(\gamma K, \gamma L) &= z (\gamma K)^{\alpha} (\gamma L)^{1-\alpha}\\ &= z \gamma^{\alpha}\gamma^{1-\alpha} K^{\alpha} L^{1-\alpha}\\ &= \gamma z K^{\alpha} L^{1-\alpha} = \gamma F(K, L) \end{aligned} \end{split}\]

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.

\[ Y = z K^{\alpha_1} L^{\alpha_2} \]

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

\[ \frac{F(K, L + \varepsilon) - F(K, L)}{\varepsilon} \]

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

\[\begin{split} \begin{aligned} MPL(K, L) &= \frac{\partial F(K, L)}{\partial L}\\ MPK(K, L) &= \frac{\partial F(K, L)}{\partial K} \end{aligned} \end{split}\]

In the Cobb-Douglas example above, this becomes

\[\begin{split} \begin{aligned} MPK(K, L) &= z \alpha \left(\frac{K}{L} \right)^{\alpha - 1}\\ MPL(K, L) &= (1-\alpha) z \left(\frac{K}{L} \right)^{\alpha}\\ \end{aligned} \end{split}\]

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:

  1. We call them by doing variable.method_name(other_arguments) instead of function_name(variable, other_arguments).

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

\[\begin{split} \begin{eqnarray} f(K, L) &= z\, K^{\alpha} L^{1-\alpha}\\ f(K_2, L_2) &= z\, K_2^{\alpha} L_2^{1-\alpha} \end{eqnarray} \end{split}\]

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`

(back to text)

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.

(back to text)

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?

(back to text)

Exercise 4#

Experiment with the sep and end arguments to the print function.

These can only be set by name.

# Your code here.

(back to text)

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.

(back to text)