[CS61A]Week01笔记

week01笔记

开篇

课程是Summer2020的
https://inst.eecs.berkeley.edu/~cs61a/su20/

我水平有限,这也就只能当笔记看看,无任何指导价值,样例多来自于课程中。

相似课程

以下课程只是与CS61A教学内容有部分重叠

  • CS10
  • CS88
  • Data8

学习目的

  • 作为计算机科学的介绍(Intruduction to CS)
    • 可解性
    • (若可解)解法
    • (若能优化)高效解法
  • 管理复杂性(complexity)
    • 掌握抽象(不是0和1这种细节,而是对于本质的探寻)
    • 编程范式(paradigm)
  • 理解python基础
  • 在大项目中融会贯通多种理念
  • 理解电脑如何解释程序

什么是抽象?

  • 指定(assignment, 将名与值绑定)是一种最简单的抽象方式
  • 函数定义(将名与一系列表达式绑定)是一种更加强大的抽象方式

一些英语词汇

我单列出来这个是因为可能用到且可能忘(好吧…就是我不会这些词,至少学之前不会)

英文 中文释义
forsooth 确实
comma 逗号
be nested within 嵌套于
infix operator 中缀运算符(比如18+69的加号)
evaluate 求值(计算机术语)
formal parameter 形参
colon 冒号
indented 缩进
quotient and remainder 商和余数
Domain of function 函数的定义域
Range of function 函数的值域
derive 派生
formulas 公式
cubes of number 立方数
converge 收敛(函数收敛/发散的那个收敛)
compression 压缩
pitch 音高
intrinsic 固有的

关于lab与pyok

pyok是一个类似于做题系统的东西,可以根据设置的题目检验掌握的知识,非该校学生没有不要使用在线部分,据观察,在线部分的用途主要是保存该校学生的分数以供日后考核。

测验内容据目前观察分为文件和问答测验。

文件部分打开文件,补全下划线上的内容,保存。

在ok文件的目录下运行ok对文件进行测验,命令如下,若不成功将python换成python3或者py。

python ok

这里是要登陆okpy账号的,命令行显示要求是该大学的教育邮箱(.edu结尾),或者与该大学账号绑定的谷歌邮箱,不然登了也是白登。

如果不想/不能与该网站交互信息,则可以选择本地运行,使用如下命令。

python ok --local

事实上不是该校人员,就直接使用–local就行了

相关使用信息可以在该网站看到:
https://inst.eecs.berkeley.edu/~cs61a/su20/articles/using-ok.html#signing-in-with-ok

这里也有ok的教程,包括自己制作本地测验。

本节课讲到的python知识

注:样例很多来自于课程提供

  1. 使用等号将名字(name)与值(value)绑定到一起
  2. 名字是和值绑定的,而非表达式
  3. 允许一句话为多个名字绑定值
  4. //运算符:整除向下取整
  5. 7/2结果是3.5,因为python是弱类型语言
  6. 使用import来引入内置(built-in)函数与常量

解释:

a = (100 + 50) // 2
# 4.这时a的值是75而非(100 + 50) // 2

from math import pi
# 5.在math库中导入了常量pi
radius = 10
# 1.将值和名字绑定到一起
area, circ = radius * radius * pi, 2 * pi * radius
# 3.的解释,名值绑定的过程始于等号的右侧
# 在右侧的一个到多个值(多个值使用逗号分隔)中,自左到右依次求值
# 拿到所有(注意是所有)这些值后再绑定给等号左侧的名字
# 如果右侧的不是

#此时若将radius改为20,area值不变,验证了第2条
#对于这种依赖某个值,需要同步(sync)的,应当使用函数

名值绑定的三种方式

  • import语句(对built-in有效)
  • 赋值语句(assignment statement)
  • def语句(def statement)

表达式的种类

  1. 原始表达式(Primitive expressions)
    • 数字
    • 名字
    • 字符串
  2. 调用表达式(Call expressions)
    • 算子(operator)
    • 操作数(operand)

注:算子亦可作为操作数

表达式树的手动创建

  1. 看算子(operator)是什么
  2. 依次计算该算子对应的操作数(就是括号里,逗号隔开的那些)
  3. 如果操作数含有算子,对该算子执行1

函数的定义

定义一个函数应当使用def语句

def <name>(<formal parameters>):
	return <return expressions>
# 应当注意圆括号右侧有个冒号
# 函数结束会执行return语句

形如():的被称为函数签名(function signature)

函数签名指定了函数接受的参数个数,以及接收过来后绑定到那些对应的名字上

在第一行后面缩进的被称为函数体,它定义了一个函数的具体实现

执行到def语句会在当前的帧(我觉得说成当前上下文对应的物理内存)进行名值绑定,将函数绑定到函数签名中说的名字上,此过程不涉及函数具体语句的执行

函数的执行

  1. 在内存开辟一块新的局部帧(Local Frame, 这是相对于全局帧而言的,有种变量作用域那种感觉),其中Frame是物理地址上的基本单位。关于Frame这个词汇及其相关细节可以参阅https://www.jianshu.com/p/c16737703423
  2. 将形参与在该帧内的参数绑定
  3. 在那个新创建的空间内执行函数的操作

注:返回值一般不涉及绑定(因为多数情况下用不到等号)

Frame的开辟也是依照函数签名来操作的,这个函数签名指定了局部帧的名字,以及绑定用到的名(绑定用到的值现在不用处理,因为根本没有,只有使用函数时才会知道),这个Frame,就是这个函数执行时的上下文(Context),每个函数需要再对应的上下文中执行。

