Skip to content

实验 2:高阶函数,Lambda 表达式 lab02.zip

MOLI:本次实验你将会学到什么

  • Python 短路求值
  • Python 高阶函数
  • Python lambda 表达式用法

开始文件:

下载 lab02.zip

主题

你认为如果我们在 Python 中输入以下内容会发生什么?

1 / 0

在 Python 中尝试一下!你应该会看到一个 ZeroDivisionError 错误。但这个表达式呢?

True or 1 / 0

它的结果是 True,因为 Python 中的 andor 操作符是 短路求值。也就是说,他们不一定会计算每个操作数。

操作符 检查是否: 从左到右计算结果直到: 示例
AND 所有值为真 第一个假值 False and 1 / 0 结果是 False
OR 至少一个值为真 第一个真值 True or 1 / 0 结果是 True

短路发生在操作符遇到一个能够让他们得出表达式结果的操作数时。例如,and 会在遇到第一个假值时短路,因为它知道并非所有值都为真。

如果 andor 不是 短路求值,他们将直接返回最后一个值;另一种记住这一点的方法是,andor 总是返回他们计算的最后一个值,无论他们是否短路。记住,当使用 TrueFalse 以外的值时,andor 不一定总是返回布尔值。

如果你需要复习本实验的材料,可以参考这一部分。你可以直接跳到问题部分,并在卡住的时候再回来查看这里的内容。

短路求值

短路求值

你认为如果我们在 Python 中输入以下内容会发生什么?
1 / 0
在 Python 中尝试一下!你应该会看到一个 ZeroDivisionError 错误。但这个表达式呢?
True or 1 / 0
它的结果是 True,因为 Python 中的 andor 操作符是短路求值。也就是说,他们不一定会计算每个操作数。
操作符 检查是否: 从左到右计算结果直到: 示例
AND 所有值为真 第一个假值 False and 1 / 0 结果是 False
OR 至少一个值为真 第一个真值 True or 1 / 0 结果是 True
短路发生在操作符遇到一个能够让他们得出表达式结果的操作数时。例如,`and` 会在遇到第一个假值时短路,因为它知道并非所有值都为真。 如果 andor 不是短路求值,他们将直接返回最后一个值;另一种记住这一点的方法是,andor 总是返回他们计算的最后一个值,无论他们是否短路。记住,当使用 TrueFalse 以外的值时,andor 不一定总是返回布尔值。
高阶函数

高阶函数

变量是绑定到值的名称,这些值可以是原始数据类型,比如 3'Hello World',也可以是函数。因为函数可以接受任何类型的参数,所以其他函数也可以作为参数传递。这就是高阶函数的基础。 高阶函数是操作其他函数的函数,通过接受函数作为参数、返回一个函数,或者两者兼有。在本实验中,我们将介绍高阶函数的基础知识,并将在下一个实验中探索高阶函数的更多应用。 函数作为参数: 在 Python 中,函数对象是可以传递的值。我们知道,创建函数的一种方法是使用 def 语句:
def square(x):
    return x * x
上述语句创建了一个内部名称为 square 的函数对象,并将其绑定到当前环境中的名称 square。现在让我们尝试将其作为参数传递。 首先,我们编写一个接受另一个函数作为参数的函数:
def scale(f, x, k):
    """ Returns the result of f(x) scaled by k. """
    return k * f(x)
我们现在可以在 scale 上调用,并使用 square 和其他参数
>>> scale(square, 3, 2)  # Doubke square(3)
18
>>> scale(square, 2, 5)  # 5 times 2 squares
20
注意,在对 scale 的调用中,拥有内部名称 square 的函数对象被绑定到参数 f 上。然后,我们通过调用 f(x) 来调用 square。 正如我们在上面关于 lambda 表达式的部分所看到的,我们还可以将 lambda 表达式传递到调用表达式中!
>>> scale(lambda x: x + 10, 5, 2)
30
在此调用表达式的框架中,名称 f 被绑定到由 lambda 表达式 lambda x: x + 10 创建的函数。 返回函数的函数: 因为函数是值,他们也可以作为返回值!下面是一个例子:
def multiply_by(m):
    def multiply(n):
        return n * m
    return multiply
在这个例子中,我们在 multiply_by 的函数体内定义了 multiply 函数,然后返回了它。让我们看看它的运行效果:
>>> multiply_by(3)
<function multiply_by.<locals>.multiply at ...>
>>> multiply(4)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'multiply' is not defined
调用 multiply_by 返回了一个函数,正如预期的那样。然而,调用 multiply 时却发生了错误,尽管我们给了它这个名字。这是因为 multiply 这个名称只在我们评估 multiply_by 函数体时存在。 那么我们如何实际使用内部函数呢?这里有两种方法:
>>> times_three = multiply_by(3) # Assign the result of the call expression to a name
>>> times_three(5) # Call the inner function with its new name
15
>>> multiply_by(3)(10) # Chain together two call expressions
30
关键是,由于 multiply_by 返回了一个函数,你可以像使用任何其他函数一样使用它的返回值。
Lambda 表达式

