发表在

函数

作者
  • avatar
    名字
    Wisdom Keeper
    Twitter

引言

诶,我点进来之前的时候就觉得奇怪了,为什么编程入门之夏是从函数开始讲的,我学Java,函数都在后面的章节了呀 (WKU新生编程入门所用教材安排在第六章),首先呢,我们 借鉴 CS 61a 的课程安排,而在 CS 61a 中贯穿整个课程的便是抽象,我们没必要一来就以解决问题为方向,虽然这个阶段,你不能使用编程语言解决问题,但你能对编程有一个 更深入的了解,且Python接近人类自然语言的语法,动态语言的类型,凭借你的自觉就也能写一些东西,我们相信这也是伯克利这样的高校课程安排的原因吧。


函数

点击 检查点 ⏭️ 到 对应视频位置,因为b站高清晰度需要登陆,建议在这里勾选检查点后跳转到对应位置后再点击b站登陆观看

表达式是编程语言中的基本构建块。

很重要

为 DQ 1 做准备

很重要的课题,会在群里讨论,或以后专开一节

总结了表达式和语句的基本概念。

非常重要的一个DQ ‼️‼️‼️

Environment diagram 主要用于教学和调试,帮助理解程序的执行过程。 知识,哪个高校教的都一样,我们看伯克利这类高校的课,主要学的就是他们一些分析问题的方法,philosophy 以及一些 idioms

赋值语句用于将值赋给变量。

调用表达式的结构是理解函数调用的关键。

提供了第一个讨论问题的解决方案。

定义函数是编程中的重要部分。

在环境中查找名称是理解作用域的关键。



Python 入门教程 - 语句与表达式

Python 代码由表达式和语句组成。广义上讲,计算机程序包括以下两类指令:

  1. 表达式: 计算某个值
