Lecture 2.1

Functions, Namespaces, and Scope

Python and Big Data in Economics

Guoliang Ma
The Chow Institute, 2025

What you will learn

How you can define and call simple functions.

How functions, as objects (chunks of memory), are stored, and when you call a function, what will happen inside Python.

Functions naturally divide memory into smaller pieces, and names live in such spaces. You’ll see how Python manages the spaces and controls the accessibility of variables.

“Functions are first-class objects.”

2.1 Simple functions

In-class exercise 2.1.1

Review (read text file, work with dict):

Context: You are managing a database for a training camp. Under the data folder, there is students’ information from 5 classes. Each class has an age.txt and a gender.txt.

Please read the age.txt and gender.txt from class 1.

Make a new .txt file to store the information of class 1.

Repeat the steps for the other classes.

2.1 Simple functions

From the above example, you probably have noticed that for each class, the job is the same, requiring the same code to be used more than once. What are the disadvantages?

Prone to errors

Hard to modify

We now introduce how to abstract the procedure and encapsulate it into a function.

2.1 Simple functions

How is a function composed?

A function is designed to free us from repetitive jobs so that we can work more efficiently. So we need the function to know what we want to do. This is known as the function body.

The function will work on different classes (or more generally, objects). We need to let it know which one we need to proceed. We provide case-dependent information to the function via parameters.

As always, the name is so important. A function also needs a name.

There is another component of a function: the return value. We’ll talk about it later.

2.1.1 Simple functions: def and call

Putting the elements together. In code, we create a function with:

The above is the Pythonic style to define a function.

In-class exercise 2.1.2

Assemble the code from exercise 2.1.1 to define a function.

def <name>(<parameters>):
    <body>
    return None

2.1.1 Simple functions: def and call

Once defined, the functions will be stored in the memory as other objects. Try to print the function and see what is printed.

Programming functions are very similar to math functions.

In math, we need to repeatedly find the distance between any two points P1=(𝑥1,𝑦1) and P2=(𝑥2,𝑦2). So we define the distance function

𝑓(𝑃1,𝑃2)=(𝑥1𝑥2)2+(𝑦1𝑦2)2.

We define functions so we can use them. In Python, when we use a function, we call it.

In math, given two specific points (1, 2) and (3, 4). We evaluate the 𝑓 function by passing the values to the function: 𝑓((1,2), (3,4)).

Python does exactly the same --- pairs of parentheses.

2.1.1 Simple functions: def and call

In-class exercise 2.1.3

Call the function you just defined and apply it to class 2.

2.1.1 Simple functions: def and call

In-class exercise 2.1.4

Summarize the steps to define a function.

In-class exercise 2.1.5

Define a function that computes the square of all numbers in a list. For example, if the list is l = [1, 2, 3], the function finds [1, 4, 9].

Store the new list for future use.

Special topic I: the Python call stack

Code Example (the call stack)

import inspect


def print_stack():
    stack = inspect.stack()
    for frame in stack:
        print(f"Function: {frame.function}")
        print(f"Code context: {frame.code_context}")
        print("-" * 80)


print_stack()

Special topic I: the Python call stack

A summary of a Python function call

Add a local frame, forming a new environment

Bind the function's formal parameters to its arguments in that frame

Execute the body of the function in that new environment

source: Prof. John DeNero, CS61A, UC Berkeley

In-class exercise Special topic.1

How do you distinguish a function from a function call

Special topic II: the Newton’s method

This is more mathematical …

Given any function, how do you find its root?

The Newton’s method is one of the most commonly used.

In-class exercise Special topic II.1

Write a function to find the root of 𝑓(𝑥)=0.3×𝑥2sin(𝑥)+𝑥 around −4.5. When the error is smaller than a value, report the root.

How do you stop the function iteration?

2.1 Simple functions (special case I)

In Newton’s method, we can choose an error bound each time we call the function.

Usually we want to use the same bound, changing it only when needed.

A default parameter value supplies that value when the caller omits the argument.

2.1.3 Simple functions (default parameters)

In-class exercise 2.1.6

Try the code below. What do you find? Can you explain?

def append_to(element, to=[]):
    to.append(element)
    return to

my_list = append_to(12)
print(my_list)

my_other_list = append_to(42)
print(my_other_list)

2.1.3 Simple functions (function factory)

In-class exercise 2.1.7

Try the code below. What do you find? Can you explain?

ff = {}

for i in range(5):
    def f():
        return i
    ff[i] = f


ff[3]()

2.2 Namespaces and scopes of variables

We have learned that Python interpreter binds names to objects. A namespace is a dictionary (more precisely, a hash map) telling us which name is bound to which value.

For example, we have the builtins, globals, closure, and the local namespaces.

A related concept is scope, which describes the range where a name can be resolved.

2.2.1 Namespaces and scopes of variables

In-class exercise 2.2.2.1

Write a function to swap the values of two objects. For example, a, b = 1, 2. After swapping, a is 2 and b is 1.

Analyze the function for variable names.

Check the names in the namespaces

import builtins

print(type(builtins), dir(builtins))

globals()

2.2.1 Namespaces and scopes of variables

