Skip to content

Latest commit

 

History

History
333 lines (236 loc) · 13.6 KB

File metadata and controls

333 lines (236 loc) · 13.6 KB

Functions and Modules

Before we start this lesson, let us first study a math problem: how many groups of positive integer solutions does the equation below have?

$$ x_{1} + x_{2} + x_{3} + x_{4} = 8 $$

You may already have thought of it: this problem is actually the same as dividing 8 apples into 4 groups, with at least one apple in each group. It is also the same as putting 3 dividers into the 7 gaps between 8 apples to split them into 4 groups. So the answer is $\small{C_{7}^{3} = 35}$, where:

$$ C_m^n = \frac {m!} {n!(m-n)!} $$

Based on what we learned before, we can use loops and repeated multiplication to calculate $\small{m!}$, $\small{n!}$, and $\small{(m-n)!}$ separately, and then use division to get the combination number $\small{C_{m}^{n}}$, as shown below.

"""
Read m and n, then compute C(m, n)

Version: 1.0
Author: Luo Hao
"""

m = int(input('m = '))
n = int(input('n = '))
# Calculate m!
fm = 1
for num in range(1, m + 1):
    fm *= num
# Calculate n!
fn = 1
for num in range(1, n + 1):
    fn *= num
# Calculate (m - n)!
fk = 1
for num in range(1, m - n + 1):
    fk *= num
