[CS61A]Week03笔记1

week03笔记

List

List(列表)是python内置的数据类型,它被应用于python各处。

List通常至少有以下性质:

  • 使用[]来包围参数来声明一个List
  • 也可以使用list()来构造一个
  • 可以绑定到name上
  • 可以使用下标访问,下标从零开始。对应List从左开始的第一个元素
  • 有很多配合list使用的方法,例如len()
  • 可以使用list1 * k让这个list1这个list扩大k倍,内容是重复填充已有内容
  • 如果a和b都是list,可以使用a + b将a和b连接起来,b在a后面
  • List的内容可以是任何东西,包括另一个List
  • in 关键字的使用
  • 列表的成员有序、允许重复且内容动态可变更

由于List含有的元素还可以是List,这样List本身就可以达到类似于C语言的二维数组的效果,如下:

>>> List1 = [[30,34],[23,43]]
>>> List1[1]
[23, 43]
>>> List1[1][0]
23

in关键字的使用例子如下,显然不难得知这是一个布尔表达式,返回True和False之一

>>> a
[1, 23, 45, 34]
>>> 23 in a
True
>>> 2 in a
False
>>> 3 not in a
True
# 等同于
>>> not(3 in a)
True
# 只会查找元素,不会查找子List
>>> [1, 23] in a
False
# 下式[1, 2]就作为一个元素可以被查到
>>> [1, 2] in [3, [1, 2], 4]
True
# 当然,不能嵌套的太深,不然也找不到
>>> [1, 2] in [3, [[1, 2]], 4]
False

For语句(List的迭代)

基于while写一个计数并返回某个可以使用下标访问的序列s里value出现的次数的函数。

def count_while(s, value):
    total, index = 0, 0
    while index < len(s):
        if s[index] == value:
            total = total + 1
        index = index + 1
    return total

相同地可以使用for完成

def count_for(s, value):
    total = 0
    for elem in s:
        if elem == value:
            total = total + 1
    return total

很明显python的for和C语言的for是不一样的。

for循环的使用涉及到一个名值绑定问题,在上例中是把某些值绑定到elem这个name上。这个过程是在当前frame中进行的,而不是新的frame,每绑定一遍就会执行一遍函数体。

for <name> in <expression>
    <suite>

上面就是for语句的格式

  1. 首先计算<expression>,它应当产生一个可迭代的值
  2. 对于这个值的每一个元素,按先后顺序对它们:
    1. 在current frame将该value绑定到name上
    2. 执行<suite>

数据的拆箱(unpacking)

与拆箱相反的过程,称之为装箱。

>>> pairs = [[1, 2], [2, 2], [3, 2], [4, 4]]
>>> same_count = 0
>>> for x, y in pairs:
        if x == y:
            same_count += 1
>>> same_count
2

为长度固定的序列中每个元素取名就是一个拆箱的过程。

Ranges

Ranges are sequence type

A range is a sequence of consecutive integers

Ranges是一种序列类型(List也是)。Range是一段连续的整数的序列。

事实上也未必是连续的(consecutive)

range(x, y)表示的序列,由x起始,到y之前(不包含y),y-x就是包含元素的个数

......,-4 ,-3, -2, -1, 0, 1, 2, 3, 4, ......
              └──range(-2,3)──┘

例如range(-2,3)包括的值就是-2, -1, 0, 1, 2,为了验证这一点,可以使用如下语句

>>> list(range(-2,3))
[-2, -1, 0, 1, 2]
>>> list(range(4))
[0, 1, 2, 3]
>>> [i for i in range(3)]
[0, 1, 2]

对于以上有两点说明:

  • range只有一个参数时,默认第一个参数为0,接受的参数会绑定到第二个形参上
  • list()可以构造出一个列表

或者还有:

>>> a = [0, 1, 2, 3]
>>> [x+3 for x in a]
[3, 4, 5, 6]
# 甚至还可以加if表达式
>>> a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
>>> [i for i in a if 24 % i == 0]
[1, 2, 3, 4, 6, 8, 12]
>>> 

然后甚至还可以这样求最大公约数:

>>> a = [i for i in range(24)]
>>> b = [i for i in range(36)]
>>> a = [x+1 for x in a]
>>> b = [x+1 for x in b]
>>> c = [x for x in a if (24 % x == 0)]
>>> d = [x for x in c if 36 % x == 0]
>>> max(d)
12