Lambda 表达式

Lambda 表达式是通过指定两项内容来求值为函数的表达式:参数和返回表达式。
lambda <parameters>: <return expression>
虽然 lambda 表达式和 def 语句都创建函数对象,但他们之间有一些显著的区别。lambda 表达式像其他表达式一样工作;就像数学表达式评估为一个数字并且不会改变当前环境一样,lambda 表达式评估为一个函数而不会改变当前环境。让我们仔细看看:
类型 lambda 表达式 def 语句
类型 评估为一个值的表达式 改变环境的语句
执行结果 创建一个没有内在名称的匿名 lambda 函数 创建一个带有内在名称的函数并将其绑定到当前环境中的名称
对环境的影响 评估 lambda 表达式不会创建或修改任何变量 执行 def 语句时会创建一个新的函数对象并将其绑定到当前环境中的名称
用法 lambda 表达式可以在任何需要表达式的地方使用,例如赋值语句中或作为调用表达式的操作数或操作符 执行 def 语句后,创建的函数会绑定到一个名称。你应该使用这个名称来引用该函数,任何需要表达式的地方都可以使用这个名称
示例

# A lambda expression by itself does not alter
# the environment
lambda x: x * x

# We can assign lambda functions to a name
# with an assignment statement
square = lambda x: x * x
square(3)

# Lambda expressions can be used as an operator
# or operand
negate = lambda f, x: -f(x)
negate(lambda x: x * x, 3)
        

def square(x):
    return x * x

# A function created by a def statement
# can be referred to by its intrinsic name
square(3)
        
环境图

环境图

环境图是理解 lambda 表达式和高阶函数的最佳学习工具之一,因为它可以帮助你跟踪所有不同的名称、函数对象以及函数的参数。如果你在做 “Python 会显示什么?”问题时遇到困难,我们强烈建议绘制环境图或使用 Python Tutor。关于环境图应该是什么样子的,你可以尝试在 Python Tutor 中运行一些代码作为示例。以下是一些规则:

赋值语句

  1. 评估等号右边的表达式。
  2. 如果等号左边的名称在当前框架中尚不存在,则将其写入;如果已经存在,则删除当前的绑定。将第 1 步获得的值绑定到这个名称上。
  3. 如果语句中有多个名称/表达式,先从左到右依次评估所有的表达式,然后再进行绑定。

如果语句中有多个名称/表达式,先从左到右依次评估所有的表达式,然后再进行绑定。

定义语句

  1. 绘制函数对象,包括其固有名称、形式参数和父框架。函数的父框架是定义该函数的框架。
  2. 如果函数的固有名称在当前框架中尚不存在,则将其写入;如果已经存在,则删除当前的绑定。将新创建的函数对象绑定到这个名称上。

调用表达式

注意:对于像 maxprint 这样的内置 Python 函数,你不需要经过这个过程。

  1. 评估操作符,其值应该是一个函数。
  2. 从左到右评估操作数。
  3. 打开一个新框架,并标注框架的顺序编号、函数的固有名称以及其父框架。
  4. 将函数的形式参数绑定到你在第 2 步中找到的参数值。
  5. 在新的环境中执行函数体。

Lambda 表达式

注意:正如我们在上面的 lambda 表达式部分所看到的,lambda 函数没有固有名称。在环境图中绘制 lambda 函数时,他们被标记为 lambda 或小写希腊字母 λ。当环境图中有多个 lambda 函数时,这可能会造成混淆,因此你可以通过编号或者写出定义他们的行号来区分他们。

  1. 绘制 lambda 函数对象,并标注为 λ、其形式参数以及其父框架。函数的父框架是定义该函数的框架。

这是唯一的步骤。我们包含这一部分是为了强调 lambda 表达式与 def 语句之间的区别:lambda 表达式不会在环境中创建任何新的绑定。

需要回答的问题

Python 会显示什么?

问题 1: WWPD: 真理会胜利

使用 Ok 来测试你的知识,以下是一些“Python 会显示什么?”的问题:

python3 ok -q short-circuit -u --local

>>> True and 13
______

>>> False or 0
______

>>> not 10
______

>>> not None

>>> True and 1 / 0
______

>>> True or 1 / 0
______

>>> -1 and 1 > 0
______

>>> -1 or 5
______

>>> (1 + 1) and 1
______

>>> print(3) or ""
______
>>> def f(x):
...     if x == 0:
...         return "zero"
...     elif x > 0:
...         return "positive"
...     else:
...         return ""
>>> 0 or f(1)
______