# Calculate C(m, n)
print(fm // fn // fk)

I do not know whether you noticed it, but in the code above we did the factorial operation three times. Although the values of $\small{m}$, $\small{n}$, and $\small{m - n}$ are different, the three code blocks are not essentially different, so they are duplicate code. The world-class programming master Martin Fowler once said: "Code has many bad smells, and duplication is one of the worst!" To write high-quality code, the first thing is to solve the problem of duplicate code. For the code above, we can package the factorial operation into a code block called a function. Then, whenever we need to calculate a factorial, we only need to call the function.

Defining Functions

In mathematics, functions are usually written in forms like $\small{y = f(x)}$ or $\small{z = g(x, y)}$. In $\small{y = f(x)}$, $\small{f}$ is the function name, $\small{x}$ is the independent variable, and $\small{y}$ is the dependent variable. In $\small{z = g(x, y)}$, $\small{g}$ is the function name, $\small{x}$ and $\small{y}$ are the independent variables, and $\small{z}$ is the dependent variable. Python functions follow the same structure. Every function has its own name, independent variables, and dependent variable. We usually call the independent variables of a Python function parameters, and the dependent variable the return value.

In Python, we can use the def keyword to define a function. Like variables, every function should also have a good name, and the naming rules are the same as the naming rules for variables. In the parentheses after the function name, we can set the function parameters, which are the independent variables we just talked about. After the function finishes running, we use the return keyword to return the result of the function, which is the dependent variable we just talked about. If there is no return statement in a function, the function returns None, which means an empty value. Also, a function can have no independent variables, which means no parameters, but the parentheses after the function name are still required. The things a function needs to do are placed after the function definition line by indentation, just like the code blocks in branching and loop structures, as shown below.

Now let us put the factorial operation from the earlier code into a function. In this way, we refactor the previous code. Refactoring means adjusting the structure of the code without changing the execution result. The refactored code is shown below.

"""
Read m and n, then compute C(m, n)

Version: 1.1
Author: Luo Hao
"""


def fac(num):
    """Return the factorial of a non-negative integer."""
    result = 1
    for n in range(2, num + 1):
        result *= n
    return result


m = int(input('m = '))
n = int(input('n = '))
# When calculating factorials, we do not need to write repeated code.
# We only need to call the function directly.
# The syntax for calling a function is to put parentheses after the function name and pass in parameters.
print(fac(m) // fac(n) // fac(m - n))

You can feel that the code above is simpler and more elegant than the earlier version. More importantly, the factorial function fac that we defined can also be used again in other code that needs factorials. So, using functions can help us package code that is relatively independent in function and will be used repeatedly. When we need this code, instead of writing duplicate code again, we reuse existing code by calling the function.

In fact, the math module in Python's standard library already has a function named factorial that calculates factorials. We can directly import the math module with import math, and then use math.factorial to call the factorial function. We can also directly import the factorial function with from math import factorial, as shown below.

"""
Read m and n, then compute C(m, n)

Version: 1.2
Author: Luo Hao
"""
from math import factorial

m = int(input('m = '))
n = int(input('n = '))
print(factorial(m) // factorial(n) // factorial(m - n))

In the future, the functions we use are either functions we define ourselves, or functions provided by Python's standard library or third-party libraries. If there is already a ready-to-use function, there is no need for us to define it again. "Reinventing the wheel" is a very bad thing. For the code above, if you think the name factorial is too long and not very convenient when writing code, we can also use the as keyword to give it an alias when importing the function. When calling the function, we can use the alias instead of the original name.

"""
Read m and n, then compute C(m, n)

Version: 1.3
Author: Luo Hao
"""
from math import factorial as f

m = int(input('m = '))
n = int(input('n = '))
print(f(m) // f(n) // f(m - n))

Function Parameters

Positional and Keyword Arguments

Let us write another function. According to the lengths of three sides, it checks whether they can form a triangle. If they can form a triangle, it returns True; otherwise, it returns False, as shown below.

def make_judgement(a, b, c):
    """Return whether three sides can form a triangle."""
    return a + b > c and b + c > a and a + c > b

The three parameters in the make_judgement function above are called positional parameters. When calling the function, we usually pass them from left to right, and the number of arguments passed in must be the same as the number of parameters when the function is defined, as shown below.

print(make_judgement(1, 2, 3))  # False
print(make_judgement(4, 5, 6))  # True

If we do not want to give the values of a, b, and c from left to right, we can also use keyword arguments, and pass the parameters by writing parameter_name=parameter_value, as shown below.

print(make_judgement(b=2, c=3, a=1))  # False
print(make_judgement(c=6, b=4, a=5))  # True

When defining a function, we can use / in the parameter list to set positional-only parameters, and use * to set keyword-only parameters. So-called positional-only parameters are parameters that can only receive values by parameter position when calling the function. Keyword-only parameters can only be passed and received by writing parameter_name=parameter_value. Look at the example below.

def make_judgement(a, b, c, /):
    return a + b > c and b + c > a and a + c > b

Note: Positional-only parameters are a new feature introduced in Python 3.8. If you use a lower Python version, you need to pay attention to this.

def make_judgement(*, a, b, c):
    return a + b > c and b + c > a and a + c > b

Default Values

Python allows function parameters to have default values. We can package the operation of rolling dice in the CRAPS game example we talked about before into a function, as shown below.

from random import randrange


def roll_dice(n=2):
    """Roll n dice and return the total."""
    total = 0
    for _ in range(n):
        total += randrange(1, 7)
    return total


print(roll_dice())
print(roll_dice(3))

Let us look at a simpler example.

def add(a=0, b=0, c=0):
    """Add three numbers."""
    return a + b + c


print(add())         # 0
print(add(1))        # 1
print(add(1, 2))     # 3
print(add(1, 2, 3))  # 6

It should be noted that parameters with default values must be placed after parameters without default values. Otherwise, it will raise SyntaxError. The error message is non-default argument follows default argument, which means a parameter without a default value was placed after a parameter with a default value.

Variable-Length Arguments

In Python, we can use star-expression syntax to make a function support variable-length arguments. Variable-length arguments mean that when calling a function, we can pass in 0 or any number of arguments. In the future, when we develop business projects in teams, we may design functions for other people to use, but sometimes we do not know how many arguments the caller will pass to the function. At that time, variable-length arguments are useful.

def add(*args):
    total = 0
    for val in args:
        if type(val) in (int, float):
            total += val
    return total


print(add())                      # 0
print(add(1))                     # 1
print(add(1, 2, 3))               # 6
print(add(1, 2, 'hello', 3.45, 6))  # 12.45

If we want to pass some parameters in the form of parameter_name=parameter_value, and we are also not sure how many of these parameters there will be, we can also add variable keyword arguments to the function and group the passed keyword arguments into a dictionary, as shown below.

def foo(*args, **kwargs):
    print(args)
    print(kwargs)


foo(3, 2.1, True, name='Luo Hao', age=43, gpa=4.95)

Output:

(3, 2.1, True)
{'name': 'Luo Hao', 'age': 43, 'gpa': 4.95}

Managing Functions with Modules

No matter what programming language we use to write code, naming variables and functions can lead to the awkward situation of name conflicts. The simplest scene is defining two functions with the same name in one .py file:

def foo():
    print('hello, world!')


def foo():
    print('goodbye, world!')


foo()

Of course, the situation above is easy to avoid, but in a team project with many programmers, several programmers may all define a function named foo. In this case, how do we solve the name conflict? The answer is actually very simple. In Python, each file represents one module. We can have functions with the same name in different modules. When using the function, we import the specified module with the import keyword and then use the fully qualified name module_name.function_name, and then we can tell which module's foo function we want to use.

module1.py

def foo():
    print('hello, world!')

module2.py

def foo():
    print('goodbye, world!')

test.py

import module1
import module2

module1.foo()
module2.foo()

When importing a module, we can also use the as keyword to give the module an alias. In this way, we can use a shorter fully qualified name.

import module1 as m1
import module2 as m2

m1.foo()
m2.foo()

In the two code sections above, what we imported were the modules that define the functions. We can also directly import the functions we need from the module by using from...import....

from module1 import foo

foo()

from module2 import foo

foo()

But if we directly import two functions with the same name from two different modules, the later import will replace the earlier import. If we want to use both foo functions at the same time in the code above, there is still a way. We can use the as keyword to give aliases to the imported functions.

from module1 import foo as f1
from module2 import foo as f2

f1()
f2()

Standard Library Modules and Built-In Functions

Python's standard library contains many useful modules and functions. We have already seen:

  • random for random numbers and sampling
  • time for time-related operations
  • math for mathematical functions such as sine, cosine, exponentials, and logarithms

Python also has many built-in functions that require no import. Examples include:

Function Description
abs absolute value
bin convert an integer to binary
chr convert a Unicode code point to a character
hex convert an integer to hexadecimal
input read one line of input
len get the length of a string, list, etc.
max return the maximum value
min return the minimum value
oct convert an integer to octal
open open a file
ord convert a character to its Unicode code point
pow exponentiation
print print output
range construct a range sequence
round round a number
sum sum values in a sequence
type return the type of an object

Summary

A function is a package of code that is relatively independent in function and will be used repeatedly. After learning how to define and use functions, we can write better code. Of course, Python's standard library has already provided many modules and common functions for us. If we use these modules and functions well, we can do more things with less code. If these modules and functions still cannot meet our needs, then we may need to define our own functions, and then use modules to manage these custom functions.