有没有感觉好像linq?不过在我即兴写完这个之后,老师果然又讲了类似的:

def divisors(n):
    return [1] + [x for x in range(2, n) if n % x == 0]

可以使用上面的函数来对其参数n求因数

>>> divisors(24)
[1, 2, 3, 4, 6, 8, 12]

String

string是字符串的意思,在python中它会按每个字符的编码储存。

字符串能表示很多东西

  • 一些数据,如’23.02’或者’1.2e-5’或者’False’
  • 一些自然语言语句,如"这是一段话"
  • 一段程序,如"curry = lambda f: lambda x: lambda y: f(x, y)"

但这设计String的目的并不是让编码者关注如何储存这种细节的,更多的关注点应当在表示的字面意义上。

from operator import add
>>> exec("curry = lambda f: lambda x: lambda y: f(x, y)")
>>> curry
<function <lambda> at 0x000001FAB4B5E8B0>
>>> curry(add)(3)(4)
7

字符串使用引号包裹,单引号和双引号区别不大,但是一对单引号里面不能直接出现另一个单引号,比如要输入I'm Albert

>>> a = 'I\'m Albert'
>>> a
"I'm Albert"
>>> b = "I'm Albert"
>>> b
"I'm Albert"
>>> c = """I'm A
... lbert"""
>>> c
"I'm A\nlbert"

