Shixuan Li

流水不争先,争的是滔滔不绝

0%

CS 61A 学习手册 [Part 1] 函数与表达式

本章我们将从计算机科学大框架入手, 并开始以python为基础介绍编程的思想和实现. 注意, 本节课不是编程语言课, 所以不会花太多的功夫介绍python的语言特性, python标识方面的问题, 可以结合网络进行理解学习.

表达式 (Expressions)

表达式: 描述一个运算并得到结果

从最简单的 \(1+1\), \(2*5\), \(\frac{6}{23}\)\(\sqrt{3493161}\), \(\sum\_{i=1}^{100}i\)等. 这些都是运算的表达式. 这里请注意我们的定义, 我们的定义之中是“表达式”而不是一个完整的结果, 所以说我们并没有写出“= xxx"之类的结果性东西. 实际上, 我们可以理解为, 表达式本身就代表着结果. 例如\(1+1\)代表的就是2, \(2*5\)代表的就是10一样.

函数方程 (Functions)

所有的表达式都可以使用函数方程调用的表述形式来表现 (function call notation)

我们要做的, 就是用一个统一的方式去代表这些运算表达式. 首先我们先细致的了解一下表达式的组成.

我们会发现, 所有的表达式都由至少两部分组成: 如何算 以及 算什么. “如何算”代表着我们要用怎样的运算方式, 加减乘除等, 即我们的“计算方式”; 而“算什么”代表着我们计算的对象, 即“计算对象”. 下图可以较好的体现出这种关系:

咱们再来反观函数表达式的一般结构. 每一个函数表达\(f(x, y)\)一般也会由两部分组成.

定义函数方程

定义一个函数的时候, 我们要定义函数的名称, 输入(函数因子), 以及运算方式. 例如我们常见的$y=f(a,b); f(a, b)=a+b $, 其中"f"是函数的名称, 我们定义了两个输入端口(input)即a和b. 然后我们定义了函数的内容, 也就是计算a+b. 这样, 当我们将数字输入方程, 也就是定义了函数(接口)中的a和b的时候, 函数里面的a和b就被定义了, 于是乎函数就进行了运算然后会输出a+b的值.

故此, 我们可以将所有的运算, 都用函数的形式表达出来. 我们可以定义不同的函数, 并给他们起上名字, 当我们想要使用该计算的时候, 我们就可以将我们想要计算的部分当作函数的函数因子放入函数中, 以此得到一个结果.

调用函数方程

计算机中函数就像是一个盒子, 或者说一个机器. 每一个函数在被定义的时候, 就留着一些“输入”端口用来接受函数因子, 一旦接收到了函数因子, 机器就会运作并生成结果. 其过程如下所示: 我们先定义一个函数, 定义函数名称和函数因子名称(输入端口)
给函数的运算内容进行定义
假设我现在想要计算2+3
我会先找到做加法的函数单元
将我想要计算的函数因子(运算因子)放入函数的输入端口, 只有将两个都放进去函数才能正常运行.
于是我得到了我想要的结果

解析表达式 (Anatomy of a Call Expression)

使用表达式

我们首先来看下面的标述:
我们可以将上面的表达式理解为刚刚所提到的方程. 我们现在把它理解为一个运算的表达式. 我们可以把它同样看作两部分. 实际上他就是已经被明确定义过函数名称 (add), 函数因子 (a和b), 和函数内容 (因为这个函数是系统自带的函数, 已经有了定义, 这里的函数内容就是计算a+b). 我们定义在运算过程中的表达式的两部分为“运算类型”(operator), 和“运算因子”(operand)
假设我们将2和3放入函数的输入端口, 让a=2, b=3, 我们将会得到a + b = 2 + 3 = 5.

解析顺序

当我们使用表达式的时候, 我们的计算机是如何解析我们的表达式的呢? 想一想, 当我们看看到一个表达式的时候, 我们会下意识的进行运算了, 但是机器不是, 他们需要一个被定义的顺序去理解所接收到的东西. 这其实也是一个很重要的计算机思想, 也就是如何让行为原则化, 任何的操作都有一个明确的定义. 这一理念也是人工智能的高阶领域的基础.