当前上下文要么是全局帧,要么是局部帧(再紧接着返回上一级全局帧)

划重点

  • An environment is a sequence of frames

(执行的)环境就是帧的序列(序列当然有先后顺序)

要么是一个全局帧,要么先是局部帧,紧接着是全局帧(最多就两层啊,哪怕函数是有嵌套的,每一个函数的上一层都是global。表达式树是从上到下或者说从嵌套外层到嵌套内层创建,但是执行是从最底部或者说嵌套最里面的开始执行),我个人的理解是,这个全局帧是一个相对概念,每个产生调用的上下文对于那个被调用的上下文都是所谓的全局

  • A name evaluates to the value bound to that name in earliest frame of the current environment in which that name is found

这句话诠释了一个现象,就是在当前执行上下文是局部作用域(比如一个函数)时使用到了一个名(前面有讲过,名可以和值绑定,也可以和函数绑定),如果该名在当前的局部作用域和全局作用域都有一个(就是重名了),那么使用先找到的那个(先从当前的作用域找,找不到在找上级,那么肯定先找到的是当前这个局部作用域内的名,所以使用这个)

这两句话想表达啥:(在已知名字和值是一一对应的前提下)对于已知的名字寻找其对应的值是要结合具体上下文来看的,在不同上下文里结果可能不一样,离开上下文来谈一个名是没有意义的,并且第二句话给出了为一个名寻找其绑定的值的顺序。

from operator import mul
def square(square):
	return mul(square, square)
square(4)

对于这样一段程序,在global下square是一个函数调用表达式,但是在square这个函数的上下文里,square是一个保存了值的名字,本例中是4。并且在该上下文找不到mul,因此会去上级也就是global中寻找并且找到了mul。

若干python细节与杂项

关于print与None

# 从何说起
print(print(1), print(2))
# 在交互终端执行这句话的输出结果是什么
# 结果如下:
# 1
# 2
# None None

一个函数如果没有明确return什么就会return一个None

None一般不会被自动显示出来,不过…

  1. 可以被print出来
  2. 可以作为右值被绑定到某个名上
  3. 被绑定到了某个名上也不意味着在交互终端直接输入名就能打印出None。

这就引出了下面的纯函数的定义

纯函数(Pure Function)与非纯函数(None-Pure Function)

维基百科是这样写的:

在程序设计中,若一个函数符合以下要求,则它可能被认为是纯函数:

  • 此函数在相同的输入值时,需产生相同的输出。函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关。
  • 该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。

print就不是纯函数,它有函数副作用,即在屏幕上显示东西。这里的副作用,就是除了返回结果以外的其他东西

运算符作为内置函数

在operator下有mul相当于*,add相当于+,truediv相当于/,floordiv相当于//,mod相当于%

from operator import mod
a = mod(2013,10)
b = 2013%10
# a和b结果是一样的

python的注释

# 单行注释使用#开头
"""
多行注释使用三个双引号
其实三个单引号也可以
"""

python的doctest

使用python可以实时获得对于输入的响应,但是在某些想要保存或者反复使用代码的情况下,会将代码写在以.py为后缀名的文件中,并且在命令行中使用

//假设文件叫做apy
python a.py

为了在运行完后能够使用交互(比如查看某些变量的值),可以使用-i参数,通常使用-i参数时,py文件仅提供函数定义就够了,因为你会在随后的交互界面有选择性的执行它们(比如说传入不同的参数)

python -i a.py

为了能够检验某些函数编写的样例是否能够使用,这里的i是interactive(交互)的意思

python -m doctest a.py

假设a.py内容如下

from operator import floordiv,mod

def divide_exact(n, d):
    """返回商和余数
    
    >>> q, r = divide_exact(2013,10) 
    >>> q
    201
    >>> r
    3
    """
    return floordiv(n, d), mod(n, d)

那个多行注释里面的像python交互环境起始的>>>的后面的操作就会像在python交互环境中操作,并且对于结果进行检验,如果符合预期则不产生任何信息,如果有错误(和多行注释里的预期不相等),则产生报错。

检验:把代码里的201改成别的试试。比如我这里把201改成2011后的报错如下

**********************************************************************
File "D:\CodeSource\pythonProject\a.py", line 7, in a.divide_exact
Failed example:
    q
Expected:
    2011
Got:
    201
**********************************************************************
1 items had failures:
   1 of   3 in a.divide_exact
***Test Failed*** 1 failures.

对于没有错不产生任何信息而又想看到具体细节,可以使用如下命令进行查看

python -m doctest -v a.py

python函数的默认参数

from operator import floordiv,mod

def divide_exact(n, d=10):
    return floordiv(n, d), mod(n, d)

这样你可以在交互界面尝试使用

q, r=divide_exact(2013)

并且得到跟上面一样的结果

python的条件语句

python的条件语句是一个复合语句(compound statement),它们可以由多个clause(就是判断条件,并且else也是一种判断条件,即’否则’)

每个clause后面相应的执行代码叫做suite。

执行时会按顺序依次从上到下判断每个clause,如果计算结果是一个真值,则执行它对应的suite,并且跳过剩下的clause不再判断。

显然,对于一组条件语句,总有且最多有一个clause会被执行。

def absolute_value(x)
	"""返回x的绝对值"""
	if x < 0:
		return -x
	elif x == 0:
		return 0
	else:
		return x

上述语句就有3个clauses,即if、elif、else。

对于一组条件语句,总有一个if出现在头里,有0个到多个elif出现在中间,有0个或1个else出现在结尾。