由两组"""包裹的可以作为多行字符串/注释使用,但是作为多行字符串是会记录下换行信息的,使用\n这个转义字符来记录在字符串内。

# 可以使用len()来获取字符串长度
# 转义字符由多个字符表示,但是算作一个字符
>>> c = "I'm A\nlbert"
>>> len(c)
11
# 可以使用[]和想应的下标访问字符串某几个元素
# 下标从左到右从零递增
>>> c[0]
'I'

此外还可以使用in

>>> 'here' in 'Where are you now'
True
# 当然还是要强调下面这个
>>> [2, 3, 4] in [1, 2, 3, 4, 5]
False

Data Abstrcation

An abstruct data type let us manipilate compound objects as units

抽象数据类型使我们能够以复合对象为单位进行操作

ADT,abstruct data type,数据结构这里有讲过,是不是很熟悉?

数据抽象将数据的表示方式与操作方式分离。表示是一部分一部分的,比如日期是由年月日组成,但是使用是整体的,某个变量就可以表示一个完整的日期。

Data abstraction: A methodology by which functions enforce an abstraction barrier between representation and use

数据抽象是一种加强数据的表示与操作之间的抽象障碍的方法。

“是个程序员都需要在某些时刻处理复合数据,但只有伟大的程序员才会使用数据抽象来让程序更加模块化”

网上说python中的list就是一种抽象数据类型。

根据维基百科的解释:

抽象数据类型(Abstract Data Type,ADT)是计算机科学中具有类似行为的特定类别的数据结构的数学模型;或者具有类似语义的一种或多种程序设计语言的数据类型。

数据抽象是计算机科学中一个强大的概念,它允许程序员将代码视为对象——例如,汽车对象、椅子对象、人对象等。这样程序员就不必担心代码是如何实现的——他们只需要知道它的作用。

数据抽象模仿我们对世界的看法。当你想开车时,你不需要知道发动机是如何制造的或者轮胎是用什么材料制成的。你只需要知道如何转动方向盘并踩下油门踏板。

抽象数据类型由两种类型的函数组成:

  • 构造函数(constructor):构建抽象数据类型的函数。
  • 选择器(selector):从数据类型中检索信息的函数。

程序员设计ADT以抽象出信息是如何存储和计算的,这样最终用户就不需要知道构造函数和选择器是如何实现的。抽象数据类型的性质允许任何使用它们的人假设函数已正确编写并按描述工作。

自定义类型Rational

根据我读的数据结构书讲,ADT既包括数据对象,又包括数据关系,还包括基本运算。

ADT = (D, S, P)
D: 数据对象
S: D上的关系集
P: D上的操作集

任何学过cpp的人在学习面向对象时可能都做过实现一个复数类,回忆至此就不难发现,那其实可能就是最早实现的ADT。ADT的意义就是能够让我们将基础已有的数据类型组合作为整体使用。

菜鸟教程里讲C++数据抽象的文章开篇就说到:数据抽象是指,只向外界提供关键信息,并隐藏其后台的实现细节,即只表现必要的信息而不呈现细节。

这样的抽象实际上是在运用时的化繁为简。因为实现细节被隐藏了,取而代之的是一个专门功能的函数,因此不必去理解实现原理并敲一大堆实现代码,只需要调用已经封装好的函数就行了。

显然,一个实数可以表示为分子/分母,也就是由两个整数复合而成。

为什么需要一个复合类型?因为当除法发生时,有可能会损失精度(计算机表示浮点数也是有位数限制的)

我们思考它至少有这样的功能(但显然是不完备的):

  • rational(n, d)返回一个实数x
  • numer(x)返回实数x的分子
  • denom(x)返回实数x的分母

其中rational(n, d)也被称为constructor(构造函数),numer(x)叫做selectors(选择器)

在这里分子分母就是数据对象,实数由分子/分母构成就是数据关系,numer(x)就是基本运算

有了这些东西,就可以进一步地写出更为复杂的操作:

# 加法
def add_rational(x, y):
    """The sum of rational numbers X and Y."""
    nx, dx = numer(x), denom(x)
    ny, dy = numer(y), denom(y)
    return rational(nx * dy + ny * dx, dx * dy)
# 乘法
def mul_rational(x, y):
    """The product of rational numbers X and Y."""
    return rational(numer(x) * numer(y), denom(x) * denom(y))
# 比较是否相等
def rationals_are_equal(x, y):
    """True iff rational numbers X and Y are equal."""
    return numer(x) * denom(y) == numer(y) * denom(x)

上面提到了数据抽象将数据的表示方式与操作方式分离。现在已经写出了操作方式,但是如何表示还不清楚,rational(n, d)还未实现。在Cpp中可能会想到定义一个class,那么在python中该采用什么样的方式呢?

一个实数有分母和分子,这是一对(pair)数据,我们考虑使用list表示Pairs可以看到它有如下特性:

>>> x = [2, 3]
>>> a, b = x
>>> a
2
>>> b
3
# x归根到底还是个list,因此可以下标访问
>>> x[1]
3
# 或者这样
>>> from operator import getitem
>>> getitem(x,1)
3

有必要指出的是这个unpacking过程中等号左侧的变量个数要和右侧列表内部元素的个数一致,如果上面的列表x = [2, 3, 4]再用a, b = x就不行了。这时候就得用a, b, c = x才可以。

因此对于数据的表示相关的内容就可以有如下代码实现

def rational(n, d):
    """A representation of the rational number N/D."""
    return [n, d]

def numer(x):
    """Return the numerator of rational number X."""
    return x[0]

def denom(x):
    """Return the denominator of rational number X."""
    return x[1]

以上就简要地实现了rational这个ADT,但是这些功能是远远不够的,比如并不能化简分数,例子如下:

>>> x = rational(2,3)
>>> y = rational(3,4)
>>> print_rational(mul_rational(x, y))
6 / 12

对此需要进行的操作是重写rational(n, d)

from fractions import gcd
def rational(n, d):
    """A representation of the rational number N/D."""
    g = gcd(n, d)
    return [n//g, d//g]

实现了rational的数据抽象,就自然而然要说它为什么有用。上面重写了rational,但是不用改变任何其他的代码,依旧可以正常使用,因为数据对象的逻辑关系规范了他们的格式,所有按照格式延伸出来的功能都不会受到影响。

当使用rational相关定义以期望计算1/2 * 1/3的值时,只需要使用一个rational变量即可,它们代表一些实数,使用者不必关系如何实现的,事实上值得使用者关心的只有运算结果,因此也提供了诸如add_rational()之类的函数用于数据抽象,为使用者对数据进行操作提供了手段。

老师一再强调的是使用这些实数和它的操作函数不意味着要去理解实数是如何被表示的。

但是在创建一个实数或者构建一些实数的运算时,与使用者把rational整体当做一个数据值不同,这时需要理解实数使用分子和分母构成表示,这时用到的就是rational()/numer()/denom()这三个。

再往下一层,实现rational()/numer()/denom()这三个函数时,需要将实数视为只有两个元素的list,用到的也只有list和列表元素的选择。

甚至再往下,还有list的实现,这时候又要考虑和上面更不一样的东西

这就体现了抽象壁垒。每一层考虑的内容都不一样,也不必考虑更加底层的东西。就比如实现rational()的人不用考虑list是怎么实现的,只要会用list就行了。类似地,使用rational类数据去设计程序的人不必考虑该ADT是如何实现的,这也是和上面一样的道理。

在实现一个大程序的每一部分时,应当明确它属于诸多抽象层的哪一部分,以及进一步明确该层的任务是什么。当每一层的界限都很明确时,在未来修改(改动细节、添加功能)这个程序也会得到遍历

一个不好的例子是这样的:

add_rational([1,2], [2, 4])
def divide_rational(x, y):
    return [x[0] * y[1], x[1] * y[0]]

这打破了抽象壁垒,在加法函数里我们使用了rational的constructor,这是不合规范的。本例固然能够运行,但是当我们改变了constructor后就未必能够正常运行,由此引发的后续修改的代价可能会非常大。

另外一个问题就是divide_rational函数,它return语句中将x和y当做list来处理了,事实上x的实现未必是list,并且即使现在是,以后随着设计上的更改,和功能的增加,未必会再选用list来表示一个rational,因此它的实现过于深入底层,打破了抽象壁垒,这是不对的。

就连返回的东西也未必应当是list,而是使用constructor进行构造才是正确的做法。之前有提到DRY原则,这种东西不做成函数其实本身就是一种重复,毕竟后面很多功能都要用到这个。

回想起来从最一开始讲高阶函数时就有这种体现,不断地从基础功能做起,每个函数紧密地利用了之前实现的功能,基于它们进一步地构造了新功能,每个函数承担一个功能。

Abstraction Barrier

上面一大块内容都是在讲抽象壁垒。

保持抽象壁垒的意义就是保证局部的改动不会影响其他部分。就好比一块面包板换个器件插上去(修改某一块的功能),只要引脚对应好(符合已经设计好的接口),就能够正常使用。

如果打破了抽象壁垒,各个模块/功能间相互有重叠,这时候对某一部分内容的改动可能就是牵一发而动全身的,修改的可能遍及整个程序。

想到这里总能联想到平常常说的"低耦合"的思想…

回想起来这些东西都是在以前学习编程中有意无意地用到的,但是那种使用尚不能成为一种思考方式,不过在学习了CS61A之后,从思考方式上都会有所转变,我想这就是收获。

Data Representation

数据的表示。

拿rational来说,我们确保constructor和selector一起工作,使一些数据能够有正确的行为。因为只有它表现地像一个rational时,我们才会认为这是一个实数。也因此它就得由两个数构成,一个是分母(可以由denom获得),一个是分子(由numer获得)。这样我们才会认为它具有实数的特性。

毕竟哪怕是写在纸上运算,rational也是由上方的分母和下方的分子以及中间的横线构成。运算时,我们也需要获取它的分子和分母进行计算。

数据表示使用constructor和selector来定义行为。

如果满足行为条件,这样的表示就是有效的。

也就是说,有一个rational(n, d)并不意味着实现了rational的表示,还得辅以numer()denom()才行。

You can recognize data abstraction by its behavior

可以通过其行为来识别是哪种数据的抽象

而不是依靠constructor和selector的实现来识别

constructor和selector之间是相互补充的。

def rational(n, d):
    g = gcd(n, d)
    n, d = n//g, d//g
    def select(name):
        if name == 'n':
            return n
        elif name == 'd':
            return d
    return select

def numer(x):
    return x('n')

def denom(x):
    return x('d')

把之前的constructor和selector改写为如下内容,这样某个rational变量x就成了一个函数,但是这不影响其他部分的运行,你依旧可以像之前一样使用它们:

>>> x, y = rational(1,2), rational(3, 8)
print_rational(mul_rational(x, y))
3 / 16

甚至这样还免去了使用内置的list类型。(P.S.我怎么感觉跟之前那个邱奇自然数有点像啊…

这样的使用放在修改前和修改后都是有效的,不必再去修改诸如mul_rational()之类的函数。

数据的表示:数据的构成(分子&分母两个数)、数据的性质(写下一个分数很自然地就知道它的分子是多少,分母又是多少)、数据基于已有的约定或者规则构建出来的行为(分数的加减乘除)

Dictionary

A dictionary allows you to associate values with keys

特点:

  • 包括在一对花括号{}内,花括号英文是curly brace
  • dict是关键字
  • 键(key)是唯一的,重复赋值只会导致替换
  • 值(value)不是唯一的
  • 可以储存任何对象
  • 可以使用len()获取其含有多少元素
  • 可以使用in和for语句
  • dictionary是无序的
>>> numerals = {'I': 1, 'V': 5, 'X': 10}
>>> numerals['X']
10
# 使用dict.keys()获得其中的key
>>> numerals.keys()
dict_keys(['I', 'V', 'X'])
# 使用dict.values()获得其value构成的list
>>> dict.values(numerals)
dict_values([1, 5, 10])
# 等同于
>>> numerals.values()
dict_values([1, 5, 10])
# 或者获得整个键值对构成的list
>>> numerals.items()
dict_items([('I', 1), ('V', 5), ('X', 10)])
# 当然这个过程是可逆的
>>> item = ([('I', 1), ('V', 5), ('X', 10)])
>>> dict(item)['X']
10
# in语句的使用
>>> "X" in numerals
True
# 对于不确定的元素的尝试获取
# 如果没有该key就返回第二个参数
>>> numerals.get('X',0)
10
>>> numerals.get('x',-1)
-1
# for语句的使用
>>> squares = {x:x*x for x in range(10)}
>>> squares[7]
49
# 不能重复声明两遍一个key,会覆盖
>>> {1: 3, 1: 4}
{1: 4}
# 可以使用各种对象作为value
# 当然dictionary和list不能作为key
>>> a = {1:{1:'a',2:'b'},2:[2,3]}
>>> a[1][2]
'b'
>>> a[2][0]
2

关于List

s = [2, 3]
t = [5, 6]
>>> s.append(t)
>>> t = 0
>>> s
[2, 3, [5, 6]]

对于append的用法(有点像链表了啊

s ──────>  | 2 | 3 | t |
            ┌────────┘
            ▼
t ──────>  | 5 | 6 |

这时即便t = 0了,原先的[5, 6]依然存在,并且被s的第三个元素指向。

>>> s = [2, 3]
>>> t = [5, 6]
>>> s.extend(t)
>>> s
[2, 3, 5, 6]
>>> t
[5, 6]

此时内存环境是这样的

s ──────>  | 2 | 3 | 5 | 6 |
t ──────>  | 5 | 6 |

但是对于a = s + [t]其环境图示是这样的

s ──────>  | 2 | 3 |
t ────────────────────────>  | 5 | 6 |
                               ▲
                        ┌──────┘
a ─────────>  | 2 | 3 | ● |

这时使用a[1] = 9不会改变s[1]的值

加上b = a[1:]之后

s ──────>  | 2 | 3 |
t ────────────────────────>  | 5 | 6 |
                               ▲
                        ┌──────┤
a ─────────>  | 2 | 3 | ● |    │
                        ┌──────┘
b ─────────────>  | 3 | ● |

因此+和切片同时进行是创建新的list的

对于list()构造的新list,是将原有的复制出一份新的

>>> s = [2, 3]
>>> t = list(s)

其环境结构如下图所示

s ──────>  | 2 | 3 |
t ──────>  | 2 | 3 |

但是只进行切片操作是在原list基础上插入的

>>> s = [2, 3]
>>> t = [5, 6]
>>> s[0:0] = t
>>> s
[5, 6, 2, 3]
# 此时s和t分别占用一块空间,修改一个list的元素不会影响另一个

<list_var>.pop()会把list的最后一个元素删掉并返回该元素

>>> s = [2, 3]
>>> t = [5, 6]
>>> t = s.pop()
>>> s
[2]
# 注意t是数字不是list
>>> t
3

<list_var>.remove(value)会把list的第一个和参数匹配的元素删掉

>>> t = [5, 6]
>>> t.extend(t)
>>> t
[5, 6, 5, 6]
>>> t.remove(6)
>>> t
[5, 5, 6]

list的切片也可以赋值,如果赋值的内容是空列表[]就相当于remove

>>> s = [2, 3]
>>> t = [5, 6]
>>> s[:1] = []
>>> t[0:2] = []
>>> s
[3]
>>> t
[]

对于某一list变量x,x[a:b]意味着从索引为a开始到b之前的一个变量,但不包括b本身。如果a = b那么这相当于光标,就是在下标a之前的空隙进行操作,例如赋值(其实就是相当于一种插入)

倒数第一个元素的索引是-1,往前数一个就是-2,以此类推。

Debugging

assert

断言用于判断一个表达式,在该表达式的结果为False时触发异常

assert expression
# equals to
if not expression:
    raise AssertionError

加参数的assert格式如下

assert expression [, arguments]
# equals to
if not expression:
    raise AssertionError(arguments)

可以使用assert来确保一些按道理需要"恒成立"东西,比如说确保某些值在运行时永远为正数之类的。因为为负数就会触发异常。比如说开平方的参数应该是个正数。

老师给出的建议是

  • 在实际开发和理解他人代码时用途不大
  • 最好在自己的代码里使用

Testing

测试的目的

  • 检测代码中的错误
  • 能证明代码在某些情况下至少是能用的
  • 缩小调试范围
  • 测试的正确的样例可以写到文档/doctests里

Doctest

  • 只需要写在函数开始的多行注释里
  • 使用python -m doctest xxx.py运行

Print debugging

为什么需要这种调试

  • 简单
  • 便捷
  • 直观
  • 不用在脑子里记忆和推导过多的东西

比如说对于阶乘的求解函数

def fact(x):
    if x == 0:
        return 1
    else return x * fact(x - 1)

def half_fact(x):
    return fact(x / 2)

假如输入了一个half_fact(10)会出问题,但是在不知道问题在哪时,也许会使用断言

def fact(x):
    assert isinstance(x, int)
    assert x >= 0
    if x == 0:
        return 1
    else return x * fact(x - 1)

但是这样做还得分析这些变量的那些恒成立的条件,而且分析到的也未必有用,并且在复杂的函数中思考量也未必小。

如果print出每回的x就很直观了

def fact(x):
    assert isinstance(x, int)
    assert x >= 0
    if x == 0:
        return 1
    else return x * fact(x - 1)

当然,print debugging会造成额外的输出,在一些用于通过测验的代码中,这会使输出结果与测验期望的不一致,因为print了很多中间值。应当在提交代码之前去掉它们。

interactive debugging

使用python提供的REPL来在python提供的交互式环境中进行调试

只需要键入如下命令即可python -i xxx.py

本课程提供的python ok也可以使用类似的功能,命令是python ok -q question_name -i,其中question_name应当替换成对应问题的名字

CS61A还提供了在线的可视化调试,访问tutor.cs61a.org即可使用

或者使用ok的python -q question_name --trace

Error Types

  • Syntax Error(语法错误)
    • 比如缺少了半边括号
    • 比如打错字
    • 比如int print
    • 少了个冒号for i in range(1,6)
  • IndentationError
    • 没啥好说的,就是缩进不对呗
  • TypeError
    • 参数传的数目不对
    • int与string相加
    • 比如1 + print
    • 某函数形参是一个函数但是传入一个数值
  • Name Error
    • 不能找到绑定值的name
    • 比如变量未声明或者函数名拼写错误
  • ZeroDivisionError
    • 除数为零
  • IndexError
    • 索引/下标超出范围

Tracebacks

def f():
    1 / 0
def g():
    f()
def h():
    g()
h()

运行这个例子显然会得到报错,其部分信息如下

Traceback (most recent call last):
  File "temp.py", line 7, in <module>
    h()
  File "temp.py", line 6, in h
    g()
  File "temp.py", line 4, in g
    f()
  File "temp.py", line 2, in f
    1 / 0
ZeroDivisionError: division by zero

根据提示,距离产生错误最近的调用在最底部,上例中就是line 2的函数f。

自顶向下是一个调用的过程,也就是最初源自于使用了h函数,该函数调用了g函数,最终g函数调用了f函数,产生了错误。

当然,首先先应当看错误的类型,根据错误的提示再去看栈回溯,从底部往上分析到底是哪一个环节导致了错误。但是需要注意的是并不总是由于最底部的那个调用产生错误。

Exception

程序的出错原因是极为丰富的,平常遇到的错误和上面的比起来可能更加纷繁。比如说

  • 用到的某些源文件不可用/不存在/被占用
  • 在数据传输过程中网络突然断开

甚至世界上第一个电脑的bug,是由一只蛾子飞入电脑导致的故障(这也是bug代指错误的起源)

不过总而言之在电脑程序运行阶段其错误是各种各样的,我们不知道什么是错误的,但是我们知道什么是正确的就够了。

编程语言可能会内置一种处理异常的机制(mechanism)

Python raises an exceptions whenever an error occurs

def f():
    1 / 0
def g():
    f()
def h():
    g()
h()
print('11111')

还是这个例子,该程序在h()出错之后讲就没有执行后面的print语句。在此前没接触异常处理时,程序出错往往意味着会导致该程序终止。但是事实上产生了错误可以由程序本身处理,如果程序本身能够处理发生的错误,就不会导致程序被python解释器终止。

而对于没有被程序处理的异常,会导致Python停止执行程序,并且打印栈回溯

关于python中的异常需要知道如下内容

  • 异常是对象(Object)
    • 是有构造函数(Constructor)的类
  • they enable non-local continuations of control
    • 其实说的就是可以改变函数的执行顺序,比如略过某些函数
  • 异常处理可能很慢
  • 异常的处理分为两个步骤
    • 异常的raise和handling
    • 其中assert是最简单的raise异常的方式

有必要说明的是使用python -O这个-O参数可以不执行任何断言语句。O代表optimized。注意是大写的O!

对于-O参数,如果 Python 没有以-O选项启动,则__debug__常量为真值。根据官方文档的解释是,该参数会使python移除assert语句以及任何以__debug__的值作为条件的代码。

官方文档对于raise的解释是如下

简单形式 assert expression 等价于:

if __debug__:
    if not expression: raise AssertionError

扩展形式 assert expression1, expression2 等价于

if __debug__:
    if not expression1: raise AssertionError(expression2)

Try Statements

在java中异常的处理是try-catch语句,而在python中则是try-except语句。

执行顺序是这样的:

  • 先执行try语句体的内容
  • 执行过程中可能会有异常被raise
  • 如果异常被raise并且没有被处理,就得看它的类型
  • 如果后面有对应类型的except语句就会执行该语句的语句体
  • try要和except写在一块,中间不能有相同缩进的其他无关语句

一个例子是这样的:

try:
    x = 1 / 0
    print("1111")
except IndexError:
    print("Index")
except TypeError:
    pass
except ZeroDivisionError:
    print("Zero")

并且显然地得到输出zero

由上不难知:

  • 只会执行对应错误类型的except语句体
  • try语句内部抛出异常后不会执行后面的语句(改变了语句顺序)

对于异常的except语句还有一种使用as的写法

try:
    x = 1 / 0
    print("1111")
except ZeroDivisionError as e:
    print('handling a ', type(e))
    x = 0 # 对于因异常导致没有赋值成功的最好给个默认值

输出是handling a <class 'ZeroDivisionError'>

上面说了"如果异常被raise并且没有被处理",什么叫没有被处理?看下面的程序

def invert(x):
    y = 1 / x
    print('Never printed if x is 0')
    return y

def invert_safe(x):
    try:
        return invert(x)
    except ZeroDivisionError as e:
        print('handled', e)
        return 0

-i参数运行并进行如下操作会得到这样的结果:

>>> try:
...     invert_safe(0)
... except ZeroDivisionError as e:
...     print('handled')
...
handled division by zero
0

由于invert_safe()里的异常被处理了,因此轮不到外面这个except处理这个异常。

Example:Reduce

这里的Reduce的意思应该是简化而不是减少

给定函数原型reduce(f, s, initial),如reduce(mul, [2, 4, 8], 1)相当于mul(mul(mul(1, 2), 4), 8),其结果是64。

就是说这是使用一个传入的函数f,该函数接受两个参数,然后对于一个序列和一个起始值,用过程f处理序列的第一个参数和初始值,然后结果和第二个参数用f处理。依次类推直到把整个序列都处理完,返回其结果。

我一看,啪的一下,很快啊,就写完了。

def reduce(f, s, initial):
    i = 0
    while i < len(s):
        initial = f(initial, s[i])
        i += 1
    return initial

好吧,其实应该用for会更加简洁

def reduce(f, s, initial):
    for x in s:
        initial = f(initial, x)
    return initial

然后老师还给出了递归版本的:

def reduce(f, s, initial):
    if not s:
        return initial
    else:
        first, rest = s[0], s[1:]
        return reduce(f, rest, f(initial, first))

然后就是最后的正题内容,异常处理:

def reduce(f, s, initial):
    if not s:
        return initial
    else:
        first, rest = s[0], s[1:]
        return reduce(f, rest, f(initial, first))

def divide_all(n, ds):
    try:
        return reduce(truediv, ds, n)
    except ZeroDivisionError:
        return float('inf')

print(divide_all(1024, [2, 4, 8]))
print(divide_all(1024, [2, 4, 8, 0]))

输出是16.0inf

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值