>>> f(0) or f(-1)
______

>>> f(0) and f(-1)
______

问题 2: WWPD: 高阶函数

使用 Ok 来测试你的知识,以下是一些“Python 会显示什么?”的问题:

python3 ok -q hof-wwpd -u --local

>>> def cake():
...    print('beets')
...    def pie():
...        print('sweets')
...        return 'cake'
...    return pie
>>> chocolate = cake()
______

>>> chocolate
______

>>> chocolate()
______

>>> more_chocolate, more_cake = chocolate(), cake
______

>>> more_chocolate
______

>>> def snake(x, y):
...    if cake == more_cake:
...        return chocolate
...    else:
...        return x + y
>>> snake(10, 20)
______

>>> snake(10, 20)()
______

>>> cake = 'cake'
>>> snake(10, 20)
______

问题 3: WWPD: Lambda

使用 Ok 来测试你的知识,以下是一些“Python 会显示什么?”的问题:

python3 ok -q lambda -u --local

提醒一下,以下两行代码执行时将不会在交互式 Python 解释器中显示任何输出:

>>> x = None
>>> x
>>>

>>> lambda x: x  # A lambda expression with one parameter x
______

>>> a = lambda x: x  # Assigning the lambda function to the name a
>>> a(5)
______

>>> (lambda: 3)()  # Using a lambda expression as an operator in a call exp.
______

>>> b = lambda x, y: lambda: x + y  # Lambdas can return other lambdas!
>>> c = b(8, 4)
>>> c
______

>>> c()
______

>>> d = lambda f: f(4)  # They can have functions as arguments as well.
>>> def square(x):
...     return x * x
>>> d(square)
______
>>> higher_order_lambda = lambda f: lambda x: f(x)
>>> g = lambda x: x * x
>>> higher_order_lambda(2)(g)  # Which argument belongs to which function call?
______

>>> higher_order_lambda(g)(2)
______

>>> call_thrice = lambda f: lambda x: f(f(f(x)))
>>> call_thrice(lambda y: y + 1)(0)
______

>>> print_lambda = lambda z: print(z)  # When is the return expression of a lambda expression executed?
>>> print_lambda
______

>>> one_thousand = print_lambda(1000)
______

>>> one_thousand # What did the call to print_lambda return?
______

代码练习

问题 4: 复合恒等函数

编写一个函数,该函数接收两个单参数函数 fg,并返回另一个函数,该函数具有一个单一参数 x。返回的函数应当返回 True,如果 f(g(x)) 等于 g(f(x)),否则返回 False。你可以假设 g(x) 的输出是 f 的有效输入,反之亦然。

def composite_identity(f, g):
    """
    Return a function with one parameter x that returns True if f(g(x)) is
    equal to g(f(x)). You can assume the result of g(x) is a valid input for f
    and vice versa.

    >>> add_one = lambda x: x + 1        # adds one to x
    >>> square = lambda x: x**2          # squares x [returns x^2]
    >>> b1 = composite_identity(square, add_one)
    >>> b1(0)                            # (0 + 1) ** 2 == 0 ** 2 + 1
    True
    >>> b1(4)                            # (4 + 1) ** 2 != 4 ** 2 + 1
    False
    """
    "*** YOUR CODE HERE ***"

使用 Ok 去测试你的代码:

python3 ok -q composite_identity --local

问题 5: 条件计数

考虑以下 count_fivescount_primes 函数的实现,这些实现使用了之前作业中的 sum_digitsis_prime 函数:

def count_fives(n):
    """Return the number of values i from 1 to n (including n)
    where sum_digits(n * i) is 5.

    >>> count_fives(10)  # Among 10, 20, 30, ..., 100, only 50 (10 * 5) has digit sum 5
    1
    >>> count_fives(50)  # 50 (50 * 1), 500 (50 * 10), 1400 (50 * 28), 2300 (50 * 46)
    4
    """
    i = 1
    count = 0
    while i <= n:
        if sum_digits(n * i) == 5:
            count += 1
        i += 1
    return count

def count_primes(n):
    """Return the number of prime numbers up to and including n.

    >>> count_primes(6)   # 2, 3, 5
    3
    >>> count_primes(13)  # 2, 3, 5, 7, 11, 13
    6
    """
    i = 1
    count = 0
    while i <= n:
        if is_prime(i):
            count += 1
        i += 1
    return count

这些实现看起来非常相似!通过编写一个通用函数 count_cond,我们可以将其进行抽象化,count_cond 接收一个两参数的谓词函数 condition(n, i),并返回一个接受一个参数 n 的函数,该函数会计算满足条件的数字数量。

