对象与类型
引言
在写出有用的 Python 程序之前,你需要对程序所操作的“东西”有一个清晰的认识。在 Python 中,这些东西有一个统一的名字:每一份数据——一个数、一段文本、一串结果,甚至一个函数——都是一个对象(object)。本页先帮你建立关于对象的心智模型,再用两个简单的问题来组织 Python 提供的所有内置类型。读完之后,你应当能看着任意一个值,说出它是什么、能对它做什么、以及它能不能改变。
打个比方:如果把程序比作一座建筑,那么对象就是建材,类型就是建材的种类。了解你的建材——哪些坚硬、哪些柔韧、哪些能容纳别的东西——正是你能设计出稳固结构的前提。
本页大多数代码都是可运行的:按 Run(或 Ctrl/Cmd+Enter)即可在浏览器中就地执行,改完再运行一次。学习这些概念最好的方式,就是亲手把玩它们。
1. 对象:标识、类型与值
编程在很大程度上是一门把数学思想付诸实践的艺术,而它从数学中借来的最重要的思想就是抽象。抽象最基本的形式是变量。这个概念你早已见过:小学时你用 1、2、3 这样具体的数字来计数,中学时你用符号 \(x\) 来表示一个可能等于 1、2 或 100 的泛指的数。它的威力正来自这种通用性。
在 Python 中,我们把这样的符号称为名称(name)。当你写下 x = 1 时,Python 会开辟一块内存来存放值 1,再把名称 x 绑定到那个对象上。名称是一张标签,对象才是标签所指向的东西。
核心概念:对象
对象是存在于内存中、具有明确结构的一份数据。每个对象都同时携带三样东西:
- 标识(identity)——一个唯一标识(在 CPython 中即其内存地址),用
id()读取; - 类型(type)——它是哪一种东西,这决定了它能存放哪些值、允许哪些操作,用
type()读取; - 值(value)——它实际存储的数据。
有两句话概括了 Python 的世界观,我们整学期都会反复用到:名称指向对象,以及一切皆对象。
下面的示例表明:赋值是把一个名称绑定到对象上,而非复制值——运行它,看看两个名称如何最终共享同一个标识。
示例:名称指向对象
a = 1
print(id(a)) # 对象 1 的标识(地址)
print(type(a)) # 类型:<class 'int'>
b = a
print(id(b)) # 与 id(a) 相同:b 指向同一个对象
a 和 b 是同一个对象上的两张标签。我们并没有复制值 1,只是为它添了第二个名称。
下面的图捕捉了刚刚发生的事:在命名空间里,名称 a 和 b 是两个抽屉,而两支箭都指向内存中同一个对象。
这里的地址只是示意——重点是每个对象都有一个地址(详见下方“深入了解”)。
这个示例已经用到了三个你今后会不断使用的工具:print() 显示你传给它的任何值,id() 返回一个对象的标识,type() 返回它的类型。我们还用到了 f-string 格式化——在字符串前加上 f,并把表达式放进 {},例如 f"{id(b):02X}" 就能以十六进制显示一个标识。
课堂练习:名称与值
- 把名称
person_name绑定到字符串"Alice",然后打印它。 - 把
person_name重新绑定到"Bob",并用 f-string 打印 The current name is Bob. - 给同一个对象绑定第二个名称,用
id()确认两个名称共享同一个对象。
深入了解:一个对象有多大,又存在哪里?
对象内部数据的排布方式称为它的内存布局(memory layout)。你可以用 sys 模块查看一个对象占用多少字节:
“标识就是内存地址”是 CPython 的实现细节——其他 Python 实现只保证 id()
在对象的生命周期内唯一且不变。请依赖这个保证,而不要依赖具体的地址值。
延伸阅读:Python 是如何运行你的代码的
选读,远超本课程要求——给好奇的同学,一窥解释器究竟如何执行你的程序、又把对象存在哪里:
- Python 虚拟机概念(N. Kavitha,SlideShare)——用通俗的图示讲解执行你代码的解释器。
- 《Inside the Python Virtual Machine》(Obi Ike-Nwosu,Leanpub)——从“The view from 30,000 ft”一章开始,了解整体框架。
2. 组织所有类型的两个问题
Python 内置了许多类型,但你不必把它们当作一张扁平的清单来死记。几乎每一个类型,都可以由两个问题来锁定。
第一个问题是它装什么? 有些对象是标量(scalar)(或称原子的):它们代表一个不可再分的单一值,例如整数 7 或常量 None。另一些是容器(container):它们容纳别的对象,例如一个数字列表,或一个“姓名—成绩”的字典。
第二个问题是它能改变吗? 这就是可变性(mutability)这一性质。
核心概念:可变与不可变
一个对象如果在创建之后其值还能改变,就是可变的(mutable);如果不能,就是
不可变的(immutable)。列表、字典、集合和 bytearray 是可变的;数字、字符串、
元组、frozenset 和 bytes 是不可变的。
把这两条轴——标量 vs. 容器、可变 vs. 不可变——记在心里。本页讲解标量类型;下一页 1.2 容器集合 接着讲容器,并回到可变性——它能解释的东西多得出人意料。
3. 标量类型
我们从最简单的对象——标量——开始。标量类型代表一个不可再分的单一值:它“内部”没有可供你拆开或遍历的东西。这使得标量成为自然的起点,之后我们再去认识专门用来容纳它们的容器。Python 的标量就是数字(几乎所有程序都离不开它)以及孤零零的对象 None。
3.1 数字
Python 有三种数值类型——int(整数)、float(带小数点的数)和 complex(带实部与虚部的复数)——再加上 bool,即 True 与 False 的类型。这四者都是不可变的。
下面的示例各创建一个数值类型的值,并打印出 Python 认为它们分别是什么。
示例:数值类型
要在运行时检查某个东西的类型,可用 isinstance(),它判断一个对象是否是某个类型的实例。下面的示例检查了几个值——包括 bool 与 int 之间那层令人意外的关系。
示例:用 isinstance 检查类型
易错点:bool 是一种 int
bool 是 int 的子类,所以在算术中 True 表现得像 1、False 像 0,而且
isinstance(True, int) 为 True。这通常很方便(sum([True, False, True]) 等于
2),但偶尔会让人意外,所以请记住:在 Python 看来,布尔值就是一种整数。
课堂练习:数字与 isinstance
- 对上面的
a、b、c、flag,分别打印它们的type()。 - 研究
False:isinstance(False, int)和False + 1分别是什么? - 查阅
isinstance()的官方文档,留意它的第二个参数可以是什么。
3.2 None 对象
None 是一个特殊的标量,表示“没有值”。它属于自己的类型(NoneType),且只有唯一一个实例;当一个函数做了事情却没有有意义的结果可返回时,你就会见到它。
下面的示例展示了你最常遇到 None 的场景之一:print() 自己就返回它。
示例:None 从何而来
4. 文本与二进制数据
文本处在标量与容器之间一个有趣的中间地带,所以我们把它放在两者之间来讲。字符串由更小的片段——它的字符——构成,因此严格说来它是一个容器,一种序列。可是在实际使用中,我们把字符串当作一个单一的值来对待,而且大多数初学者在想到“序列”之前,早就用上文本了。所以我们现在把它作为一种基本类型来介绍,顺带点明它的容器本质,等到了 1.2 再回头讲它作为序列的行为。
4.1 字符串
字符串(str)是一段文本,用单引号或双引号写出。字符串是不可变的:对它的操作会产生新字符串,而不会改变原来的那个。
下面的示例表明:字符串方法返回一个新字符串,而原字符串原封不动——这正是不可变类型的标志。
由于处理文本太常见,str 自带一套庞大的方法。你最常用的几类是:改变大小写、去除空白、拆分与拼接、查找与替换。每一个都会返回一个新字符串,原字符串绝不会被改动。
下面的示例把最有用的几个方法用在一段杂乱的文本上。
示例:常用字符串方法
s = " Hello, World "
print(s.strip()) # "Hello, World" —— 去掉首尾空白
print(s.strip().lower()) # "hello, world" —— 方法可以链式调用
print("a,b,c".split(",")) # ['a', 'b', 'c'] —— 拆分成列表
print("-".join(["x", "y", "z"])) # "x-y-z" —— 把列表拼成字符串
print("hello".replace("l", "L")) # "heLLo"
print("hello".find("l")) # 2 —— 首次匹配的下标(找不到则为 -1)
课堂练习:字符串方法
- 用切片反转一个字符串(回忆 1.2 中的
[::-1])。 - 用
split()数一数"the quick brown fox"有多少个单词。 - 给定
" 2024-01-02 ",先strip再按"-"拆分,得到['2024', '01', '02']。
4.2 字节
当你需要的是原始二进制数据而非文本时,Python 提供了 bytes(不可变)以及它可变的孪生兄弟 bytearray。早期你不会经常用到它们,但它们是 str/list 的二进制对应物,能让整幅图景更完整。
深入了解:文本不是字节
str 是 Unicode 字符的序列;bytes 是原始 8 位字节的序列。在两者之间转换需要
一种编码(通常是 UTF-8):"café".encode("utf-8") 得到字节,.decode("utf-8")
再把它们变回来。混淆这两者,正是大多数“乱码(mojibake)”问题的根源。
小结
Python 中一切皆对象,每个对象都携带标识(id())、类型(type())和值。要组织众多类型,问两个问题——它装什么?(标量 vs. 容器)以及它能不能变?(可变 vs. 不可变)。本页讲了标量——数字(int、float、complex、bool)和孤独的 None——以及文本类型(str,外加 bytes/bytearray)。下一页 1.2 容器集合 接手容器——序列、集合与映射——并回到可变性,把它们串到一起。