| jupytext |
|
||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| kernelspec |
|
||||||||||||||||||||||||||||||||||||
| translation |
|
(functions)=
<div id="qe-notebook-header" align="right" style="text-align:right;">
<a href="https://quantecon.org/" title="quantecon.org">
<img style="width:250px;display:inline;" width="250px" src="https://assets.quantecon.org/img/qe-menubar-logo.svg" alt="QuantEcon">
</a>
</div>
函数是几乎所有编程语言都提供的极其有用的构造。
我们已经接触过几个函数,例如
- NumPy 中的
sqrt()函数,以及 - 内置的
print()函数
在本讲座中,我们将
- 系统地介绍函数,涵盖语法和使用场景,以及
- 学习构建我们自己的用户自定义函数。
我们将使用以下导入。
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl # i18n
import matplotlib.font_manager # i18n
FONTPATH = "_fonts/SourceHanSerifSC-SemiBold.otf" # i18n
mpl.font_manager.fontManager.addfont(FONTPATH) # i18n
mpl.rcParams['font.family'] = ['Source Han Serif SC'] # i18n
函数是程序中实现特定任务的命名代码段。
许多函数已经存在,我们可以直接使用它们。
首先我们回顾这些函数,然后讨论如何构建我们自己的函数。
Python 有许多内置函数,无需 import 即可使用。
我们已经接触过一些
max(19, 20)
print('foobar')
str(22)
type(22)
Python 内置函数的完整列表在这里。
如果内置函数不能满足我们的需求,我们要么需要导入函数,要么创建自己的函数。
导入和使用函数的示例已在{doc}上一讲 <python_by_example>中给出。
这里再举一个例子,用于检验给定年份是否为闰年:
import calendar
calendar.isleap(2024)
在许多情况下,能够定义我们自己的函数是非常有用的。
让我们从讨论如何定义函数开始。
这是一个非常简单的 Python 函数,实现了数学函数
def f(x):
return 2 * x + 1
现在我们已经定义了这个函数,让我们调用它并检验它是否符合预期:
f(1)
f(10)
这是一个较长的函数,用于计算给定数字的绝对值。
(这样的函数已经作为内置函数存在了,但我们作为练习来编写自己的版本。)
def new_abs_function(x):
if x < 0:
abs_value = -x
else:
abs_value = x
return abs_value
让我们回顾一下这里的语法。
def是用于开始函数定义的 Python 关键字。def new_abs_function(x):表明该函数名为new_abs_function,且有一个参数x。- 缩进的代码是称为函数体的代码块。
return关键字表示abs_value是应该返回给调用代码的对象。
整个函数定义由 Python 解释器读取并存储在内存中。
让我们调用它来验证它是否正常工作:
print(new_abs_function(3))
print(new_abs_function(-3))
注意,一个函数可以有任意多个 return 语句(包括零个)。
函数的执行在遇到第一个 return 时终止,这允许如下示例的代码
def f(x):
if x < 0:
return 'negative'
return 'nonnegative'
(通常不鼓励编写具有多个 return 语句的函数,因为这会使逻辑难以跟踪。)
没有 return 语句的函数会自动返回特殊的 Python 对象 None。
(pos_args)=
在{ref}前一讲 <python_by_example>中,你遇到了以下语句
:class: no-execute
plt.plot(x, 'b-', label="white noise")
在这个对 Matplotlib 的 plot 函数的调用中,注意最后一个参数是用 name=argument 语法传递的。
这称为关键字参数,其中 label 是关键字。
非关键字参数称为位置参数,因为它们的含义由顺序决定
plot(x, 'b-')与plot('b-', x)不同
当函数有很多参数时,关键字参数特别有用,因为此时很难记住正确的顺序。
你可以毫无困难地在用户自定义函数中采用关键字参数。
下一个示例说明了语法
def f(x, a=1, b=1):
return a + b * x
我们在 f 的定义中提供的关键字参数值成为默认值
f(2)
它们可以按如下方式修改
f(2, a=4, b=5)
正如我们在{ref}前一讲 <python_by_example>中讨论的,Python 函数非常灵活。
特别是
- 在给定文件中可以定义任意数量的函数。
- 函数可以(且经常)在其他函数内部定义。
- 任何对象都可以作为参数传递给函数,包括其他函数。
- 函数可以返回任何类型的对象,包括函数。
我们将在以下章节中给出将函数传递给函数是多么简单的示例。
lambda 关键字用于在一行上创建简单函数。
例如,以下定义
def f(x):
return x**3
和
f = lambda x: x**3
是完全等价的。
为了理解 lambda 为什么有用,假设我们想计算
SciPy 库有一个名为 quad 的函数可以为我们完成这个计算。
quad 函数的语法是 quad(f, a, b),其中 f 是一个函数,a 和 b 是数字。
要创建函数 lambda
from scipy.integrate import quad
quad(lambda x: x**3, 0, 2)
这里由 lambda 创建的函数被称为匿名函数,因为它从未被赋予名称。
用户自定义函数对于提高代码的清晰度非常重要,通过
- 分离不同的逻辑线索
- 促进代码重用
(将同样的事情写两遍几乎总是一个坏主意)
我们将在{doc}后面 <writing_good_code>进一步讨论这个问题。
再次考虑{doc}前一讲 <python_by_example>中的以下代码
ts_length = 100
ϵ_values = [] # 空列表
for i in range(ts_length):
e = np.random.randn()
ϵ_values.append(e)
plt.plot(ϵ_values)
plt.show()
我们将把这个程序分成两部分:
- 一个生成随机变量列表的用户自定义函数。
- 程序的主要部分,它
- 调用此函数获取数据
- 绘制数据
这在下一个程序中实现
(funcloopprog)=
def generate_data(n):
ϵ_values = []
for i in range(n):
e = np.random.randn()
ϵ_values.append(e)
return ϵ_values
data = generate_data(100)
plt.plot(data)
plt.show()
当解释器到达表达式 generate_data(100) 时,它以 n 等于 100 执行函数体。
最终结果是名称 data 被绑定到函数返回的列表 ϵ_values。
我们的函数 generate_data() 功能相当有限。
让我们通过赋予它根据需要返回标准正态分布或
这在下面的代码中实现。
(funcloopprog2)=
def generate_data(n, generator_type):
ϵ_values = []
for i in range(n):
if generator_type == 'U':
e = np.random.uniform(0, 1)
else:
e = np.random.randn()
ϵ_values.append(e)
return ϵ_values
data = generate_data(100, 'U')
plt.plot(data)
plt.show()
希望 if/else 子句的语法是不言自明的,缩进再次划定了代码块的范围。
注意
- 我们将参数
U作为字符串传递,这就是为什么我们将其写为'U'。 - 注意等号测试使用
==语法,而不是=。- 例如,语句
a = 10将名称a赋值给值10。 - 表达式
a == 10根据a的值求值为True或False。
- 例如,语句
现在,有几种方法可以简化上面的代码。
例如,我们可以完全去掉条件判断,只需将所需的生成器类型作为函数传递。
要理解这一点,请考虑以下版本。
(test_program_6)=
def generate_data(n, generator_type):
ϵ_values = []
for i in range(n):
e = generator_type()
ϵ_values.append(e)
return ϵ_values
data = generate_data(100, np.random.uniform)
plt.plot(data)
plt.show()
现在,当我们调用函数 generate_data() 时,我们将 np.random.uniform 作为第二个参数传递。
这个对象是一个函数。
当函数调用 generate_data(100, np.random.uniform) 被执行时,Python 以 n 等于 100 和名称 generator_type "绑定"到函数 np.random.uniform 来运行函数代码块。
- 在这些行被执行时,名称
generator_type和np.random.uniform是"同义词",可以以相同的方式使用。
这个原则更普遍地适用——例如,考虑以下代码
max(7, 2, 4) # max() 是 Python 内置函数
m = max
m(7, 2, 4)
这里我们为内置函数 max() 创建了另一个名称,然后可以以相同的方式使用它。
在我们程序的背景下,将新名称绑定到函数的能力意味着将函数作为参数传递给另一个函数没有任何问题——正如我们上面所做的那样。
(recursive_functions)=
这是一个进阶主题,你可以随意跳过。
同时,这是一个很好的想法,你应该在编程生涯的某个阶段学习它。
基本上,递归函数是一个调用自身的函数。
例如,考虑在某个 t 时计算
:label: xseqdoub
x_{t+1} = 2 x_t, \quad x_0 = 1
显然答案是
我们可以用循环很容易地计算这个
def x_loop(t):
x = 1
for i in range(t):
x = 2 * x
return x
我们也可以使用递归解,如下所示
def x(t):
if t == 0:
return 1
else:
return 2 * x(t-1)
这里发生的情况是每次连续调用都在栈中使用其自己的帧
- 帧是存储给定函数调用的局部变量的地方
- 栈是用于处理函数调用的内存
- 一个先进后出(FILO)队列
这个例子有些刻意,因为第一个(迭代)解通常比递归解更受欢迎。
我们以后会遇到递归的不那么刻意的应用。
(factorial_exercise)=
:label: func_ex1
回想一下,$n!$ 读作"$n$ 的阶乘",定义为
我们这里只考虑
各种模块中都有计算这个的函数,但让我们作为练习编写自己的版本。
特别地,编写一个函数 factorial,使得对任意正整数 factorial(n) 返回
:class: dropdown
这是一种解法:
def factorial(n):
k = 1
for i in range(n):
k = k * (i + 1)
return k
factorial(4)
:label: func_ex2
二项随机变量
除了 from numpy.random import uniform 之外不使用任何其他导入,编写一个函数
binomial_rv,使得 binomial_rv(n, p) 生成
:class: dropdown
如果 $U$ 在 $(0, 1)$ 上均匀分布且 $p \in (0,1)$,则表达式 `U < p` 以概率 $p$ 求值为 `True`。
:class: dropdown
这是一种解法:
from numpy.random import uniform
def binomial_rv(n, p):
count = 0
for i in range(n):
U = uniform()
if U < p:
count = count + 1 # 或者 count += 1
return count
binomial_rv(10, 0.5)
:label: func_ex3
首先,编写一个函数,返回以下随机装置的一次实现
- 掷一枚无偏硬币 10 次。
- 如果在此序列中正面连续出现
k次或更多次至少一次,支付一美元。 - 如果没有,则不支付。
其次,编写另一个函数执行相同的任务,但上述随机装置的第二条规则变为
- 如果在此序列中正面出现
k次或更多次,支付一美元。
除了 from numpy.random import uniform 之外不使用任何其他导入。
:class: dropdown
这是第一个随机装置的函数。
from numpy.random import uniform
def draw(k): # 如果序列中连续成功 k 次则支付
payoff = 0
count = 0
for i in range(10):
U = uniform()
count = count + 1 if U < 0.5 else 0
print(count) # 打印计数以便清楚
if count == k:
payoff = 1
return payoff
draw(3)
这是第二个随机装置的另一个函数。
def draw_new(k): # 如果序列中成功 k 次则支付
payoff = 0
count = 0
for i in range(10):
U = uniform()
count = count + ( 1 if U < 0.5 else 0 )
print(count)
if count == k:
payoff = 1
return payoff
draw_new(3)
在以下练习中,我们将一起编写递归函数。
:label: func_ex4
斐波那契数列定义为
:label: fib
x_{t+1} = x_t + x_{t-1}, \quad x_0 = 0, \; x_1 = 1
数列中的前几个数为
编写一个函数,对任意 t 递归计算第
:class: dropdown
这是标准解法
def x(t):
if t == 0:
return 0
if t == 1:
return 1
else:
return x(t-1) + x(t-2)
让我们测试一下
print([x(i) for i in range(10)])
:label: func_ex5
使用递归重写练习 1 中的函数 factorial()。
:class: dropdown
这是标准解法
def recursion_factorial(n):
if n == 1:
return n
else:
return n * recursion_factorial(n-1)
让我们测试一下
print([recursion_factorial(i) for i in range(1, 10)])