python的布尔(bool)值

结果是二元的,即真或假其一,就是bool值。比如上一节的例子中x<0结果总是在"成立"或者"不成立"之间选一个,这就是bool值。

python中某些量可以代表布尔假值,见下表

定义
假值 False,0,‘’,None,(),[],{}
真值 除了假值以外的所有值

其中’'代表空字符串,()代表空元组,[]代表空列表,{}代表空字典,0包括整数0、小数0等各种数值类型下的0。

提示:python中True和False首字母大写的。

python中的迭代(以斐波那契数列为例)

iteration means repeats things

可以使用while语句来进行一遍又一遍的操作(迭代),while是一个复合语句。

while的执行步骤:

  1. 判断第一句话里的表达式是否为真
  2. 如果为真就执行while表达式的suite,并且在执行完一遍后跳转到1
def fib(n):
    """计算斐波那契数列第n项且n应当>=1,数列首项认为是第零项"""
    pred, curr = 0, 1
    k = 1
    while k < n:
        pred, curr = curr, pred + curr
        k = k + 1
    return curr
a = fib (7)
print(a)

划重点,while语句块里的第一个赋值,它是都给计算好了再统一对应赋值到左边。不会先把pred绑定好新值后在计算后面的pred + curr。先都计算,再统一绑定新值。

视频指出了另一种方法:

def fib(n):
    """计算斐波那契数列第n项且n应当>=1,数列首项认为是第零项"""
    pred, curr = 1, 0
    k = 0
    while k < n:
        pred, curr = curr, pred + curr
        k = k + 1
    return curr

该方法较改变了pred、curr和k的值,较原先的优势是能够正确计算下标为0的元素。初始化时,pred不代表该数列任何一个元素,curr代表第零个。

python中的return语句

调用一个函数执行诸多语句中的return语句作为函数的结尾并决定了该函数的返回值。在执行一个函数时,它的诸多调语句中有且最多有1个return会被执行。

python中的lambda

为什么要提一下lambda呢,因为他上来就讲高阶函数,例子中用到了lambda。

函数有两种,一种是def的,一种是lambda的(也叫做匿名函数,因为没有绑定到某个名上)

在lambda后面,冒号前面是参数列表,冒号后面的表达式是单行的,没有return,该单行表达式的结果就是返回值。

看起来限制很多,一般在特定场合使用,为了方便。

def square(x):
    return x * x
# 一定程度上等价于
f = lambda x: x * x
# f(4)和square(4)结果是一样的

为什么是一定程度上等价于呢,在交互终端输入函数名(不加括号)看看结果:

>>>square

>>>f
 at 0x00000183715E5C10>

一个是lambda,一个不是,很直观能看出来的。

python中的高阶函数_part1

维基百科对于高阶函数(Higher-order function)是这样定义的:

在数学和计算机科学中,高阶函数是至少满足下列一个条件的函数:

  • 接受一个或多个函数作为输入
  • 输出一个函数
"""文件名a.py | 使用(Windows下的CMD里)python -i a.py运行"""
"""sqrt本质上是一个开平方的函数,但是限制颇多,只能做有整数解的"""
def square(x):
    return x * x

def search(f):
    x = 0
    while not f(x):
        x += 1
    return x

def inverse(f):
    """返回使g(f(x))->x的g(y)"""
    return lambda y: search(lambda x: f(x) == y)

sqrt = inverse(square)

这个程序吧…有点折磨。

square(x)不用说,是返回平方的。

search(f)也好理解,就是从零开始正向每次递增1以寻找到第一个使得f(x)返回值为真的x值,并返回它

然后是inverse(f),它返回一个lambda函数,该函数有一个形参y,该函数的函数体只有一个语句,就是search(f1),那search(f1)这个f1究竟是什么样的f1呢,它也有一条语句,如果f(x)的值与y相等就返回真,否则返回假。最好结合下表来分析。

name type
x value
y value
f Function
# 先对它稍加改写
def inverse(f):
    """返回使g(f(x))->x的g(y)"""
    def f2(y):
        return search(lambda x: f(x) == y)
    return f2

稍加改写之后再进一步改写(如下):

def inverse(f):
    """返回使g(f(x))->x的g(y)"""
    def f2(y):
        def f1(x):
            return f(x) == y
        return search(f1)
    return f2

就变得更加清(shao)晰(nao)了。

inverse(f)返回一个函数f2(y),它有传入一个参数y,f2(y)本质是个search,我们希望对于这个f2(y),从零开始找到一个最小的正整数,能够使f1成真(能够使f(x)==y成真,放到具体inverse(square)中就是能够使square(x)==y成真)。

简而言之就是我们对于一个y,希望这个函数能找到f(x)==y(事实上找不到那直接死循环了…)并返回那个x,square(x)==y的话,那么就有x*x=y,又由于x是从零开始正向递增1来寻找的,所以x如果找到则必然是y的正整数平方根。

一句话,返回的函数接受的参数y,以它为结果寻找f(x)=y的x,返回这个x值。

所以inverse(square)的逻辑就是,从零开始试整数来寻找目标值的平方根,如果试的数的平方等于目标值,那么该数自然就是目标值的平方根。

(内心OS:这玩意打死我也写不出来啊…)

python的raise

raise就类似于java的throw,那么后面也得跟一个异常,这个异常应当是从BaseException派生出来的。

如果你瞎抛出异常就会得到类似下面的结果:

>>> raise a
Traceback (most recent call last):
  File "", line 1, in <module>
    raise a