说明:当我们说 condition 是一个谓词函数时,我们的意思是这个函数会返回 TrueFalse

def sum_digits(y):
    """Return the sum of the digits of non-negative integer y."""
    total = 0
    while y > 0:
        total, y = total + y % 10, y // 10
    return total

def is_prime(n):
    """Return whether positive integer n is prime."""
    if n == 1:
        return False
    k = 2
    while k < n:
        if n % k == 0:
            return False
        k += 1
    return True

def count_cond(condition):
    """Returns a function with one parameter N that counts all the numbers from
    1 to N that satisfy the two-argument predicate function Condition, where
    the first argument for Condition is N and the second argument is the
    number from 1 to N.

    >>> count_fives = count_cond(lambda n, i: sum_digits(n * i) == 5)
    >>> count_fives(10)   # 50 (10 * 5)
    1
    >>> count_fives(50)   # 50 (50 * 1), 500 (50 * 10), 1400 (50 * 28), 2300 (50 * 46)
    4

    >>> is_i_prime = lambda n, i: is_prime(i) # need to pass 2-argument function into count_cond
    >>> count_primes = count_cond(is_i_prime)
    >>> count_primes(2)    # 2
    1
    >>> count_primes(3)    # 2, 3
    2
    >>> count_primes(4)    # 2, 3
    2
    >>> count_primes(5)    # 2, 3, 5
    3
    >>> count_primes(20)   # 2, 3, 5, 7, 11, 13, 17, 19
    8
    """
    "*** YOUR CODE HERE ***"

使用 Ok 去测试你的代码:

python3 ok -q count_cond --local

本地查看分数

你可以通过运行以下命令本地查看每个问题的分数:

python3 ok --score --local

环境图练习(附加题)

问题 6: 高阶函数环境图练习

绘制执行下面代码时生成的环境图,可以在纸上或白板上完成。使用 tutor.cs61a.org 来检查你的作业。

n = 7

def f(x):
    n = 8
    return x + 1

def g(x):
    n = 9
    def h():
        return x + 1
    return h

def f(f, x):
    return f(x + n)

f = f(g, n)
g = (lambda y: y())(f)

可选问题

这些问题是可选的。如果你没有完成他们,你仍然会获得此作业的学分。他们是很好的练习,所以还是做一下吧!

问题 7: 最小公倍数

编写一个函数,该函数接收两个数字并返回他们的最小公倍数。

def multiple(a, b):
    """Return the smallest number n that is a multiple of both a and b.

    >>> multiple(3, 4)
    12
    >>> multiple(14, 21)
    42
    """
    "*** YOUR CODE HERE ***"

使用 Ok 去测试你的代码:

python3 ok -q multiple --local

问题 8: 我听说你喜欢函数...

定义一个函数 cycle,该函数接收三个函数 f1f2f3 作为参数。cycle 应返回另一个函数 gg 应接收一个整数参数 n 并返回另一个函数 h。最终的函数 h 应接收一个参数 x,并根据 n 的值循环地将 f1f2f3 应用到 x 上。以下是对于一些 n 值,最终函数 h 应如何对 x 执行操作:

  • n = 0, 返回 x
  • n = 1, 对 x 应用 f1,即返回 f1(x)
  • n = 2, 对 x 应用 f1 然后再对结果应用 f2,即返回 f2(f1(x))
  • n = 3, 对 x 应用 f1,然后对 f1 的结果应用 f2,再对 f2 的结果应用 f3,即返回 f3(f2(f1(x)))
  • n = 4, 从头开始循环,先应用 f1,然后应用 f2,再应用 f3,最后再应用 f1,即返回 f1(f3(f2(f1(x))))
  • 依此类推。

提示:大部分工作都在最内层的函数中完成。

提示:你如何利用 % 操作符来实现循环行为?尝试计算 n % 3,对于从 0 到 12 的所有整数 n,你会发现什么模式?

def cycle(f1, f2, f3):
    """Returns a function that is itself a higher-order function.

    >>> def add1(x):
    ...     return x + 1
    >>> def times2(x):
    ...     return x * 2
    >>> def add3(x):
    ...     return x + 3
    >>> my_cycle = cycle(add1, times2, add3)
    >>> identity = my_cycle(0)
    >>> identity(5)
    5
    >>> add_one_then_double = my_cycle(2)
    >>> add_one_then_double(1)
    4
    >>> do_all_functions = my_cycle(3)
    >>> do_all_functions(2)
    9
    >>> do_more_than_a_cycle = my_cycle(4)
    >>> do_more_than_a_cycle(2)
    10
    >>> do_two_cycles = my_cycle(6)
    >>> do_two_cycles(1)
    19
    """
    "*** YOUR CODE HERE ***"

使用 Ok 去测试你的代码:

python3 ok -q cycle --local