我们的解析顺序是: 先根据运算类型(Operator)判断将要进行的操作, 再按顺序处理运算因子(Operands).

也就是说, 我们的机器会先看到 (按照上部分的例子), “哦, 我遇到了”add“, 我要做加法了”. 然后看到”哦, 我们有一个输入被定义叫a, a被赋值2, 还有一个输入被定义叫b, b被赋值3“. 然后计算机判断 “哦, 我们有了足够的信息了, 现在计算a+b=2+3=5”. 于是乎我们得到了5. 理解计算机的处理过程有什么用呢? 我们在紧接着的这一部分就能看到.

表达式嵌套

假设我们有了如下的一个表达式:
我们可以将其理解为划线部分的多层嵌套:
### 表达式解析过程 下面让我们来按步骤解析计算机是如何处理这部分信息的: 首先我们会先判断最外层运算类型“mul“
紧接着我们分析mul的第一个operand(运算因子). 我们发现这个operand(运算因子)也是一个表达式, 我们开始解析这个表达式
我们先得到他的运算类型是add
它的第一个运算因子是4
它的第二个运算因子是一个表达式, 于是我们对它进行分析
我们得到它的类型是mul, 两个operand(运算因子)是4和6, 非常明确
于是我们对它进行计算
并得到24
接下来我们发现, 上一级的add运算的第二个运算因子已经得到了是24
于是我们进行这一级运算并得到28
返回最初的mul表达式, 我们得到了第一个operand(运算因子)是28. 紧接着我们来解析他的第二个运算因子, 我们发现它是一个表达式
于是我们解析这个表达式, 发现它的运算类型是add, 两个运算因子分别是3和5
于是我们对该表达式进行计算
并得到8
于是, 最上层表达式mul的运算类型(operator)和运算因子(operands)全部集齐, 我们进行计算
然后得到224
至此, 我们的计算全部完成. 我们可以大体来回顾一下这个嵌套表达式的结构. 我们发现它由很多子表达式组成. 这个整体被称为Expression Tree(表达结构树). 我们按照Operator -> Operands的顺序进行解析, 并得到我们的结果.

表达式类型

从以上的嵌套表达式中我们可以发现, Operand不但可以是数据, 也可以是一个表达式.

定义函数方程

在python中, 我们有一个简单的方法去定义一个函数方程. 还记得我们讲过定义一个函数需要哪些部分吗? 我们需要函数的名称, 输入端口, 和运算过程. 在函数的定义的时候, 我们不但要考虑以上所说的所有内容, 还有一项叫做“返回值”, 意思是这个函数返回的值.

函数定义的方式如下:
其中函数的标识signature(也就是def <name>(<formal parameters>)的部分)体现了函数的名称, 以及它将接收多少个输入. 函数的内容body定义了函数的运算过程, 以及它返回的值.

我们定义的函数的步骤是: 先定义函数的标识, 再对函数的内容进行定义, 最后计算机将把函数内容的意义绑定在函数名称上. 今后一旦我们想要使用这个函数, 我们取用这个函数名称, 并加之以输入内容(函数因子)即可. 注意! python的特性要求我们将函数的内容body均放入定义函数标识signature向后一个缩进(indent). 即, 若我们定义一个函数使得输入的数字加一. 我们应如此定义:

1
2
>>> def add_one(x):
... return x + 1 # 注意这行有一个tab的缩进
而以下就是缩进错误的:
1
2
>>> def add_one(x):
... return x + 1 # 缺少缩进, 计算机将不会把这行理解为函数内容而是另一行命令
同样需要注意的是, 如果一个方程没有return返回值, 此方程将不会返回内容.

练习

以下表达式的最终输出是什么?

1
2
3
4
5
>>> f = min
>>> f = max
>>> g, h = min, max
>>> max = g
>>> max(f(2, g(h(1, 5), 3)), 4)

答案请见下一章节末