TypeError: exceptions must derive from BaseException
name mean
NameError 变量未声明
TypeError 类型错误
IndexError 下标越界

关于TypeError,比如说

>>>1+'234'
Traceback (most recent call last):
  File "", line 1, in <module>
    1+'234'
TypeError: unsupported operand type(s) for +: 'int' and 'str'

但是JavaScript里就不会

a = 1+'234'
//a的值现在就是'1234'
//typeof(a)的结果是'string'

这里还是要注意一下的,不能搞混。

异常的捕获使用try语句,处理是except块,这跟java的try-catch块不一样。

try:
    a = int(input("输入被除数"))
    b = int(input("输入除数"))
    if b == 0:
        raise ValueError("0不能做除数")
    else:
        print(a/b)
except ValueError as e:
    print(repr(e))

当输入除数时输入的是0,则会有如下输出:

ValueError('0不能做除数')

python中的断言

呃…我们先来认识一下这几个单词

英文 中文
assest 评估
asset 资产
assert 断言

断言语句(assert statement)

对后面的布尔表达式求结果

如果为真则无任何反应,继续后续内容

如果结果为假就触发异常,可以后面跟一串字符串

# 例子
assert 3>2,'0hhhhhNo'

进一步的解释如下:

assert bool_expression
# 相当于
if not bool_expression:
    raise AssertionError
# 由于AssertionError可以自定义显示什么内容,因此对于
assert bool_expression, arguement
# 相当于
if not bool_expression:
    raise AssertionError(argument)

设计函数

接着上面,很自然而然地引出了下面的内容(说实话我记笔记时也不知道这一块内容该放哪里…虽然应该是按听课顺序往下记,但是还是打算把相关内容放一块,比如高阶函数)

函数的出现和使用避免了很多语句的不断堆砌以及相互跳转导致的错综复杂的执行顺序。它的意义在于让编码者能够良好地组织程序

同一个目的可以有很多不同的实现方式,也就意味着针对同一个目标,可以写出很多个不同的函数,但是某些函数因为一些特性使它优于其他的函数,比如说便于读懂或者说适用范围更广等等。

原则如下(仅供参考,请酌情使用)

  • 一个函数只承担一项工作
  • DRY(Don’t Repeat Yourself, 就是有重复使用的地方就封装成函数)
  • Define function generally

对于DRY的解释(源自维基百科):一次且仅一次。当DRY原则被成功应用时,一个系统中任何单个元素的修改都不需要与其逻辑无关的其他元素发生改变。

对于Define function generally(定义通用的函数)的解释:电线插头的例子,各国的插头标准各不相同,这就是不通用的。

python中的高阶函数_part2

assert和raise是高阶函数的铺垫。

对于以下程序片段

"""Generalization(泛化、一般化)"""

from math import pi, sqrt

def area_square(r):
    return r * r

def area_circle(r):
    return r * r * pi

def area_hexagon(r):
    return r * r * 3 * sqrt(3) / 2

有这样几个函数来计算正方形,原先和六边形的面积。如果想加一个判断来确保r输入的大于0,那么我们最基础的做法是把下面条语句添加到每一个函数的return前

assert r > 0, 'r应当大于0'

我们通过使用断言来确保输入的r是大于0的值,但是这违反了上面提到的"Don’t repeat Yourself"原则,因此考虑把它封装成函数。

如果观察返回值,就会发现一个共同点:所有返回值是一个系数不同的r^2项,这意味着我们可以有这样一个函数:

def area(r, shape_constant):
    assert r > 0, 'r应当大于0'
    return r * r * shape_constant
"""r是半径/边长,shape_constan是r^2项的系数"""

这样上面的代码片段就能改写成如下的样子:

"""Generalization(泛化、一般化)"""

from math import pi, sqrt

def area(r, shape_constant):
    assert r > 0, 'r应当大于0'
    return r * r * shape_constant

def area_square(r):
    return area(r, 1)

def area_circle(r):
    return area(r, pi)

def area_hexagon(r):
    return area(r, 3 * sqrt(3) / 2)

这是一个抽象的过程,我们把它们相同的模式给概括了出来,虽然它们使用目的不一样,但是拥有一些相同的东西。

∑ k = 1 5 k = 1 + 2 + 3 + 4 + 5 = 15 {\mathop{ \sum }\limits_{{k=1}}^{{5}}{k=1+2+3+4+5=15}} k=15k=1+2+3+4+5=15

∑ k = 1 5 k 3 = 1 3 + 2 3 + 3 3 + 4 3 + 5 3 = 225 {\mathop{ \sum }\limits_{{k=1}}^{{5}}{k\mathop{{}}\nolimits^{{3}}=1\mathop{{}}\nolimits^{{3}}+2\mathop{{}}\nolimits^{{3}}+3\mathop{{}}\nolimits^{{3}}+4\mathop{{}}\nolimits^{{3}}+5\mathop{{}}\nolimits^{{3}}=225}} k=15k3=13+23+33+43+53=225

∑ k = 1 5 8 ( 4 k − 3 ) ( 4 k − 1 ) = 8 3 + 8 35 + 8 99 + 8 195 + 8 323 = 3.04 {\mathop{ \sum }\limits_{{k=1}}^{{5}}{\frac{{8}}{{ \left( 4k-3 \left) \left( 4k-1 \right) \right. \right. }}=\frac{{8}}{{3}}+\frac{{8}}{{35}}+\frac{{8}}{{99}}+\frac{{8}}{{195}}+\frac{{8}}{{323}}=3.04}} k=15(4k3)(4k1)8=38+358+998+1958+3238=3.04

