Skip to content

理论考前常见问题总结

说明

这里的问题总结自大家向 AI 助教提问以及向助教提问时比较频繁提到的问题. 希望大家在理论考试之前通读一遍,确保对里面的问题有所理解.

数据类型

andor,布尔连词

大家经常问的一个问题是,"3" and 0 or 3 这类表达式的值为何为 3,而不是我们通常认知的某个“逻辑值”. 在此,首先需要提醒的是,Python 中所谓的布尔连词并非按照我们通常认知的方式执行,它的执行方式在之前的文档中有所介绍,在此展示一表:

运算 结果:
x or y 如果 x 为真值,则 x,否则 y
x and y 如果 x 为假值,则 x,否则 y
not x 如果 x 为假值,则 True,否则 False

它按照运算符优先级升序排列. 注意上面的表述是按照 x 的真假值判断,其运算结果除了 not 会给出符合直观的 TrueFalse 以外,andor 两个运算符返回的结果是 xy 之一. 按照这样的逻辑,我们也可以理解另一个容易出现在考试中的点,即短路(short-circuiting),考虑下面的代码:

print(1 and print("done")) 

这时,为了计算 and 的结果,因为 1 是真值,所以 print("done") 被执行了. 而且 print 函数返回 None 值,因此,输出的结果是:

done
None

而如果考虑代码:

print(1 or print("done")) 

现在 1 为真值,所以它的结果就是 1. 这里没有提到过 y 的执行,即 print("done") 不会被执行. 这就是我们通常所谓的短路规则:如果逻辑表达式的值已经被确定,那么后面的代码不会被执行. 实际上,只要记住并严格遵照上面的表格给出的计算规则就可以解决各种相关的问题.

字典操作以及 None 返回值

请看下面的代码,这在我们的作业中出现过:

dic1={"赵洁" : 15264771766,"张秀华" : 13063767486,"胡桂珍" : 15146046882,"龚丽丽" : 13606379542,"岳瑜" : 13611987725}
dic2={"王玉兰" : 15619397270,"王强" : 15929494512,"王桂荣" : 13794876998,"邓玉英" : 18890393268,"何小红" : 13292597821}
dic3=dic1.update(dic2)
print(dic3["王强"])

这段代码会报错,为什么?注意看第三行的地方,update 方法的返回值被赋给了 dic3. 但是,update 方法的返回值到底是什么?我们容易直觉地认为,update 的结果是 dic1 更新之后的结果,但并非如此,这个结果被存回了 dic1 中,而返回值则是 None. 对于 None 值,取下标的操作是错误的,因此会报错.

另一个问题是,在我们的最后一次作业中,出现了这样一个类似这样的填空题:

x = ________
for i in range(10):
    x[i] = 0
...

这里的选项有空列表和空字典. 区别二者的重点在于,对空列表直接取下标赋值是会报错的,因为它会进行长度的检查,而对空字典取下标赋值会向其中增加一个新的键值对,因此可以通过这样的代码完成初始化.

另外,字典的迭代同样频繁被问及:

for i in {1: 2, 3: 4}:
    print(i)

它的执行结果是

1
3

也就是说,在对字典迭代时,迭代的是键. 如果要迭代值和迭代键值对,应当使用 valuesitems 两个方法.

列表推导式

考虑下面的代码:

myth=[{'label': color, 'value': color} for color in ['blue', 'red', 'yellow']]

这种列表推导式通常的读法就是:

  1. 将其拆成三段,for 前面,forin 之间以及 in 后边.
  2. 观察最后一段中的东西,它应当是一个可迭代对象,其中的东西被一个一个拿出来,解包到 forin 之间的变量当中.
  3. 根据这样的赋值计算 for 前面的表达式,这应当会被迭代多次,然后一次一次得到的东西都会被放入结果列表中.

因此,它的结果就是三个字典构成的列表. 另一个类似的例子是:

dic1 = {"姓名": "xiaoming", "年龄": 27} 
dic2 = {"性别": "male","年龄": 30} 
dic3 = {k:v for d in [dic1, dic2] for k,v in d.items()} 

这里唯一的不同就是,结果变成了一个字典,需要计算的东西变成了键值对. 注意这里两个推导式的顺序是从前往后读的:它会先读取 d,然后从 d.items() 里边拿出 k, v,最后计算出键值对.

控制流

if s 是什么东西?

这同样是一个常见的问题:如果 if 语句后面跟的不是明确的 True 或者 False,那么它会被理解成什么?这里需要注意的是,一个表达式可以被自然地理解成真值或者假值,假值列表如下:

  • 被定义为假值的常量: NoneFalse
  • 任何数值类型的零: 0, 0.0, 0j, Decimal(0), Fraction(0, 1)
  • 空的序列和多项集: '', (), [], {}, set(), range(0)

其它值都会被理解成真值. 这在 andor,布尔连词部分中也有用上.

关于 lambda 表达式

一个 lambda 表达式形如以下结果:

lambda [<para_list>]: <ret_value>

其中的方括号表示可以省略. <para_list> 是参数列表,<ret_value> 为返回值. 也就是说,我们可以将其理解成一个函数:

def annoymous([<para_list>]):
    return <ret_value>

用这样的方式来理解这个表达式的值是比较简单的. 一个较为极端的例子是:

f = lambda : print(3)
f()

程序会输出 3,这是没有参数列表的情形.

面向对象程序设计

提示

这一部分考试的内容是较为基础的,大家可以不用过于担心,了解基础概念即可.

运算符重载的意义

下面这道题被问及的频率相当高,AI 助教给出了错误的回答:

以下哪个不是 Python 中运算符重载的用途?

A. 允许对象使用相同的运算符执行不同的操作

B. 增加代码的可读性

C. 允许自定义类型的对象使用内置运算符

D. 实现多态性的一种手段

正确的答案应当是 B 选项. 运算符重载不利于增加代码的可读性往往是程序设计的共识. 这出现在一些其被错误利用的情况下. 且看下面的代码:

p = m + n

在不知道 mn 的类型的情况下,p 给出的结果完全无法预知. 例如,我们这样来定义一个类:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, p):
        return Point(self.x - p.x, self.y / p.y)

    def __str__(self):
        return str((self.x, self.y))

print(Point(1, 3) + Point(3, 1))

对于这种不良书写的代码,我们会对运算符重载感到绝望:实际上,这种复杂性主要来源于程序员对代码中某些功能的理解的不同,这种问题就被称作是“降低了代码的可读性”. 我们需要“将信息保存在函数/运算符的表面意义当中”,而允许重载就允许了某些非表面的含义的存在,因此降低了可读性.

继承和函数重载

下面的代码在我们的作业中有提及:

class Animal:  
    def speak(self):  
        return "Some sound"

class Dog(Animal):  
    def speak(self):  
        return "Woof"

这里问的是一个简单的概念题:Dog 继承自哪里?注意定义 Dog 后面的括号中的 Animal,这意味着它继承自 Animal. Dog 的实例中调用 speak 方法会返回什么?它会返回 "Woof",这是因为它是“最细致的分类下的结果”. 实际上,基类就相当于是一个粗的分类系统下的“默认值”,对于这一个类别中的任何子类,其中的东西都可以直接继承这样的行为. 但是,如果我们说这个子类有某些“个性”,那么它就会取代基类中的“共性”,构成其对应的实例的行为.

Comments