# python, 注意 >>> 代表在 python console 中的输入,后文不再赘述
>>> 2 + 2
  1. 语句:执行某个操作(即产生副作用,例如在 Java 中的 void(字如其意,为不返回任何东西的方法)
# python, 注意 >>> 代表在 python console 中的输入,后文不再赘述
>>> we = "WKU SRA" # Assignment statement, 赋值语句
>>> print(we) # Print statement, 输出语句

你可以观察到上述均执行一个操作

其实到这里区别已经很明显了,如果你已经完全理解了这两个概念,那么你可以跳过这一部分,直接跳转 names 小节。

表达式

表达式,你可以把他看成是编程语言中的基本原子 ⚛️,构建块。

调用表达式

  • 函数: 一个定义了特定操作或计算的小型机器,它接受输入并产生输出。
  • 调用表达式: 一个表达式,它调用一个函数。由一个函数表达式和一组参数表达式组成。
# 函数定义
def add(a, b):
    return a + b

# 调用表达式
result = add(2, 3)  # 这里的 add(2, 3) 就是一个调用表达式
print(result)  # 输出 5

有啥区别,我感觉这俩个不都是一个东西啊,为什么要分开说呢? 首先不抬杠,我们来看看这个例子,add(2, 3) 这个调用表达式,它调用了一个函数 add,这个函数接受两个参数 23,然后返回了一个值 5,这个值被赋给了 result,然后被输出。 你发现了吗,他在计算一个值,而 def add(a, b): 这是个函数定义,他是一个操作,他只是定义了一个操作,没有调用也没干任何事情。 所以呢,其实就是文字游戏,没必要去记

All expression can be functions call, but not all functions call are expressions.

所有表达式都能被用于函数调用,但不是所有函数调用都是表达式

Call expression is a expression that calls a function. It consists of a function expression that produces a function value and a list of expressions that produce the arguments to the function. The function expression is evaluated first, and then the arguments are evaluated from left to right. The function value is then called with the arguments, and the call expression evaluates to the result of the function call.

调用表达式是调用函数的表达式。它由一个生成函数值的函数表达式和一个生成函数参数的表达式列表(参数列表)组成。 首先计算函数表达式,然后从左到右计算参数。然后使用这些参数调用函数值,调用表达式的结果就是函数调用的结果。

  • 表达式可以被用于函数调用
>>> max(1,2) # Call expression is a expression that calls a function.
2 #  It consists of a function expression that produces a function value
>>> max(max(3,4),min(1,2)) # and a list of expressions that produce the arguments to the function.
4

  • 但不是所有函数都是表达式
>>> print("WKU SRA")# 执行了一个操作,但是没有返回值,他没有直接参与计算(内部计算不考虑),所以不是表达式

非纯粹的 print 函数

我们将区分两种类型的函数。

纯函数。函数有一些输入(它们的参数)并返回一些输出(应用它们的结果)。

>>> def pure_func()
...     return 2
...     #回车,这里只做演示,如何使用交互式解释器,下文不再赘述
>>> pure_func()
2 //如你所见,他返回了一个值,没有任何副作用(side effect)是纯函数
// Java
public int pureFunction(int a){
    return a;
}
//如你所见,他返回了一个字,没有任何副作用,对java

Quiz: 纯函数|请结合昨天的拓展课程,判断以下函数是否为纯函数

we = "WKU SRA"

def func(a, b):
    we = "SRA" # 很迷惑人的地方,这里和python的作用域有关
    return a + b

是纯函数吗?

we = "WKU SRA"

def func():
    global we  # 声明we为全局变量
    we = "Modified SRA"  # 修改全局变量we的值

func()

是纯函数吗?

会在群里公布及评论区同时公布答案

非纯粹函数。除了返回一个值之外,应用非纯函数还可以产生副作用,这些副作用会对解释器或计算机的状态进行某些更改。常见的副作用是使用 print 函数生成超出返回值的额外输出。

>>> print(1, 2, 3)
1 2 3

print 返回的值始终是 None,这是一个表示没有任何内容的特殊 Python 值。交互式 Python 解释器不会自动打印值 None。 在 print 的情况下,函数本身在被调用时作为副作用打印输出。

>>> print(print(1), print(2))
1
2
None None

如果你发现这个输出出乎意料,请绘制一个表达式树来澄清为什么评估这个表达式会产生这种奇特的输出。

纯函数的限制在于它们不能有副作用或随时间改变行为。施加这些限制会带来实质性好处。首先,纯函数可以更可靠地组合成复合调用表达式。 其次,纯函数通常更易于测试。参数列表始终会导致相同的返回值,这可以与预期返回值进行比较。测试将在本章稍后详细讨论。 第三,第四章将说明纯函数对于编写并发程序是必不可少的,因为其不修改外部状态,因此多个线程可以安全地调用纯函数而不需要担心数据竞争或死锁。

举个 🌰

  • 我们假设食死徒(异步程序)的咒语 Avada Kedavra 阿瓦达啃大瓜 只能通过伏地魔(计算机)的魔杖(外部状态)才能使用
  • 伏地魔下达生死决斗的任务(调用函数,并传入决斗参数)
  • 我们已知道触发Avada Kedavra的前提是得到魔杖
  • 有俩个食死徒都要同时使用这个咒语,但是只有一个魔杖

这是一场食死徒之间的对决,他们不会等待对方,而是同时发动攻击(异步),而他们争夺魔杖的过程中就是对外部状态进行改变的过程,他们都在改变魔杖使用者的状态。 这就是racing condition即竞态,我们不知道谁能先拿到魔杖,而这种不确定性是伏地魔(计算机)最讨厌的。伏地魔通过预言得知,只有食死徒A 战死,哈利才不会降生, 但是突然你告诉伏地魔(计算机),我不能保障这场战斗的结果(计算结果),伏地魔便只能无能狂怒(抛出异常),终止他的计划(程序)。

但是在纯函数世界一切都不一样了

  • 我们假设食死徒(异步程序)的咒语 Avada Kedavra 阿瓦达啃大瓜 无需通过伏地魔(计算机)的魔杖(外部状态)才能使用,即不改变外部状态
  • 伏地魔下达生死决斗的任务(调用函数,并传入决斗参数)
  • 俩个食死徒各自拥有属于自己的魔杖(各自维持一个Avada Kedavra 阿瓦达啃大瓜纯函数)

纯函数接受决斗对手俩个参数,假设Avada Kedavra 阿瓦达啃大瓜内有一套复杂的逻辑,通过读取自身信息与对手信息,便能知道调用者的生死,那么输出结果就是确定的, 不会因为外部状态的改变而改变,伏地魔就很高兴了,因为只要知道是谁在决斗就知道结果,那么后面一系列事也就能安排了。

异步

Bonus, 会在群里讲解

内嵌表达式

来看看嵌套表达式,嵌套表达式是指一个表达式中包含另一个表达式。在 Python 中,嵌套表达式的求值顺序是从内到外的。也就是说,内部表达式首先被求值,然后外部表达式被求值。其遵守的规则是:

  1. 评估(evaluate)操作符(operator)和操作数子(operand)表达式,然后
  2. 将作为操作符子表达式值的函数应用于作为操作数子表达式值的参数。

这里我们用>sth<表示对sth求值

即函数表达式(执行函数操作,直观可理解执行 >函数名< )首先被求值(evaluated),然后参数从左到右依次被求值。接着,函数值会使用这些参数被调用,并且调用表达式的求值结果就是函数调用的结果。

求值过程本质上是递归的,也就是说,它会调用其自身作为步骤之一。递归的讲解会在后面涉及到,下面为简短的过程描述

>>> max(max(9,1), min(2,3),3)
 1. evaluate operator &gt;max&lt;(max(9,1), min(2,3),3)
 2. evaluate operand 评估操作数 ---> max(&gt;max(9,1), min(2,3),3&lt;) 评估操作数,不能得到值(递归退出点),依参数从左到右依次求值进行递归操作
    1. evaluate operator  评估操作符 ---&gt; &gt;max&lt;(9,1)
    2. evaluate operand 评估操作数 ---&gt; max(&gt;9,1&lt;),能得到值,即到达递归退出点
    3. apply operator to operand 将操作符应用于操作数
          返回 9,第一个递归操作结束,
     1. evaluate operator 评估操作符 ---&gt; &gt;min&lt;(2,3)
     2. evaluate operand 评估操作数 ---&gt; min(&gt;2,3&lt;),能得到值,即到达递归退出点
     3. apply operator to operand 将操作符应用于操作数
          返回 2,第二个递归操作结束,
     1. 无操作符
     2. evaluate operand 评估操作数 &gt;3&lt; 能得到值,到达递归退出点,立刻退出
     3.    返回 3,第三个递归操作结束
 3. evaluate operand 评估操作数 max(&gt;9,2,3&lt;) 能得到值,即到达递归退出点
 4. apply operator to operand 将操作符应用于操作数
    返回 9

下面我们再用中缀运算符 +/+-*/ 解释一遍

(2+(34))(2 + (3 * 4))

解释器会这样处理:

识别到这是一个加法操作,需要对两个操作数求值。

第一个操作数是 22,这是一个基本情况,直接返回 22

第二个操作数是 (34)(3 * 4),这是一个嵌套表达式,需要进一步求值。

(34)(3 * 4) 进行求值,识别到这是一个乘法操作,需要对两个操作数求值。

两个操作数 3344 都是基本情况,直接返回 3344

3344 进行乘法运算,得到 1212

221212 进行加法运算,得到最终结果 1414

哈哈,有没有发现很神奇,对的,这里运用了递归的思想。递归是一个由主任务一步一步分解(也可以理解我评估,evaluate)成子任务的过程。

我们初步在这里提一嘴:

  1. 递归的前提是有终止条件(其实这也是所有算法的前提,即finite,算法不能无限进行下去)

    In our case: 当我们的表达式中没有表达式时即编译器找不到继续求值的东西,既然一无所有,便及时止损吧,不能一直等那个永远不会出现的表达式直至笔电电量耗尽吧 我们的表达式就可以得到一个值,这个值就是我们的终止条件。

  2. 递归要求父任务和子任务之间有相同的逻辑,这样才能一步一步分解,调用本身,直至终止条件。

    In our case: 专一的编译器,在执行表达式的时候,只干求值这一件事。 但是为什么要求父元素和子任务之间有相同的逻辑呢? 这里用伪代码来解释一下:

def sum(a):
    如果 a 比 0:
        返回 a + sum(a-1)
    否则:
        返回 0

这里我们可以看到,我们的父任务和子任务之间有相同的逻辑,都是求和,只是子任务的参数变小了,这样我们就可以一步一步的分解,直至终止条件。

问:以下是否为递归函数?

def mul(a):
    如果 a 比 0:
        返回 a + mul(a-1)
    否则:
        返回 0

答案将在群和评论区公布

计算

>>> mul(add(2, mul(4, 6)), add(3, 5))

说实话,61a 的教授讲的很清楚了,详情请自行观看 DQ 1 solution

注:请至少观看完本节视频 environment diagram 后再使用 visualizer进行可视化




评估其根mul(add(2, mul(4, 6)), add(3, 5))(root),即顶部的完整表达式,需要首先评估其子表达式add(2, mul(4, 6))``add(3, 5)的分支。叶子(leaf)表达式 mul add 2 4 6 3 5(即,没有分支从它们延伸出来的节点)表示函数或数字。内部节点(node)有两部分:我们应用评估规则的调用表达式add(3, 5),以及该表达式的结果8。以树的角度来看待评估,我们可以想象操作数的值从终端节点开始向上渗透,然后在越来越高的层次上结合。

接下来,观察第一步的重复应用将我们带到需要评估的地方,而不是调用表达式,而是原始表达式,如数字(例如,2)和名字(例如,add)。我们通过规定来处理原始情况:

当我们说“一个数字表达式求值为一个数字”时,我们实际上是指 Python 解释器将数字表达式求值为一个数字。是解释器赋予了编程语言以意义。鉴于解释器是一个固定的程序,总是表现一致,我们可以说在 Python 程序的上下文中,数字(和表达式)本身求值为值。 如上所见,表达式用于计算值。对于表达式和语句的区别也就在这里,表达式可以是值如:1 "1" boolean 函数调用如:max(1,2)

语句

语句通常描述操作。当 Python 解释器执行一个语句时,它会执行相应的操作。另一方面,表达式通常描述计算,而语句通常不产生值,或者产生的值不是其主要目的,他是为了执行一个动作或改变程序的状态。

>>> x = 42  # 赋值语句 assignment statement, 42 is an expression, x is a name, = is an operator,
>>> print(x)  # 输出语句 print statement
>>> if x > 0:  # 条件语句 conditional statement
...     print("Positive")
>>> for i in range(5):  # 循环语句 loop statement
...     print(i)

最后总结一下,其实没必要死记什么是表达式,什么是语句,只要记住表达式是计算结果的代码片段,表达式一般会产生一些东西,我们关心谁要这些计算结果(这个返回值可以被赋给一个变量,作为另一个表达式的一部分,或者被直接使用)。 而语句很独立,他执行某个操作的代码片段就好了,不需要上下文。

函数

def live_in_wku():
    # 吃饭
    # 睡觉
    # 打豆豆

函数封装了用于操作数据的逻辑。我们生活在wku,每天有着各自各样的事情(一个又一个unit),而每个事情内部又是各种复杂的逻辑。于是我们把例如吃饭,睡觉,打豆豆的这些操作封装到一个函数中,我们没有必要去 关心函数内部发生了什么,他对于我们来说就是个黑箱,作为程序员,学会调用别人写好的函数就行了,而作为函数库作者要考虑的就很多了 😎。

导入库函数

Python 定义了大量函数,包括前面部分提到的运算符函数,但默认情况下并不使所有这些名称都可用。 相反,它将其已知的函数和其他变量组织到模块(module)中,这些模块共同构成了 Python 库。 要使用这些元素,需要导入它们。例如,math 模块提供了各种熟悉的数学函数:

为什么要把这些东西放到module呢?你说Python同学怎么这样啊,就知道给我们增加负担 😡 天天 import,我python没啥学,几周下来就from x import y as z了。 咱CS也要有自己的abandon 🤓

哈哈哈,当然不是这样的呀,谁不想设计一个程序或计算机语言能提供堪呼完美的易用性(utility first)呢?但是我们要知道工程学里面很重要的一个概念就是trade off。 权衡,我们在这里权衡的是什么呢?引入模块,虽然我们能把所有函数引入文件,不用一个一个去import并处理打包,但是 import 这些模块完全可以由ide代劳,我们不会去记忆这些 模块进行手动导入,这是毫无意义的,且打包工具的完善,也不用我们去手动处理。所以其实,在ide的加持下module系统并没有太多的缺陷,但我们通过隔离命名空间,避免了很多命名冲突,也更容易维护和更新, 因为修改一个模块不会影响到其他模块。

python把这些东西放到module里,你需要的时候导入就好了,这样既不会污染全局变量,也不会有命名冲突。

math 模块提供了常见的数学运算函数:

>>> from math import sqrt
>>> sqrt(256)
16.0

operator 模块提供了与中缀运算符对应的函数:

>>> from operator import add, sub, mul
>>> add(14, 28)
42
>>> sub(100, mul(7, add(8, 4)))
16

import 语句指定模块名称(例如,operatormath),然后列出要导入的该模块的命名属性(例如,sqrt)。一旦函数被导入,它可以被多次调用。

也就是说,他们被放入了当前作用域的命名空间中

使用这些运算符函数(例如,add)与使用运算符符号本身(例如,+)之间没有区别。通常,大多数程序员使用符号和中缀表示法(+``-``*``/)来表达简单的算术运算。

调用用户定义的函数

函数相当于绑定(bind)表达式到名字(name)上,自带函数和自定义函数无本质区别,此处目的是为了告诉大家如何构建函数

函数定义包含 def 语句,它标明了 <name> (名称)和一列带有名字的 <formal parameters> (形式参数)。之后, return (返回)语句叫做函数体,指定了函数的 <return expression> (返回表达式),它是函数无论什么时候调用都需要求值的表达式。

def <name>(<formal parameters>):#signature
   return <return expression>#body,第一层缩近,为其他语言第一层代码块{}

Function signature indicate how many args a funcitno take Body defines the computation process expressed by a function

请注意,在python中缩进为类似其他语言的的{}共同表示代码块,代码块是指一组逻辑上相关的代码, 它们被组织在一起以执行特定的任务或功能。

Procedure to execute a user defined function

  1. create a new frame
  2. bind the formal parameters to the arguments
  3. execute the body of the function in the new frame
  4. return the value of the return expression

Look up names in the environment

  1. look up the name in the current frame
  2. if not found, look up the name in the parent frame
  3. if not found, look up the name in the grandparent frame
  4. if not found, look up the name in the global frame
  5. if not found, look up the name in the built-in frame

很经典,就不翻译赘述了

environment is a sequence of frames, each frame is a mapping of names to values

名字与环境(请仔细观看61a 视频,教授讲的很清楚了,下文为仅为中文版翻译)

编程语言的一个关键方面是它提供了使用名字来引用计算对象的手段。如果一个值被赋予了一个名字,我们说该名字绑定到这个值。

在 Python 中,我们可以使用赋值语句建立新的绑定,赋值语句包含一个在 = 左边的名字和一个在 = 右边的值:

>>> radius = 10
>>> radius
10
>>> 2 * radius
20

名字也可以通过导入语句绑定。

>>> from math import pi
>>> pi * 71 / 223
1.0002380197528042

在 Python(和许多其他语言)中,= 符号被称为赋值运算符。赋值是我们最简单的抽象手段,因为它允许我们使用简单的名字来引用复合操作的结果,例如上面计算的面积。通过这种方式,复杂的程序是通过逐步构建越来越复杂的计算对象来构造的。

将名字绑定到值并随后通过名字检索这些值的可能性意味着解释器必须保持某种记忆来跟踪名字、值和绑定。这种记忆被称为环境。

名字也可以绑定到函数。例如,名字 max 绑定到我们一直在使用的 max 函数。与数字不同,函数很难呈现为文本,因此在要求描述函数时,Python 打印一个标识描述:

>>> max
<built-in function max>

我们可以使用赋值语句为现有函数赋予新的名字。

>>> f = max
>>> f
<built-in function max>
>>> f(2, 3, 4)
4

在 Python 中,名字(name)通常被称为变量名或变量,因为它们可以在执行程序的过程中绑定到不同的值。当一个名字通过赋值绑定到一个新值时,它不再绑定到任何以前的值。甚至可以将内置名字绑定到新值。

# max is a built in function
>>> max = 5
>>> max
5

max 赋值为 5 后,名字 max 不再绑定到一个函数,因此尝试调用 max(2, 3, 4) 因为5是不可调用的,他没有指向一个函数 会导致错误。

在执行赋值语句时,Python 会先计算 = 右边的表达式,然后再更改左边名字的绑定。因此,即使在赋值语句中,右侧表达式中也可以引用一个名字。

>>> x = 2
>>> x = x + 1
>>> x
3
a = 1
b = 2
b,a = a + b,b

先 evaluate a = 1,继续 b = 2 evaluate到最后一个赋值语句的时候先不管左边的,计算右边 a + b然后计算ba + b = 3 b=2,右边evaluate完毕,赋值给左边,a = 3`` b = 2

def first create func with signature and body, then bind the name to the function in the current frame 定义函数但并不立马执行

我们还可以在单个语句中将多个值分配给多个名字,= 左边的名字和 = 右边的表达式用逗号分隔。

>>> area, circumference = pi * radius * radius, 2 * pi * radius
>>> area
314.1592653589793
>>> circumference
62.83185307179586

改变一个名字的值不会影响其他名字。下面,即使名字 area 最初绑定的值是基于 radius 计算的,area 的值也没有改变。更新 area 的值需要另一个赋值语句。

>>> radius = 11
>>> area
314.1592653589793
>>> area = pi * radius * radius
380.132711084365

通过多重赋值,= 右边的所有表达式在任何名字绑定到这些值之前都会被求值。根据这个规则,交换两个名字绑定的值可以在一个语句中完成。

>>> x, y = 3, 4.5
>>> y, x = x, y
>>> x
4.5
>>> y
3

函数的实现细节不应影响函数的行为,其中之一就是函数的形式参数(parameters)名称。不同名称的函数如果实现相同的操作,应当具有相同的行为。比如:

>>> def square(x):
        return mul(x, x)
>>> def square(y):
        return mul(y, y)

这个原则表明函数的意义应当与其作者选择的参数名称无关。这样,如果 square 函数的参数是 x,而 sum_squares 函数的参数也是 x,它们的绑定不会互相影响。局部名称的作用域仅限于其定义的函数体(local space),当名称不再可访问时,它就超出了作用域。这种作用域行为是环境模型的内在结果。

命名选择 (来自中文版翻译,群里会有拓展)

尽管名称的可替换性表明形式参数名称不完全重要,但选择合适的函数和参数名称对于函数定义的可读性至关重要。以下是一些命名指南,来自 Python 的编码风格指南:

  • 函数名称:使用小写字母,单词之间用下划线分隔,建议使用描述性的名称。例如:print, add, square
  • 参数名称:使用小写字母,单词之间用下划线分隔,单字名称优先。参数名称应表明参数在函数中的作用,而非仅仅是允许的参数类型。单个字母的参数名称在作用明确时是可以接受的,但应避免使用“l”(小写字母L)、“O”(大写字母O)或“I”(大写字母I),以避免与数字混淆。

虽然这些指南提供了广泛的建议,但 Python 标准库中也存在许多例外。这反映了 Python 语言从不同贡献者那里继承的多样化词汇。

函数作为抽象

尽管简单,sum_squares 函数展现了用户定义函数的强大属性。sum_squares 依赖于 square 函数,但只关心 square 在输入和输出之间定义的关系。

我们可以在不关心如何计算平方的细节的情况下定义 sum_squares。函数 square 可以被视为一个函数抽象,用户不需要了解其具体实现,只需知道它的行为。实际上,Python 库中的许多函数都是这样的“黑箱”,开发者使用这些函数而无需检查其内部实现。

函数抽象的三个核心属性:

  1. 定义域(Domain):函数可以接受的参数集合。例如,square 函数的定义域是任何实数。
  2. 值域(Range):函数可能返回的值的集合。例如,square 函数的值域是所有非负实数。
  3. 意图(Intent):函数计算输入和输出之间的关系(以及可能产生的副作用)。例如,square 函数的意图是返回输入的平方。

理解这些属性对于正确使用复杂程序中的函数抽象至关重要。函数的意图应明确,但如何实现这一意图的细节应被抽象掉,使得使用者无需关心函数的内部实现。

函数的作用域

作用域 是指在程序中可以访问变量或名称的区域。在函数内部定义的名称仅在该函数内部有效。这种特性允许在不同的函数中使用相同的名称而不会发生冲突。例如,函数 squaresum_squares 都可以使用 x 作为参数名称,而这两个 x 在各自的函数内部是独立的:

>>> def square(x):
        return mul(x, x)
>>> def sum_squares(x, y):
        return add(square(x), square(y))

square 函数中,x 仅在 square 函数的局部作用域内有效。类似地,sum_squares 函数中的 xy 也仅在 sum_squares 的局部作用域内有效。这种局部作用域的隔离保证了在函数内对名称的引用不会影响到其他函数或全局环境中的名称。

递归的作用域

递归是函数调用自身的一个技术,允许在解决问题时重复使用相同的函数。递归函数的局部名称作用域遵循与非递归函数相同的规则。

>>> def factorial(n):
        if n == 0:
            return 1
        else:
            return n * factorial(n - 1)

factorial 函数中,每次递归调用都创建一个新的局部帧,其中 n 是当前调用的局部变量。递归的每一层都使用独立的 n 值,因此不会相互干扰。

闭包与自由变量 (群内拓展)

闭包(Closure) 是一种特殊的函数,其定义的环境包括外部函数的局部变量。闭包允许函数在其外部作用域中访问这些局部变量,即使外部函数已经返回。例如:

>>> def make_multiplier(factor):
        def multiplier(x):
            return x * factor
        return multiplier
>>> double = make_multiplier(2)
>>> double(5)
10

这里建议做几道leetcode加深理解,但鉴于我们还没有掌握基本语法,我们会在之后的课程中作为quiz。

在这个例子中,multiplier 函数是 make_multiplier 的闭包。multiplier 函数可以访问 make_multiplier 中的 factor 变量,即使 make_multiplier 已经执行完毕并返回。factor 被称为自由变量,它在 multiplier 函数的作用域内仍然可用。

作用域的总结

理解作用域和局部名称对于编写清晰和可维护的代码至关重要。局部名称只在定义它们的函数内有效,函数调用创建新的局部帧,每次调用都拥有自己的作用域。递归函数和闭包展示了作用域的更复杂应用,允许我们在复杂的编程任务中有效地管理名称和变量。

总结这些概念有助于编写更可靠和可读的代码,并确保不同函数和代码块之间的名称不会发生冲突或误用。

Labs

完成61a DQ 1