再观察上述三个式子,它们也有共同之处,共同之处未必是数据,也可以是过程,比如说这里就有一个连加的过程,它们都是很多个数据加起来,并且这些数据都是可以由一个已知的构造器构造的,比如说第二个的构造器就是 k 3 k\mathop{{}}\nolimits^{{3}} k3

所以这可以抽象为

formula(n, constructor_of_k)
"""其中n表示计算到第n项,constructor_of_k是k的构造器"""

甚至还可以再抽象一下,毕竟连加也是一个过程。

formula(n, process, constructor_of_k)
"""我们用process处理项与项之间的过程
比如说1^3+2^3就是process(constructor_of_k(1),constructor_of_k(2))
其中constructor_of_k在这里返回的是k的三次方
"""

好吧,如果做了hw02我觉得这不是问题…

所以狭义上来讲,他讲了这么多,就是希望能够理解,把过程当做函数,然后对于一个计算,将被计算的和它的计算过程都传进去。

打个比方,有几个员工,每个员工处理一类事物,比如说A只管盖章,给他文件他就在上面盖章,然后返回盖完章的。B只管签字,你给他文件他就在上面签字。然后现在让一个人来完成A和B的工作,你给他个文件,告诉他要签字,它就签字,告诉他要盖章,他就盖章。

其中,员工就是那个处理数据的函数,告诉他干什么,就是传入一个定义计算过程的函数,文件,就是传入的数据。

函数作为返回值

这样,我们就能组装我们需要的函数了

个人的思考是这样的,在看了高阶函数之后,会发现这样一个问题:对于一个函数,其中某些过程是可以替换的元件(换成其他元件的也能运行,得到不一样的结果),因此我们会想到把它封装成一个函数然后把这个过程函数当做参数传入。然后对于诸多过程的函数,如果它们之间有相关性,我们可以再度对于这类过程进行抽象,写出能够动态构造这些过程的代码,也就是返回一个装配好的函数。

def make_adder(n):
    """
    >>>add_three = make_adder(3)
    >>>add_three(4)
    7
    """
    def adder(k):
        return k + n
    return adder

# 然后可以这样使用:
make_adder(2)(3)
# 其中,make_adder(2)是算子,3是操作数

例如这个,返回了一个函数,这个函数接收一个k,并且对k进行加n返回的操作。

小结:

  • Functions are first-class,函数是头等的。即函数可以作为参数和返回值。头等函数最直观的特点就是可以当做变量来使用(别想了,C语言的函数不是头等的)
  • 能够接受函数作为参数或者返回一个函数的函数就是高阶函数
  • 高阶函数设计常常用how思考而非what,不是处理什么,而是怎么处理
  • 高阶函数有效避免了代码的重复书写

音频编码与高阶函数

为了讲为什么用到高阶函数,讲师举了一个音频编码的例子,代码如下:

其中我自己进行的注释使用#来写。

前置知识点1:.WAV格式

内容摘自维基百科,部分内容略有提炼。

WAV是一种对于波形尽可能原封不动保存的格式,不去压缩波源信息。由于储存的是波,如果按照振幅-时间点的格式储存,就可以解析为声音(它自然可以储存别的波,比如说电波)。WAV格式与有损/无损无关,如果音源是有损的,那么如实呈现的WAV也将是有损的,反之同理。

WAV文件遵守资源交换文件格式(Resource Interchange File Format,RIFF),RIFF文件由一个简单的表头(header)跟随着多个"chunks"所组成

  • 表头(Header)
    • 4位组(bytes):固定为"RIFF".
    • 4位组:little-endian 32-bit正整数,整个文件的大小,扣掉识别字符和长度,共8个字节。
    • 4位组:这个文件的类型字符,例如:"AVI “或"WAVE”.
  • 接下来是区块(Chunks),每个区块包含:
    • 4位组:此区块的ASCII识别字,例如:"fmt “或"data”.
    • 4位组:little-endian 32-bit正整数,表示本区块的长度(这个正整数本身和区块识别字的长度不算在内)。
    • 不固定长度字段:此区块的资料,大小等同前一栏之正整数。
    • 假如区块的长度不为偶数,则填入一个byte。

第一个chunk的识别字是fmt,规定了后面东西的格式与参数,此后的chunk是真正的数据,识别字是data

更多内容我认为可以参阅这篇文章:

https://www.jianshu.com/p/947528f3dff8

https://cloud.tencent.com/developer/article/1358832

前置知识点2:音频采样

对于波f(t),其内容的记录,就是储存固定的某个时间点t下的f(t),每一个时间点t对应的f(t)被储存,就是一次采样。一秒钟有多少个这样的时间点,就是其采样率。

请注意区分,WAV只是储存采样得来的样本,功能仅仅是原封不动的储存,仅此而已。

一般来说波是在现实生活中采样而来的,但是也可以人为编码生成。

前置知识点3:用到的模块(或其中的方法)

内容多来自于官方文档

https://docs.python.org/zh-cn/3/library/wave.html

https://docs.python.org/zh-cn/3.10/library/struct.html

wave模块有open函数专门用于打开WAV文件,返回一个Wave_read对象。

struct模块用于对二进制数据按照一定格式解读。数据储存在计算机内就是二进制的,使用者认为的有效数据都是建立在对于一定的字节串按一定格式的解读之上。

例如双精度(IEEE1023)格式,我们对64位二进制数据进行读取,依照规定,第一位是符号位,紧随11位指数位,再接着是52位无符号整数作为尾数,按照此方法就能解读出浮点数来。

