类型与值
警告
此处仅对一些易忽视的、容易出错的知识点和常用的知识点进行回顾,不能代替课本和课件的学习!同时,我们在这里会提及的东西可能会超出标题所框定的范围,但尽可能限制在课程范围之内,如果有超出范围的内容,将放在蓝色方框中以示提醒.
变量
读者应该已经非常熟悉 Python 中的变量了:
a = 1234
与 C 等语言不同,Python 是一种动态强类型语言. 读者不必现在理解这个概念,但是应当已经知道,对于一些不同的值,我们能够进行不同的操作:例如,对整数能够进行加减乘除,对字符串能够进行拼接、重复等等. 尝试将整数和字符串做加法会产生错误,这也许意味着有一些操作是不被允许的.
>>> 1 + 2
3
>>> "1" + "2"
'12'
>>> 1 + "2"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'str'
让我们首先回到类型的概念上. 我们知道,1234 是一个整数,其类型是 int;"1234" 是一个字符串,其类型是 str. 类型是和值绑定的,也就是说,每个值都有其固定的类型. 我们不能将一个整数作为字符串来处理,反之亦然.
那么变量呢?变量只是一个名字,它可以容纳任何类型的值. 我们可以将一个字符串赋给一个变量,然后再将一个整数赋给这个变量,这是完全合法的:
a = '1234' # 在这里,a 容纳字符串 "1234"
a = 1234 # 现在 a 容纳整数 1234
"1234" 和 1234 是两个截然不同的值,前者是字符串,后者是整数. 这非常重要.
int(input()) 做了什么?
在编程题中,我们经常会使用这样的代码. 也许读者仍然不太清楚这段代码的作用,或说,到底发生了什么:
a = int(input())
为方便起见,我们将这段代码拆分成两部分:
s = input()
a = int(s)
我们应当已经非常熟悉 input() 函数,它会从标准输入中读取一行字符串,在这里“标准输入”的意思就是你的键盘.
>>> s = input()
42
>>> s
'42'
我们称 input() 返回一个字符串. 通过使用 input() 函数,我们得到了一个字符串 s,它的值是 "42". 然而,我们绝不可将字符串作为整数来处理. 我们首先需要将字符串转换为整数:
>>> int(s)
42
>>> a = int(s)
>>> a
42
int() 函数将字符串转换为整数. 请注意,int() 函数不会改变原来的字符串,而是返回一个新的整数. 有同学写下过类似如下的代码:
b = input()
int(b)
这段代码是逻辑上错误的,因为 int(b) 的返回值没有被保存(即,我们并没有将这个结果赋给任何变量),所以第二行实际上什么也没做,b 仍然是一个字符串. 读者应当注意这一点. 如果我们想要将 b 转换为整数,应当这样写:
b = input()
b = int(b)
或者,更简洁的写法即:
b = int(input())
同样的,我们可以用 float() 函数将字符串转换为浮点数,或是用 str() 函数将任意类型的值转换为字符串.
>>> float("3.14")
3.14
>>> str(6.28)
'6.28'
>>> int(3.14)
3
>>> int("3.14")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: '3.14'
我们无法将一个表示浮点数的字符串转换为整数,因为 int() 函数只能解析表示整数的字符串. 在涉及到浮点数的题目中,就应当使用 float() 函数来解析. 不过我们可以将一个浮点数转换为整数,这会将小数部分直接截断,注意这与向上取整、向下取整或四舍五入均不同.
操作
以 split() 为例
对于不同的类型,我们能够进行不同的操作. 我们来看一个稍复杂一些的例子:
a, b, c = input().split()
或者说,仍然拆作两部分:
s = input()
a, b, c = s.split()
在此,我们需要再次强调:类型和值是绑定的. 我们已经知道 input() 函数返回一个字符串,那么第一行代码执行结束后,s 无疑是一个字符串. 那么,从类型的角度来看,研究上面的代码和研究下面的代码是一样的:
s = "1 2 3"
a, b, c = s.split()
别忘了,我们永远可以在交互式环境中查看一个操作的结果:
>>> "1 2 3".split()
['1', '2', '3']
术语上来讲,split() 是 str 类型的一个方法,不过就目前来说,我们认为这就是我们对字符串能进行的一种操作. 再次强调,类型和值是绑定的. split() 是属于字符串的一个操作,那么,对于任何字符串,我们都可以使用 split() 操作. 在上面的例子中,对于 "1 2 3" 这个字符串,在其上使用 split() 操作,我们得到了一个列表 ['1', '2', '3']. 列表也是 Python 中的一种类型,我们在后面的课程中会学习到. 目前我们只需要知道列表可以容纳任意多个值. 那么 ['1', '2', '3'] 即是容纳了 3 个字符串 "1"、"2" 和 "3" 的列表. split() 方法的作用即是将字符串按照空白符分割,它返回一个列表,列表中的元素是原字符串从左到右按照空格分割后的结果,它们也都是字符串.
有一点非常重要:split() 是属于字符串的方法,我们需要在一个字符串上调用它. 并没有一个叫做 split() 的函数.
提示
. 和 () 都是很重要的部分,不可省略或更改. 有同学写下过类似如下的代码:
a, b = input.split()
# 或者
a, b = input().split
# 或者
a, b = input(),split() # 注意这里用的是逗号
这些代码都是错误的. input 是一个函数,我们需要调用它,所以应当写成 input();split 是一个方法,我们需要在一个字符串上实施这个操作,所以应当写成 input().split(). 至于第三种,逗号 , 并不能用于调用方法,也并没有 split() 这个函数.
剩下的操作就与下面的代码无异了:
a, b, c = ['1', '2', '3']
这会将列表中的 3 个字符串分别赋给变量 a、b 和 c,即 a = "1"、b = "2" 和 c = "3".
提示
实际上,这背后涉及到更复杂的知识:解包. 我们在这里不会详细讨论什么是解包. 不过读者在编程时,可能会遇到类似的报错:
>>> a, b, c = input().split()
1 2 3 4
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: too many values to unpack (expected 3)
这是因为 input().split() 的结果列表中有 4 个元素,而左边的变量只有 3 个. Python 无法将 4 个值赋给 3 个变量,所以会报错. 注意到 too many values to unpack 这个错误信息,“unpack” 就是解包的意思. 在进行解包时,左边的变量个数必须和右边的值的个数相同.
我们也可以给 split() 方法传递一个参数,指定应该在哪个字符处分割字符串:
>>> "/2024/10//01/".split("//")
['/2024/10', '01/']
>>> "/2024/10//01/".split("/")
['', '2024', '10', '', '01', '']
朴素的想法告诉我们,如果一个字符串中有 \( n \) 个分隔符,那么 split() 方法返回的列表中应当会有 \( n+1 \) 个元素. 事实也的确如此:在上面的例子中,"/2024/10//01/" 中有 5 个 /,所以 split('/') 返回了 6 个元素,每个元素都是原字符串中两个 / 之间的内容,或是字符串开头和 / 之间的内容,或是 / 和字符串结尾之间的内容. 那么,为什么 'A'.split('A') 返回 ['', ''],即含有两个空字符串的列表,也就不难理解了.
最后还要注意,不带参数的 split() 方法和带空格参数的 split() 方法并不一样:
>>> "1 2 3".split()
['1', '2', '3']
>>> "1 2 3".split(" ")
['1', '', '2', '', '', '3']
注意这里的 1 2 3 之间分别有 2 个和 3 个空格. 若不带参数,split() 方法会将连续的空白符都视作一个空白符处理,这里的空白符可以是空格、tab、换行符等等. 而 split(" ") 方法则只是严格地将一个空格作为分隔符. 这在某些题目中是会考察的,例如第 3 周作业题:
7-6 计算立方体水箱的水重量
计算立方体水箱的水重量.
某一水箱的外表是典型的立方体形状,请设计程序计算此水箱装满水后,其中水的重量是多少,要求:长,高,宽的单位是厘米,水的计量单位是吨.
输入格式:
在一行中依次输入水箱的长,高,宽,各数据之间至少用一个空格隔开. 注意:长,高,宽的单位是厘米,可以是小数.
输出格式:
对每一组输入,在一行中输出此水箱所装的水的重量. 注意:水的计量单位是吨,要保留 3 位小数(提示:用 format 函数).
在此我们只需关注输入格式,即“各数据之间至少用一个空格隔开”. 这意味着输入的数据之间可能有多个空格,所以使用 split(" ") 一定会出错,必须使用不带任何参数的 split().
最后,请读者自行尝试将空字符串 "" 作为参数传递给 split() 方法,看看会发生什么.
提示
实际上我们还有一些有意思的写法:
>>> str.split("abacadabra", "a")
['', 'b', 'c', 'd', 'br', '']
这和 "abacadabra".split("a") 是完全等价的.
我们以一道题目结束这一节:
判断题
当输入是:45,8 时,下面程序的输出结果是 37.
a, b = input().split(',')
b = int(b)
c = int('a', b)
print(c)
这是错误的. 读者应当注意到,这里的陷阱在于第 3 行,int('a', b) 的意思是将字符串 'a' 转换为整数,这与变量 a 毫无关系. 第三行会因为 'a' 不能被解析为一个合法的八进制数而报错(回忆一下八进制数可以包含的内容). 正确的代码应当是 c = int(a, b),这会正确地将变量 a 所容纳的字符串 "45" 解析为一个以 8 为基数的整数,即 37.
str 的其他常用方法
str 类型还有很多其他的方法,例如 strip()、find()、replace() 等等. 读者可以查阅 Python 官方文档中字符串的方法一节,搭配交互式环境进行实验.
find() 方法接收一个、两个或三个参数:
>>> "programming".find("r")
1
>>> "programming".find("r", 2)
4
>>> "programming".find("r", 5)
-1
>>> "programming".find("r", 2, 5)
4
>>> "programming".find("r", 2, 4)
-1
find() 方法在字符串中寻找第一个出现的子串,并返回其索引,注意索引是从 0 开始的. 如果没有找到,返回 -1. 第二个参数是可选的,表示从哪个索引开始查找. 第三个参数也是可选的,表示到哪个索引结束查找. 这个范围是左闭右开的. 例如对于字符串 "programming",每个字符的索引如下:
p r o g r a m m i n g
0 1 2 3 4 5 6 7 8 9 10
所以对于上面的例子,"programming".find("r", 2, 4) 返回 -1,因为 "r" 在索引 2 和 4 之间是不存在的(左闭右开!位于索引 4 的 r 并不在查找范围内). find() 方法也有一个反向的版本 rfind(),它会从字符串的末尾开始查找.
strip() 方法会去掉字符串两端的空白符:
>>> " 1, 5 ".strip()
'1, 5'
与 split() 方法类似,strip() 方法也可以接收一个参数,表示去掉哪些字符,并且无参数的 strip() 方法和带空格参数的 strip() 方法是不一样的:
>>> "abacadabra".strip("a")
'bacadabr'
>>> " \t abacadabra \t ".strip()
'abacadabra'
>>> " \t abacadabra \t ".strip(" ")
'\t abacadabra \t'
7-1 产生每位数字相同的 n 位数
读入 2 个正整数 A 和 B,1<=A<=9,1<=B<=10,产生数字 AA...A,一共 B 个 A.
输入样例 1:
1, 5
11111
3 ,4
3333
一种(错误的)解是:
a, b = input().split(',')
b = int(b)
print(a * b)
请读者思考为什么这段代码是错误的?应当如何修改?提示:注意输入中可能有多余的空格.
提示
我们可以用 dir() 函数查看一个类型或对象的所有方法:
>>> dir(str)
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isascii', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'removeprefix', 'removesuffix', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
str 类型有很多方法!当然,这里的绝大多数方法我们都是不会用到的. 如果感兴趣,你可以查看上面给出的官方文档链接,了解每个方法的具体作用.
再回到类型
我们可以使用 type() 函数查看一个值的类型:
>>> type(1234)
<class 'int'>
>>> type("1234")
<class 'str'>
>>> type(3.14159)
<class 'float'>
从上面的输出粗略来看,type() 函数告诉我们一个值的类型是什么. 有趣的是,如果我们直接查看 int、str、float 等类型:
>>> int
<class 'int'>
>>> str
<class 'str'>
>>> float
<class 'float'>
这是否说明 type 函数返回的东西和 int、str、float 这些类型是一样的呢?实际上,是的,我们可以用 == 运算符来比较它们:
>>> type(1234) == int
True
>>> type("1234") == str
True
>>> type(3.14159) == float
True
>>> type(1234) == float
False
关于 type
思考一下这一行代码是如何运作的!
>>> type(1)("1234")
1234
答案
type(1) 的结果是 int,所以 type(1)("1234") 实际上就是 int("1234").
从现在开始,我们要改口了!我们不再使用 int 函数、str 函数等说法;现在我们称之为 int 类型、str 类型,等等.
"1234" 是一个字符串,它的类型是 str,这应当毫无疑问. 我们称 "1234" 是一个对象,它的类型是 str——这照应了我们在第一节中提到的说法:Python 是一种面向对象的语言. 当我们使用下面的代码:
a = "1234"
b = int(a)
我们实际上在做的事情,是使用 "1234" 这个 str 对象,创建了一个新的 int 对象 1234,并将其赋给了变量 b. 这就是类型转换的过程,在这里,我们将一个 str 转换为了一个 int. 读者应当注意,"1234" 这个 str 对象并没有被改变,它仍然是一个 str. 我们只是使用它创建了一个新的 int 对象 1234. 即使无法完全理解这个过程也无所谓,只需知道这个过程是存在的.
任何值都有类型,而类型也是值
我们以一个有趣的例子结束本节:
>>> type(print)
<class 'builtin_function_or_method'>
>>> type(input)
<class 'builtin_function_or_method'>
>>> type(int)
<class 'type'>
>>> type(type)
<class 'type'>
>>> type(type) == type
True
无需理解这些东西. 重要的是,通过使用 type(),我们可以查看任何值的类型,包括函数、类,甚至是 type 类型本身. 类型本身也是一个值,这正是 Python 中一切皆对象的体现.
我们可以这样:
>>> int()
0
>>> str()
''
>>> float()
0.0
当我们像这样使用 int、str、float 等类型时,我们实际上就是在创建一个新的 int、str、float 对象. 创建一个新的数值的默认值是 0,创建一个新的字符串的默认值是空字符串.
当然,并不是所有的类型都可以这样使用:
>>> type()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: type() takes 1 or 3 arguments
这已经远远超出了我们的课程范围.
提示
现在我们应当可以理解下面的代码为什么是错误的:
a = input.split()
因为 input 函数是一个 builtin_function_or_method 类型的值,而不是一个字符串. 它没有 split 方法. 只有使用 input(),我们才能得到一个 str 类型的值,并在其上调用 split 方法.
bool 类型与逻辑运算
在 Python 中,bool 表示布尔值. bool 类型只有两个值:True 和 False. 同 int、str 等类型一样,bool 也是 Python 中的一个内置类型. 它被广泛地运用在各种控制结构中,例如 if 语句、while 循环等等.
>>> type(bool)
<class 'type'>
>>> type(True)
<class 'bool'>
>>> type(False)
<class 'bool'>
而提到布尔值,我们就不得不提到另一对极其重要的概念:真值(truthy)和假值(falsy),我们现在可以理解为,能使得控制结构中的条件成立的值是真值,反之则是假值. 在这里非常非常非常重要的一点是,真值和假值只是一个概念,它们并不指代一个具体的值.
...以下基本完整地列出了具有假值的内置对象:
- 被定义为假值的常量:
None和False - 任何数值类型的零:
0,0.0,0j,Decimal(0),Fraction(0, 1) - 空的序列和多项集:
'',(),[],{},set(),range(0)
if 0: # 把这里的 0 换成上面的任何一个假值,效果都是一样的
print("这一句永远不会打印")
如果将任何假值转换为 bool 类型,就会得到一个 False:
>>> bool(0)
False
>>> bool("")
False
>>> bool([])
False
>>> bool(False)
False
而真值则是除了假值之外的所有值. 任何非零的数,非空的字符串、列表、元组、字典、集合,甚至是函数、类型等等,都是真值. 例如:
>>> bool(-1)
True
>>> bool(4.9406565e-324) # 最接近 0 的正浮点数
True
>>> bool(" ") # 一个空格
True
>>> bool([0])
True
>>> bool(bool)
True
>>> bool(type)
True
>>> bool(print)
True
同样的,这些对变量也是适用的:
>>> a = [0]
>>> bool(a)
True
判断题
在 Python 中,if 语句的条件必须是一个布尔值.
当然,我们知道这是错误的. 条件可以是任何值,只要它能被转换为布尔值即可.
判断题
bool(FALSE) 的返回值是 True.
这是一道错题,即题目本身是错误的. 它既不是 bool(False)(注意大小写!),也不是 bool("FALSE"). 在这里 FALSE 只会被作为一个变量名来解释,所以结果完全取决于 FALSE 这个变量的值:
>>> FALSE = 0
>>> bool(FALSE)
False
>>> FALSE = 1
>>> bool(FALSE)
True
当然,如果 FALSE 没有被定义,那么会产生一个 NameError 错误.
我们举个例子:如何将字符串 "True" 和 "False" 转换为布尔值?直接使用 bool 作转换是行不通的:
>>> bool("True")
True
>>> bool("False")
True
这是因为任何非空的字符串都是真值,当然包括 "False" 这个字符串. 对于这种情况,简单地使用 if 语句来判断即可:
>>> s = "True"
>>> if s == "True":
... b = True
... elif s == "False":
... b = False
... else:
... print("Invalid input")
...
>>> b
True
0 不是 False
这个小节标题称作 int 不是 bool 也许会更恰当一些. 我们已经知道 0 是一个假值,但它不是 False:
>>> 0 == False
True
>>> 0 is False
False
== 运算符是值比较,它会比较两个值是否相等. 而 is 运算符是身份比较,它会比较两个值是否是同一个对象. 当然,我们无需理解 is 运算符的具体细节,只需知道,在这里我们强调的是:0 是一个 int 类型的值,而 False 是一个 bool 类型的值. 它们是不同的类型. 我们很快会看到这意味着什么.
bool 作为数字,以及逻辑运算
在 Python 中,bool 类型的值也可以被当作数字来使用. True 被当作 1,False 被当作 0:
>>> True + True
2
>>> True + False
1
>>> False + False
0
>>> True * 3
3
>>> True * False
0
关于 bool
实际上 bool 类型的值是 int 类型的子类,但这并不是我们需要关心的事情.
>>> issubclass(bool, int)
True
逻辑运算符 not、and、or 是 Python 中表示逻辑运算的关键字,优先级按这个顺序递减.
not 计算一个值的逻辑非:
>>> not True
False
>>> not False
True
>>> not -1
False
它会将一个真值转换为 False,将一个假值转换为 True,所以 not 运算符的结果永远是一个 bool 类型的值.
当然,这没什么意思. 真正有意思的是 and 和 or 运算符,它们分别计算逻辑与和逻辑或. 我们应当已经知道逻辑与和逻辑或是如何工作的,不过也许,实际上,它们真正的工作方式并不是我们想象的那样——不妨来看看 Python 文档中的布尔运算一节:
这些属于布尔运算,按优先级升序排列:
| 运算 | 结果: |
|---|---|
x or y |
如果 x 为真值,则 x,否则 y |
x and y |
如果 x 为假值,则 x,否则 y |
not x |
如果 x 为假值,则 True,否则 False |
我们来看一个例子:
判断题
表达式 3 and 0 and "hello" 的值是 False.
我们来分析这个表达式:
- 逻辑与运算是从左到右进行的,所以上面的表达式等价于
(3 and 0) and "hello". - 首先来看
3 and 0. 由于3是真值,所以根据上面的表格,它的结果是0. - 然后再看
0 and "hello". 由于0是假值,所以根据上面的表格,它的结果是0. - 最后,表达式的值是
0,而不是False. 所以这道题是错误的.
注意到这里体现了 0 不是 False 的概念. 在这里最为重要的一点是,尽管这些值在许多方面都可能表现得很相似,但归根结底,它们仍然是不同的类型,不可混为一谈. 这也是我们在本节中反复强调的:类型和值是绑定的.
None
None 是 Python 中的一个特殊值,表示空值. 它是 NoneType 类型的唯一一个值. None 通常用于表示一个函数没有返回值,或者一个变量存在但没有被赋值. 例如:
>>> a = None
>>> a
>>> print(a)
None
>>> type(a)
<class 'NoneType'>
注意这里的第二行,交互式环境中没有任何输出. 这是因为交互式环境不会显示 None 的值. 但这里又没有报错——表明 a 这个变量是确实存在的. print() 函数会正常输出 None.
同时,print() 函数的返回值就是 None:
>>> a = print("Hello, world!")
Hello, world!
>>> print(a)
None
或者,一个更有意思的写法是:
>>> print(print(None))
None
None
读者应该可以理解这段代码为什么会输出两个 None 了.