In-class exercise 2.2.2.2

Can you please check the objects living in the local namespace?

2.2.2 Scope of a variable

When Python resolves a name, it searches in LEGB order:

Search order Namespace Where it comes from
1 · L Local The current function call
2 · E Enclosing Enclosing function calls
3 · G Global The current module
4 · B Built-in Python’s built-in names

nonlocal targets an existing binding in an enclosing function; global targets the module namespace.

import builtins
print(type(builtins), dir(builtins))
globals()

2.2.2 Scope of a variable

In-class exercise 2.2.3

What will the following give us? How can we make it normal?

print(max(1, 2))
max = min
print(max(1, 2))

2.2.2 Scope of a variable

A long example

A more complicated example

In-class exercise 2.2.2.3 [purpose of variable scopes]

How does Python count number of references?

avoid unnecessary global variables

How do you modify the function factory in Special case II?

explicitly refer to a nonlocal variable

How do you hide information from the global functions?

follow the LEGB rule

2.3 Functions are first-class objects

First-class objects are flexible. Being first class means there is no restrictions on the use of the object. We can pass this object as an argument to a function and can return it as a return value. We can also create dictionaries to store it, etc.

When we use a function as an argument and return values of another "higher-level" functions, we are using higher-order functions.

2.3 Functions are first-class objects

Functions as return values

def intercept_1():
    a = 1
    def slope_2(x):
        return 2 * x + a
    return slope_2


linear_trans = intercept_1()
linear_trans(3)

2.3 Functions are first-class objects

Functions as arguments

def call_count(func, x=[0]):
    print(f"calling {x[0] + 1} times")
    x[0] += 1
    func()


call_count(print)
call_count(print)
call_count(print)

2.3 Functions are first-class objects

In-class exercise 2.3.1

Modify code example 1, so that we can select the intercept.

Modify code example 1, so that we can also select the slope.

Modify code example 2, so that we do not need default parameters.

2.3 Functions … objects (special case III)

In the call_count example, we can pass a function as an argument to the function. But this function cannot have its own parameters. How can we pass arguments to the function being counted?

def call_count(func, arg_to_called, x=[0]):
    print(f"calling {x[0]} times")
    x[0] += 1
    func(arg_to_called)


call_count(print, "hello")
call_count(print, "python")
call_count(print, "world")

2.3 Functions … objects (special case III)

When we are not sure about how many parameters to pass to the function, the conventional parameter names are args and kwargs. The special syntax is *args and **kwargs; the names themselves are not keywords.

The * operator. A star is known as the (un)packing operator.

In-class exercise 2.3.2

How do they differ?

a = 1, 2, 3

a, b, c = 1, 2, 3

a, b = 1, 2, 3

a, *b, c = 1, 2, 3, 4, 5

2.3 Functions … objects (special case III)

In-class exercise 2.3.3

Some cases deliberately contain errors. Consider and run each assignment separately.

Summarize the pattern by considering

*a, b = 1, 2, 3, 4, 5

a, *b = 1, 2, 3, 4, 5

*a, *b = 1, 2, 3, 4, 5

*a, b, c = 1, 2, 3, 4, 5

*a, b = 1

2.3 Functions … objects (special case III)

Note that a is a list but *a unpacks the list into several elements. Passing indefinite number of arguments to a function involves two steps:

collecting positional arguments in a tuple (when defining *args)

unpacking an iterable with * when making a call

To check the unpacking behavior, we can use the sep parameter.

def call_count(func, *args, x=[0]):
    print(f"calling {x[0]} times")
    x[0] += 1
    func(*args, sep=", ")


call_count(print, "hello", "python", "world")

2.3 Functions … objects (special case III)

The ** operator.

Another type of arguments is called keyword arguments, which must be passed to a function with the form param=arg. These are named arguments. Unlike * that unpacks a list, we use ** to unpack a dictionary. There are fewer use cases than the unpacking of a list.

dict1 = {"a": 1,
         "b": 2,
         "c": 3}

dict2 = {"d": 4,
         "e": 5,
         "f": 6}

combined_dict = {**dict1, **dict2}

Argument and parameter order

Ordinary positional arguments precede keyword arguments in a call:

print("hello", "Python", sep=", ")

In the positional parameter list of a function definition, required parameters come before parameters with defaults.

Keyword-only parameters follow * or *args; **kwargs, when present, comes last.

def function(required, optional=0, *args, keyword_only, **kwargs):
    ...

2.3 Functions … objects (special case III)

In-class exercise 2.3.4

Note that **kwargs is actually unpacking a dict. This is to say, kwargs is a dict. We learned that a dict has keys and values. Read the document about named arguments: https://docs.python.org/3/library/stdtypes.html#dict

Write a function to take the sum of several (the numbers are unknown) named arguments. For example,

def sum_of_kwargs(???):
    pass

sum_of_kwargs(Alice=5, Bob=3, Charlie=4)

The assembly of tools

The creation of tools

source: https://cn.nytimes.com/culture/20180515/2001-a-space-odyssey-kubrick/

Functions, Namespaces, and Scope: original illustration, slide 25.