音频文件同理。对于每个采样点(每个数据块),要按照约定好的格式读取。

音频编码代码与理解

代码是连续的,但是我断开为几个片段,用来书写大块注释

from wave import open
from struct import Struct
from math import floor

frame_rate = 11025
# 每秒钟采样次数

def encode(x):
    """Encode float x between -1 and 1 as two bytes.
    (See https://docs.python.org/3/library/struct.html)
    """
    i = int(16384 * x)
    return Struct('h').pack(i)

根据官网的说法struct.Struct(format)会根据格式字符串format来写入和读取二进制数据,返回一个新的Struct对象

Struct对象有一个pack()方法,同struct.pack()

struct.pack(format, v1, v2, …),返回一个 bytes 对象,其中包含根据格式字符串 format 打包的值 v1, v2, … 参数个数必须与格式字符串所要求的值完全匹配。

为什么这里的格式控制字符是’h’呢,这里是和后文对应的。官方文档说了,h是2个字节长度的整数。(后文也说了,设置采样字节长度为2)

def play(sampler, name='song.wav', seconds=2):
    """Write the output of a sampler function as a wav file.
    (See https://docs.python.org/3/library/wave.html)
    """
    out = open(name, 'wb')
    # 以只写方式打开文件,不支持同时读写
    out.setnchannels(1)
    # 设置声道数为1
    out.setsampwidth(2)
    # 设置采样字节长度为2
    out.setframerate(frame_rate)
    t = 0
    while t < seconds * frame_rate:
        # 循环遍历每一个采样点
        sample = sampler(t)
        out.writeframes(encode(sample))
        # 使用writeframes(data)方法将bytes-like object数据写入
        # 该方法会自动且动态地修改记录在开头的总帧数
        t = t + 1
        # 移动到下一采样点/下一帧
    out.close()
    # 关闭文件

这个函数是写数据的,如不指定文件名,默认是song.wav,默认生成的时长是两秒

sampler,采样器,定义了如何去采样,比如三角波,sin波,锯齿波和矩形波等。给出的波振幅为1,也就是值域在-1到1之间。

关于encode(sample),它会将采样结果编码为WAV需要的二进制格式。因为writeframes会自己把其他信息处理好,因此只用考虑传入的数据即可,也就是每个点的采样。

def tri(frequency, amplitude=0.3):
    """A continuous triangle wave."""
    period = frame_rate // frequency
    def sampler(t):
        saw_wave = t / period - floor(t / period + 0.5)
        tri_wave = 2 * abs(2 * saw_wave) - 1
        return amplitude * tri_wave
    return sampler

frequency,频率,频率决定音高,频率高则音高

amplitude,振幅,振幅决定音量,振幅大音量大

period,采样率除以频率向下取整,f=1/T,则原式period=frame_rate*T,如果f是hz为单位,那么T就是以秒为单位的单位周期。因此相当于单位波形周期的采样数(单位是秒)。不太直观就拿软件画个图看看。

sampler传去的参数是t,代表第t帧。

saw_wave构造了一个锯齿波(别问,问就是画图才知道的),通过一次函数(斜线)减去阶梯函数图像得到的锯齿。

[CS61A]Week01笔记_第1张图片
这里我画的图不是完美的阶梯是因为x是1:0.1:10不够细

上图的蓝线是t / period,红线是floor(t / period + 0.5),黄线就是蓝线减红线,即得到的锯齿波saw_wave.

并且这很明显了,period决定了锯齿波的周期。

tri_wave构造了一个三角波

[CS61A]Week01笔记_第2张图片

我在matlab是这样画的上图,其中tri是红线,saw是蓝线

x = 0:0.01:10
saw = x/2-floor(x/2+0.5)
tri = 2*abs(2*saw)-1
plot(x,saw,x,tri)

返回的amplitude*tri_wave,即在原三角波的基础上乘以了振幅,相当于给波进行了纵向拉伸(原三角波振幅是1可以从图中得出来),拉伸之后的三角波振幅就变成了了amplitude。

为什么原三角波的振幅是1,这要从saw说起。看下图黄线是abs(2*saw),abs把原saw图像x坐标轴以下的部分给翻转上来,形成了一个三角波。看到这里就好理解了整个式子了。

[CS61A]Week01笔记_第3张图片

最后总结一句话,tri的作用,就是返回一个采样函数,这个采样函数的作用是依据相关上下文构建一个三角波。通过参数frequency和amplitude的变化改变音调和音量。

c_freq, e_freq, g_freq = 261.63, 329.63, 392.00
# 音符C/E/G大致对应的频率

play(tri(e_freq))
# 使用三角波输出内容是E音符的WAV音乐文件
# 到这里就已经可以自己构造某个音符的声音了
# 但是我们目前不能指定这个声音在播放多长时间(默认是两秒,上面定义了)
# 和在什么时间点开始播放
# 因此有了下面一系列函数

def note(f, start, end, fade=.01):
    """Play f for a fixed duration."""
    # 在固定的区间使用波形构造器f生成一段音波
    # 当然,音的具体细节是f里面指定的
    # start和end的单位是秒
    def sampler(t):
        seconds = t / frame_rate
        # 由于t代表第t帧,那么t/frame_rate得到对应所在的秒
        # 上面说了振幅决定音量,fade则对振幅做了些改变,做出来淡入/淡出的效果
        if seconds < start:
            return 0
        elif seconds > end:
            return 0
        elif seconds < start + fade:
            # 筛选出指定时间段的前fade秒
            # 并对振幅做处理
            return (seconds - start) / fade * f(t)
        elif seconds > end - fade:
            # 筛选出指定时间段的最后fade秒
            # 并对振幅做处理
            return (end - seconds) / fade * f(t)
        else:
            return f(t)
    return sampler

