仅用作个人学习记录
Reference:
https://inst.eecs.berkeley.edu/~cs61a/sp21/
https://www.bookstack.cn/read/sicp-py-zh/
Lecture
本节内容并未涉及到新的知识。
Example I: Which Definition?
def f():
return 0
def g():
print(f())
def h():
def f():
return 1
g()
h()
g() 创建在全局帧中,无论在何处调用,新建的局部帧 g 的父节点都是Global。
Example II: Redefinition after Assignment
def f():
return 0
g = f
def f():
return 1
print(g())
上述过程就像:
x = 3; y = x; x = 4; print(y)
Example III: Redefinition
def f():
return 0
temp = f
def g():
print(f())
def f():
return 1
g()
Example IV: Which Definition?
def f(f):
f(1)
def g(x):
print(x)
f(g)
当我们到达f(1)内部时,调用表达式,也就是f的名字,在环境中从帧f1开始寻找,其中f的值是与g绑定的全局函数。
Example V: Which Definition?
def f():
return 0
def g():
return f()
def h(k):
def f():
return 1
p = k
return p()
print(h(g))
Example VI: Multiple Executions of Def
def f(p, k):
def g():
print(k)
if k == 0:
f(g, 1)
else:
p()
f(None, 0)
Example VII: Assign to Parameter
def f(x):
x = x + 1
y = 4
f(y)
x = 2
f(x)
print(y, x)
形参与实参的问题。
Example VIII: Assign to Outer Parameter?
def f(x):
def g(y):
x = y
g(4)
return x
print(f(3))
在对 g 的调用中,对 x 的赋值会在调用 g 所创建的局部框架中创建 x 的新绑定。它与 f 的参数无关,f 的参数绑定在不同的局部框架中。
Example IX: Delayed Recursion
def print_sums(n):
print(n)
def next_sum(k):
return print_sums(n+k)
return next_sum
print_sums(1)(3)(5)
Example X: Currying
术语currying
指的是将一个多参数函数转换为一个接受一个参数并返回一个接受下一个参数的函数,以此类推,直到消耗完最后一个参数后最终产生原函数的结果。
def curry2(f):
return lambda x: lambda y: f(x, y)
from operator import add
print(curry2(add)(30)(12)) # Prints 42
print(curry2(add)(30)) # Prints a function value;lambda y: add(30, y)
Reading
Currying
我们可以使用高阶函数将一个需要多个参数的函数转换为一连串各需要一个参数的函数。更具体地说,给定一个函数f(x, y),我们可以定义一个函数g,使g(x)(y)等同于f(x, y)。这里,g是一个高阶函数,它接收一个单参数x,并返回另一个接收单参数y的函数,这种转换称为currying
。
>>> def curried_pow(x):
def h(y):
return pow(x, y)
return h
>>> curried_pow(2)(3)
8
一些编程语言,如Haskell,只允许使用带一个参数的函数,所以程序员必须对所有多参数程序进行currying
处理。在更多的通用语言中,如Python,当我们需要一个只接受一个参数的函数时,currying
是有用的。例如,map 模式将一个单参数函数应用于一个值的序列。在后面的章节中,我们将看到map模式的更多例子,但现在我们可以在一个函数中实现这个模式。
>>> def map_to_range(start, end, f):
while start < end:
print(f(start))
start = start + 1
我们可以使用map_to_range和curried_pow来计算2的前10次方,而不是专门写一个函数来做。
>>> map_to_range(0, 10, curried_pow(2))
1
2
4
8
16
32
64
128
256
512
我们同样可以用这两个函数来计算其他数字的幂。currying
允许我们这样做,而不需要为我们想要计算的每个数字的幂写一个特定的函数。
在上面的例子中,我们手动对pow函数进行了currying
变换,得到了curried_pow。相反,我们可以定义函数来自动进行currying
,以及uncurrying
变换。
>>> def curry2(f):
"""Return a curried version of the given two-argument function."""
def g(x):
def h(y):
return f(x, y)
return h
return g
>>> def uncurry2(g):
"""Return a two-argument version of the given curried function."""
def f(x, y):
return g(x)(y)
return f
>>> pow_curried = curry2(pow)
>>> pow_curried(2)(5)
32
>>> map_to_range(0, 10, pow_curried(2))
1
2
4
8
16
32
64
128
256
512
>>> uncurry2(pow_curried)(2, 5)
32
curry2函数接收一个双参数函数f,并返回一个单参数函数g。当g应用于一个参数x时,它返回一个单参数函数h。因此,curry2(f)(x)(y)等同于f(x, y)。uncurry2函数反转了currying
,因此uncurry2(curry2(f))等同于f。
牛顿法
牛顿法是一个传统的迭代方法,用于寻找使数学函数返回值为零的参数。这些值叫做一元数学函数的零点(根)。寻找一个函数的零点通常等价于求解一个相关的数学方程。
我们知道如何计算平方根,这个事实很容易当做自然的事情。并不只是 Python,你的手机和计算机,可能甚至你的手表都可以为你做这件事。但是,学习计算机科学的一部分是弄懂这些数如何计算,而且,这里展示的通用方法可以用于求解大量方程。
牛顿方法是一种迭代改进算法:它改进了任何可微的函数的零点猜测,这意味着它可以在任何一点被直线近似。牛顿方法遵循这些线性近似来寻找函数零点。
想象经过点(x, f(x))
的一条切线。这条直线的斜率是函数值改变量与函数参数改变量的比值。所以,按照f(x)
除以这个斜率来平移x
,就会得到切线到达 x 轴时的x
值。
Newton_update表达了对一个函数 f、导数 df 来说,沿着这条切线到0的计算过程。
>>> def newton_update(f, df):
def update(x):
return x - f(x) / df(x)
return update
最后,我们可以用newton_update算法、improve算法和approx_eq算法来定义find_root函数。
>>> def improve(update, close, guess=1):
while not close(guess):
guess = update(guess)
return guess
>>> def approx_eq(x, y, tolerance=1e-15):
return abs(x - y) < tolerance
>>> def find_zero(f, df):
def near_zero(x):
return approx_eq(f(x), 0)
return improve(newton_update(f, df), near_zero)
使用牛顿法,我们可以计算任意的次方根。
- 64 的平方根是满足
square(x) - 64 = 0
的x
值。 - 更加普遍的是,a 的 n 次方根是满足
pow(x,n) - a = 0
的x
值
我们首先通过定义 f 和它的导数 df 来实现square_root。
>>> def square_root_newton(a):
def f(x):
return x * x - a
def df(x):
return 2 * x
return find_zero(f, df)
>>> square_root_newton(64)
8.0
扩展到求任意的次方根:
>>> def power(x, n):
"""Return x * x * x * ... * x for x repeated n times."""
product, k = 1, 0
while k < n:
product, k = product * x, k + 1
return product
>>> def nth_root_of_a(n, a):
def f(x):
return power(x, n) - a
def df(x):
return n * power(x, n-1)
return find_zero(f, df)
>>> nth_root_of_a(2, 64)
8.0
>>> nth_root_of_a(3, 64)
4.0
>>> nth_root_of_a(6, 64)
2.0
当你尝试使用牛顿方法时,要注意它不一定会收敛。improve函数的初始猜测guess必须足够接近零点,而且必须满足关于函数的各种条件。尽管有这个缺点,牛顿方法是解决可微分方程的一个强大的通用计算方法。对数和大整数除法的快速算法在现代计算机中采用了该技术的变种。
Abstractions and First-Class Functions
Abstractions
用户定义的函数是一种重要的抽象机制,因为它们允许我们将计算的一般方法表达为编程语言中的明确元素。
作为程序员,我们应该留意识别程序中低级抽象的机会,在它们之上构建,并泛化它们来创建更加强大的抽象。这并不是说我们应该总是以最抽象的方式编写程序;专业的程序员知道如何选择适合他们任务的抽象程度。但重要的是要能够用这些抽象的方式来思考,这样我们就可以随时在新的环境中应用它们。
高阶函数的意义在于,它使我们能够在编程语言中明确地将这些抽象概念表示为元素,从而使它们能够像其他计算元素一样被处理。
First-Class Functions
在Python中,函数可以被当作任何其他变量对待。可以把函数当成值看待。
-
函数可以绑定到名称
-
函数可以作为参数被传递
-
函数可以作为函数的返回值返回
-
函数可以被包含在数据结构中
函数装饰器
Python 提供了特殊的语法,将高阶函数用作执行def
语句的一部分,叫做装饰器。
>>> def trace1(fn):
def wrapped(x):
print('-> ', fn, '(', x, ')')
return fn(x)
return wrapped
>>> @trace1
def triple(x):
return 3 * x
>>> triple(12)
-> <function triple at 0x102a39848> ( 12 )
36
这个例子中,定义了高阶函数trace1
,它返回一个函数,这个函数在调用它的参数(return)之前执行print
语句来输出参数。triple
的def
语句拥有一个注解@trace1
,它会影响def
的执行规则。像通常一样,函数triple
被创建了,但是,triple
的名称并没有绑定到这个函数上,而是绑定到了在使用新定义的函数triple
作为参数调用trace1
的返回值上。在代码中,这个装饰器等价于:
>>> def triple(x):
return 3 * x
>>> triple = trace1(triple)
装饰符@后面也可以跟上一个调用表达式。@ 后面的表达式首先求值(就像上面求值 trace 一样),其次是 def 语句,最后将装饰器表达式的求值结果应用于新定义的函数,并将结果绑定到def 声明。
>>> def log(text):
def trace1(fn):
def wrapped(x):
print(text)
print('-> ', fn, '(', x, ')')
return fn(x)
return wrapped
return trace1
>>> @log('execute')
def triple(x):
return 3 * x
>>> triple(12)
execute
-> <function triple at 0x00000283FDFC89D0> ( 12 )
36
def triple(x):
return 3 * x
triple = log('execute')(triple)