Skip to content

类型与值

警告

此处仅对一些易忽视的、容易出错的知识点和常用的知识点进行回顾,不能代替课本和课件的学习!同时,我们在这里会提及的东西可能会超出标题所框定的范围,但尽可能限制在课程范围之内,如果有超出范围的内容,将放在蓝色方框中以示提醒.

变量

读者应该已经非常熟悉 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 个字符串分别赋给变量 abc,即 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
输出样例 1:
11111
输入样例 2:
        3  ,4
输出样例 2:
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() 函数告诉我们一个值的类型是什么. 有趣的是,如果我们直接查看 intstrfloat 等类型:

>>> int
<class 'int'>
>>> str
<class 'str'>
>>> float
<class 'float'>

这是否说明 type 函数返回的东西和 intstrfloat 这些类型是一样的呢?实际上,是的,我们可以用 == 运算符来比较它们:

>>> 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

当我们像这样使用 intstrfloat 等类型时,我们实际上就是在创建一个新的 intstrfloat 对象. 创建一个新的数值的默认值是 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 类型只有两个值:TrueFalse. 同 intstr 等类型一样,bool 也是 Python 中的一个内置类型. 它被广泛地运用在各种控制结构中,例如 if 语句、while 循环等等.

>>> type(bool)
<class 'type'>
>>> type(True)
<class 'bool'>
>>> type(False)
<class 'bool'>

而提到布尔值,我们就不得不提到另一对极其重要的概念:真值(truthy)和假值(falsy),我们现在可以理解为,能使得控制结构中的条件成立的值是真值,反之则是假值. 在这里非常非常非常重要的一点是,真值和假值只是一个概念,它们并不指代一个具体的值.

逻辑值检测

...以下基本完整地列出了具有假值的内置对象:

  • 被定义为假值的常量: NoneFalse
  • 任何数值类型的零: 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 被当作 1False 被当作 0

>>> True + True
2
>>> True + False
1
>>> False + False
0
>>> True * 3
3
>>> True * False
0

关于 bool

实际上 bool 类型的值是 int 类型的子类,但这并不是我们需要关心的事情.

>>> issubclass(bool, int)
True

逻辑运算符 notandor 是 Python 中表示逻辑运算的关键字,优先级按这个顺序递减.

not 计算一个值的逻辑非

>>> not True
False
>>> not False
True
>>> not -1
False

它会将一个真值转换为 False,将一个假值转换为 True,所以 not 运算符的结果永远是一个 bool 类型的值.

当然,这没什么意思. 真正有意思的是 andor 运算符,它们分别计算逻辑与逻辑或. 我们应当已经知道逻辑与和逻辑或是如何工作的,不过也许,实际上,它们真正的工作方式并不是我们想象的那样——不妨来看看 Python 文档中的布尔运算一节:

布尔运算 --- and, or, not

这些属于布尔运算,按优先级升序排列:

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

我们来看一个例子:

判断题

表达式 3 and 0 and "hello" 的值是 False.

我们来分析这个表达式:

  1. 逻辑与运算是从左到右进行的,所以上面的表达式等价于 (3 and 0) and "hello".
  2. 首先来看 3 and 0. 由于 3 是真值,所以根据上面的表格,它的结果是 0.
  3. 然后再看 0 and "hello". 由于 0 是假值,所以根据上面的表格,它的结果是 0.
  4. 最后,表达式的值是 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 了.

Comments