play(note(tri(e_freq), 1, 1.5))

play()的第一个参数是个采样器(生成波的函数),那因此note也得返回一个sampler,sampler是对原先的tri进行的封装,补充了更加细致的功能,所以本来就应该是一个sampler。

然后对第t帧采样之前得确保它在指定的播放范围里。不在的话就是0,即没有声音,因为play是从t=0开始遍历的,不能确保从0开始的所有经过的t都在指定范围里。

note()的作用是顾名思义的,播放一个升调,在指定的时间段内。

def both(f, g):
    # 混音,把两个音加一块播放
    return lambda t: f(t) + g(t)

c = tri(c_freq)
e = tri(e_freq)
g = tri(g_freq)
low_g = tri(g_freq / 2)
# 一些对于三角波音调的定义。
play(both(note(e, 0, 1/8), note(low_g, 1/8, 3/8)))

play(both(note(c, 0, 1), both(note(e, 0, 1), note(g, 0, 1))))

为什么play里可以加both?因为在play中应用sampler(t)的语句是sample = sampler(t),both返回的东西代入上文,则是sample = f(t) + g(t),两个sampler的结果相加。这是显然无误的。

为什么在时间轴上不重叠的音块也得用both让它们组合在一个文件里,因为note中对于不在制定时间段内的波,指定为了0,如果不使用both对两个波进行叠加还意图将两个波组合到一块,会导致后面的覆盖了前面的。

def mario(c, e, g, low_g):
    z = 0
    song = note(e, z, z + 1/8)
    z += 1/8
    song = both(song, note(e, z, z + 1/8))
    z += 1/4
    song = both(song, note(e, z, z + 1/8))
    z += 1/4
    song = both(song, note(c, z, z + 1/8))
    z += 1/8
    song = both(song, note(e, z, z + 1/8))
    z += 1/4
    song = both(song, note(g, z, z + 1/4))
    z += 1/2
    song = both(song, note(low_g, z, z + 1/4))
    return song

这里就构造出来了经典的马里奥游戏的开头音乐,song用来保存已经生成好的内容,通过不断移动游标z,使用both函数,将内容附加到song上,由于没有指定的区段note会返回0,而游标又是不断移动到上一个区间的末尾,因此总体效果就是对上一次添加内容的结尾进行追加。

def mario_at(octave):
    c = tri(octave * c_freq)
    e = tri(octave * e_freq)
    g = tri(octave * g_freq)
    low_g = tri(octave * g_freq / 2)
    return mario(c, e, g, low_g)

play(both(mario_at(1), mario_at(1/2)))

mario_at(octave)函数指定了这个音乐在哪个八度播放。说实话我也不太懂音乐,直观上的感受就是音乐的变化,octave是一个系数,能够让之前规定的各音符的值按octave这个比例来改变。

总的说来,这个例子展示了高阶函数的用法:最初的函数只承担一个简单的小功能,通过高阶函数这个手段不断在原先函数的基础上封装新的功能,同时还能保持已有的架构(指哪个play函数)不变,一步一步丰富完善各种细节,让它成为一个完备的程序。

课程提到了遇到性能瓶颈时可能会采用高阶函数,此处先记下来。

关于Python控制语句的if

def if_(c, t, f):
    if c:
        return t
    else:
        return f

from math import sqrt

def real_sqrt_v1(x):
    if x>0:
        return sqrt(x)
    else:
        return 0.0

def real_sqrt_v2(x):
    return if_(x > 0, sqrt(x),0.0)

v2版本的if,也就是自己写的jf,会把参数里的每一个都执行到,因此产生了错误(错误来自于sqrt不能传入负数参数)。

因为if_是一个函数调用表达式,因此在执行其函数之前,会将参数列表将要传入的东西执行完再传入作为参数,就好比print(4+4)得到的结果是8,而不是4+4一样,它传入的不会是表达式,只会是表达式最终计算得到的结果。

以上例子是为了展示函数的特性。

逻辑运算符的短路特性

python的逻辑运算符是单词,如and、or、not,对于C语言中的&&、||、!这三个符号。

python的逻辑运算符也存在短路性质。

逻辑表达式可以包含多个逻辑符号,按照优先级先后顺序计算,同等优先级则从左到右计算,例如由诸多or连接的表达式。

其中,短路的概念是,or表达式左侧如果值为True,此时结果一定为True,因此右侧的就不用计算了。

类似地,and表达式左侧为False则结果必为False,依然不用计算后面的表达式了。

此谓短路。

>>>True and 13
13
# 结果是最后一个运算的表达式,如果短路了,就是短路那个,不然一般是最后一个
# not的结果只有True和False
# 比较运算的结果是True或False之一

bool表达式的结果有一个False或者True之外的返回值很正常,因为它们本身也能表示True或者False,比如上例的返回值是13,作为一个Bool解释时,结果就是True,不影响使用。

Python一行中的if…else

类似于C语言的三目运算符A?B:C

if <判断结果的表达式> else

是的,条件写在中间…

abs(1/x if x 1= 0 else 0)
# 比如说这个例子,解决了0不能做除数(分母)的问题

关于Python的栈回溯

栈回溯,英文Traceback,原则是most recent call last,这意味着阅读顺序是从下往上,最下面是离事发地点最近的,往上则是其上级函数(或者调用发生事故这个语句/函数的函数)

Q: In the following traceback, what is the most recent function call?
Traceback (most recent call last):
    File "temp.py", line 10, in <module>
      f("hi")
    File "temp.py", line 2, in f
      return g(x + x, x)
    File "temp.py", line 5, in g
      return h(x + y * 5)
    File "temp.py", line 8, in h
      return x + 0
  TypeError: must be str, not int
Choose the number of the correct choice:
0) g(x + x, x)
1) h(x + y * 5)
2) f("hi")
? 2
-- Not quite. Try again! --

Choose the number of the correct choice:
0) g(x + x, x)
1) h(x + y * 5)
2) f("hi")

# 答案是1

关于lab02

Q: When should you use print statements?
Choose the number of the correct choice:
0) To investigate the values of variables at certain points in your code
1) For permanant debugging so you can have long term confidence in your code
2) To ensure that certain conditions are true at certain points in your code

答案是0

Q: How do you prevent the ok autograder from interpreting print statements as output?
Choose the number of the correct choice:
0) You don't need to do anything, ok only looks at returned values, not printed values
1) Print with # at the front of the outputted line
2) Print with 'DEBUG:' at the front of the outputted line

答案是2

Q: What is the best way to open an interactive terminal to investigate a failing test for question sum_digits in assignment lab01?
Choose the number of the correct choice:
0) python3 ok -q sum_digits --trace
1) python3 -i lab01.py
2) python3 ok -q sum_digits -i
3) python3 ok -q sum_digits

答案是2

Q: What is the best way to look at an environment diagram to investigate a failing test for question sum_digits in assignment lab01?
Choose the number of the correct choice:
0) python3 ok -q sum_digits
1) python3 ok -q sum_digits --trace
2) python3 ok -q sum_digits -i
3) python3 -i lab01.py

答案是1

python函数名后面跟多个括号

这是homework02的一个知识点这种函数的格式如下:

function1(a)(b)

上例的意思就是,function1传入一个参数a,但是它还返回一个函数,打算执行这个函数,对于这个函数传入的参数是b。

多个括号同理,前面那个括号修饰的函数有一个函数返回值,此时要执行那个返回的函数,因此再加一个括号。

def a(n):
    print(n)
    return lambda x:print(x)

a(2)(3)

# 输出:
# 2
# 3

显然 第二个括号里3是对返回函数调用执行时传的参

本质相当于

def a(n):
    print(n)
    return lambda x:print(x)
b= a(2)
b(3)

hw02的邱奇自然数问题

直观上就是函数(名)表示一个数。

根据皮亚诺公理,已知:0是自然数,0可以用一个函数表示。每个自然数的后继(即+1)是自然数。后继可以认为是一个过程,也就是下面的successor函数。

def zero(f):
    return lambda x: x

def successor(n):
    return lambda f: lambda x: f(n(f)(x))

在传统的表示数的系统中,1就是0+1,2就是1+1,3就是2+1

但是由于2是1+1,那么三就是1+1+1

最终任何数都会化为(0)+1+1+1+1…+1的形式。

我们知道+1或者说后继可以是一个过程,那么显然1可以表示为

successor(zero)

同理,二就是

successor(successor(zero))

很明显,嵌套的次数就是那个要表示的数。因此我们拆开某个函数一层层嵌套直到遇到zero(),其间经历的successor的个数就是该函数表示的数,这也就是邱奇自然数转化为python常规整数的算法思路。

来看看successor,为什么return了一个lambda f?因为zero的参数是f,那么其他数应该类似,参数也是f。但是zero本身还会return一个lambda x,那么successor返回的这个函数,也应该return一个lambda x。

好吧…后面的东西还是没搞太懂,抽空再写吧。

函数柯里化(Function Currying)

柯里化,根据维基百科的解释,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

from operator import add, mul

def curry2(f):
    """Curry a two-argument function.

    >>> m = curry2(add)
    >>> add_three = m(3)
    >>> add_three(4)
    7
    >>> m(2)(1)
    3
    """
    def g(x):
        def h(y):
            return f(x, y)
        return h
    return g

此时curry2亦可写为:

curry2 = lambda f: lambda x:lambda y: f(x,y)

依然可以使用

>>>m = curry2(add)
>>>m(2)(3)

函数抽象

from operator import mul
def square(x):
    return mul(x, x)
def sum_square(x, y):
    return square(x) + square(y)

对于sum_square

  • 需要知道square需要几个参数
  • 需要知道square是做什么的(返回了x*x这个值)
  • 不需要知道square具体怎么实现的(x*x或者x^2都行)
  • square只是个代号,名字未必就是square

关于函数名

  • 函数取什么名不影响执行
  • 函数的名字会影响可读性,因为代码是给人看的
  • 所以函数的名字应当表达清楚这个函数的目的/是做什么的
  • 函数接受的参数也应当在docstring里写清楚,比如说某参数接受一个数/字符串
    `

同理,二就是

successor(successor(zero))

很明显,嵌套的次数就是那个要表示的数。因此我们拆开某个函数一层层嵌套直到遇到zero(),其间经历的successor的个数就是该函数表示的数,这也就是邱奇自然数转化为python常规整数的算法思路。

来看看successor,为什么return了一个lambda f?因为zero的参数是f,那么其他数应该类似,参数也是f。但是zero本身还会return一个lambda x,那么successor返回的这个函数,也应该return一个lambda x。

好吧…后面的东西还是没搞太懂,抽空再写吧。

你可能感兴趣的:(python,python)