封面来源:由博主个人绘制,如需使用请联系博主。

0. 学习计划

我目前有 Java 开发背景,因此肯定不能从头开始学。

使用以下提示词直接问 GPT 5.6:

1
你帮我上网搜索一下,有哪些可以观看的课程(文章或视频),适合于有 Java 开发背景的,学习 Python 基础的资料

GPT 给我的建议是:

  1. 用 4 - 5 小时完成帝国理工学院的 Python for Java Programmers 课程
  2. 再使用 Learn Python in Y Minutes 做语法复习
  3. 接着补充学习 Python for Java Developers(跳过前面的基础,直接从 Lesson 8: Exception Handling and Debugging 开始学习)
  4. 还有不理解的内容?直接查 Python 官方教程

1. 简介

1.1 Python 之禅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

1.2 与 Java 对比

  • Python 无需定义类或 main() 方法,可以随意为 .py 文件命名以作为 Python 脚本,运行 Python 脚本时,所有不在函数或类里定义的代码都会被执行
  • Python 不需要声明变量类型
  • Python 中,分号是可选的
  • Python 使用缩进和冒号 : 的组合来表示代码块,官方推荐使用四个空格来缩进代码
  • Python 中,使用 # 标记单行注释,使用三引号 """''' 来标记多行注释的开始和结束

2. 基本数据类型

Python 里有三种主要的基本数据类型:

  • 数字:intfloatcomplex(复数)
  • 布尔:bool
  • 字符串:str

还有一种很少使用的 bytes 类型。

2.1 数字

数字没有细致的类型区分。

>>> type(42)
<class 'int'>
>>> type(3.412)
<class 'float'>
>>> type(3.2e-12)
<class 'float'>
>>> type(1+2j)
<class 'complex'>
>>> type(5.)
<class 'float'>

2.2 布尔

布尔的字面量首字母需要大写。

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

2.3 字符串

使用单引号 '、双引号 "、三引号 ''' 或三重双引号 """ 把字符串字面量括起来,这四种写法都是等价的。

Python 里没有 char 类型,直接使用长度为 1 的字符串表示单个字符即可。

可以使用三引号 ''' 或三重双引号 """ 来编写文本块,Java 15 中也支持以三重双引号来编写文本块。

前面介绍的多行注释的写法与文本块写法一样,实际上那就是个文本块。Python 将文本块作为表达式处理,但由于它不是语句(没有对其进行赋值或返回),因此 Python 不会对它做任何操作。

如果将两个字符串并排放置,Python 会自动将它们连起来:

>>> words = "desert" "you"
>>> words
'desertyou'

还可以使用括号来跨行连接字符串,并且无视换行、缩进带来的额外空白字符:

>>> lyrics = ("Never gonna give you up, "
              "Never gonna let you down, "
              "Never gonna run around and desert you.")
>>> lyrics
'Never gonna give you up, Never gonna let you down, Never gonna run around and desert you.'

3. 变量与运算符

3.1 一切皆对象

Python 里的所有值都是对象,与 Java 不同,基本类型也是对象。

可以使用 isinstance() 函数来判断是不是对象:

>>> isinstance(2020, object)
True
>>> isinstance(True, object)
True
>>> isinstance("Hello, World", object)
True

因此下面这些都是相应内置类型的构造函数,它们可以接收一些输入,并根据输入创建该类型的实例:

>>> int()
0
>>> float()
0.0
>>> complex()
0j
>>> bool()
False
>>> str()
''
>>> int(-9.789)
-9
>>> float(9)
9.0
>>> float("nan")
nan
>>> float("inf")
inf
>>> float("-inf")
-inf
>>> complex(1, 2)
(1+2j)
>>> bool(1)
True
>>> bool("")
False
>>> bool(5)
True
>>> str("Hello")
'Hello'
>>> str(10)
'10'

3.2 关键字

与 Java 一样,Python 中也有一些关键字,可以通过以下方式查看 Python 中的关键字列表:

>>> import keyword
>>> keyword.kwlist
['False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield']

注意:内置数据类型(比如 intstr)不是关键字,它们是类,因此你甚至可以把它们作为变量名。

None

None 也是一个关键字,等同于 Java 里的 null

它也是一个对象,准确地说是 NoneType 类的实例对象:

>>> type(None)
<class 'NoneType'>

3.3 变量

与 Java 相比

在 Java 中,基本类型变量是内存中预留的一块空间,有类型、名称以及赋给它的值:

java_var1

Python 中的基本类型变量也是对象。Python 中的变量更像是指向对象引用的 Java 变量,它们指向堆中的某个对象。这就像在 Java 里使用 new 关键字来实例化一个变量:

java_var2

在 Python 中,变量只是一个始终指向内存堆中某个对象的标签(或昵称?)。与 Java 不同,变量本身没有类型,但它指向的对象有类型:

python_var1

与 Java 一样,Python 也有 GC,因此无需担心内存释放。

Python 的变量

再次强调:Python 变量只是标签,并不绑定到特定的类型。

variable1

可以让不同的变量指向同一个对象:

variable2

使用 id() 函数来获取对象在内存中的地址,如果两个变量具有相同的 id,证明它们都指向一个相同的对象。

可以在程序的任何阶段将已有变量重新赋值,使其指向另一个对象(可以是任意类型):

variable3

3.4 对象

Python 中也有类,并且也默认继承 object 类。比如定义一个 Person 类:

1
2
3
4
class Person:
    pass

person = Person()

Python 规定函数、类、条件分支、循环等代码块不能为空,至少要有一条语句。

为了占位,引入 pass 关键字。pass 的作用不是为了改变程序行为,而是为了满足 Python 的语法要求。所以它和注释还是有区别的,因为注释不能算 Python 语句。

3.5 运算符

Python 里的 +-*/% 数学运算符与 Java 差不多,但 / 会按数学上的效果执行,即返回一个 float,如果想让 Java 中的整除效果,应该使用 //

>>> 5 / 3
1.6666666666666667
>>> 5 // 3
1

比较运算符 <<=>>===!= 与 Java 相同,Python 更进一步,可以把表达式串联起来:

>>> x = 5
>>> 3 < x < 6
True
>>> 5 < x <= 8
False

Python 里的逻辑操作符和 Java 有区别,使用英语表示:

Java Python
! not
&& and
`

3.6 赋值运算符

可以一次将同一个对象赋值给多个对象:

1
x = y = 123

也可以使用多重赋值:

1
x, y, z = 1, 2, 3

所以可以很轻松地交换变量的值(我对 Python 的初次惊叹):

1
2
3
x = 1
y = 2
x, y = y, x

与 Java 一样,Python 里也有复合赋值运算符:+=-=*=/=%=//=**=

不过 Python 里没有自增 x++、自减 y--。谭浩强落泪~

4. 序列

4.1 list 列表

基本写法

Python 里的 list 写法像 Java 里的数组,但效果像动态数组 ArrayList

1
students = ["Abraham", "Bella", "Connor", "Da-ming", "Enya"]

可以在同一个列表里混合不同类型的对象(推不推荐是另外一回事 🤣):

1
mixed_buffet = [1, "Two", 3.03, 4j]

可以简单地嵌套列表:

1
list_in_list = [1, 2, [3, 4, [5, 6, 7], 8], 9, [10, 11]]

可以使用 list() 函数来构造一个新的 list 对象:

>>> x = list('abc')
>>> x
['a', 'b', 'c']

使用 len() 函数获取列表的长度:

>>> len(x)
3

访问列表里的元素

可以像 Java 根据索引来获取数组里的值那样来访问列表里的元素:

>>> students = ["Abraham", "Bella", "Connor", "Da-ming", "Enya"]
>>> students[0]
'Abraham'
>>> students[1]
'Bella'

也可以倒着访问:

>>> students[-1]
'Enya'
>>> students[-2]
'Da-ming'

list1

有嵌套数组?

再添加一层索引即可:

>>> x = [1, 2, [3, 4, 5], 6]
>>> x[2]
[3, 4, 5]
>>> x[2][1]
4

切片

Python 提供了「切片」来获取一个列表里的子列表。

基本语法是:

1
items[start:stop:step]

其中:

  • start:开始位置。包含
  • stop:结束位置。不包含
  • step:步长。每次移动多少个位置,默认为 1

Python 会访问这些索引的元素:

1
2
3
4
5
start
start + step
start + step * 2
start + step * 3
...

比如:

>>> items = ["a", "b", "c", "d", "e"]
>>> items[1:4]
['b', 'c', 'd']

理解成取索引 i 满足 1 <= i < 4 的元素。

可以省略开始或结束位置:

>>> items[:3]
['a', 'b', 'c']
>>> items[2:]
['c', 'd', 'e']
>>> items[:]
['a', 'b', 'c', 'd', 'e']

如果索引是负数,就从末尾开始计数,最后一个元素的位置是 -1

>>> items[-1]
'e'
>>> items[-3:] # 从倒数第三个元素向后取
['c', 'd', 'e']
>>> items[:-1] # 从第一个元素向后取,不包括最后一个元素
['a', 'b', 'c', 'd']

如果设置了步长:

>>> items[::2] # 隔一个取一个
['a', 'c', 'e']
>>> items[1::2]
['b', 'd']

如果步长是负数,表示从末尾向前取:

>>> items[::-1]
['e', 'd', 'c', 'b', 'a']
>>> items[4:1:-1]
['e', 'd', 'c']

常用的切片方式有:

写法 含义
items[:n] n
items[n:] 从第 n 个开始
items[-n:] n
items[:-n] 去掉后 n
items[::2] 每隔一个取一个
items[::-1] 反转

注意:

  • 单个索引越界会报错,但切片越界不会报错,而是取能够取到的部分
  • 切片出来的列表是一个新列表,但内部的元素还是原来的实例,是「浅拷贝」

修改列表

使用 append() 方法向列表末尾追加一个元素。

使用 extend() 方法批量向列表末尾追加元素。

使用 del 运算符删除列表中的某一项:

>>> nums = [1, 2, 3]
>>> nums
[1, 2, 3]
>>> del nums[1]
>>> nums
[1, 3]

除此之外,Python 列表还提供了许多方法:

  • items.insert(i, x):指定位置插入
  • items.remove(x):按值删除
  • items.pop(i):按位置删除并返回
  • items.clear():清空
  • items.index(x):查找位置
  • items.count(x):统计次数
  • items.sort():原地排序
  • items.reverse():原地反转
  • items.copy():浅复制

再补充一下内置函数:

  • len(items):长度
  • sum(items):求和
  • min(items):最小值
  • max(items):最大值
  • sorted(items):返回排序后的新列表

运算符

运算符 +* 已经为 list 重载:

>>> [1, 2] + [3, 4, 5]
[1, 2, 3, 4, 5]
>>> [1, 2] * 3
[1, 2, 1, 2, 1, 2]

使用 in 运算符判断某个元素是否在列表里:

>>> 1 in [1, 2, 3]
True
>>> 4 in [1, 2, 3]
False

4.2 tuple 元组

元组和列表类似,只不过列表是可变的(可以修改),元组不可变(不可被修改)。

元组适合用于表示有序的、固定大小的元素分组,例如二维坐标 (x,y)(x, y) 或三维坐标 (x,y,z)(x, y, z)

使用圆括号 () 来声明一个元组:

>>> my_vector = (1, 3, 4)
>>> my_vector
(1, 3, 4)

可以使用 tuple() 函数将一个序列转换为元组:

>>> my_list = [1, 3, 4]
>>> tuple(my_list)
(1, 3, 4)

元组与列表一样,支持通过索引访问某个位置的元素,也支持切片。

但记住,不支持修改某个位置的元素,也不支持增加、删除元素。

注意: 如果元组里只有一个元素,需要在第一个元素后加一个逗号来表示这是一个元组,否则 Python 会把它当成一个单独的变量。

>>> non_tuple = (1)
>>> non_tuple
1
>>> type(non_tuple)
<class 'int'>
>>> singleton = (1,)
>>> singleton
(1,)
>>> type(singleton)
<class 'tuple'>

4.3 str 字符串

字符串也是一种序列

可以像处理列表、元组那样来访问单个字符、切片。

注意: 和元组一样,字符串也是不可变的,所以不能修改字符串中某个位置的字符,也不能向一个字符串里添加或删除某个字符。

Python 也为字符串提供了许多方法,这里不再展开,可以参考 官方文档

f-strings

f-strings 的全称是 formatted string literals,即格式化字符串字面量。

在字符串里预留 {},Python 在运行时计算其中的表达式,并把结果填进去。

>>> name = '小明'
>>> age = 18
>>> f'{name}今年{age}岁' # 直接填结果
'小明今年18岁'
>>> price = 20
>>> cnt = 3
>>> f'总价:{price * cnt}元' # 计算表达式
'总价:60元'
>>> pi = 3.1415926
>>> f'{pi:.2f}' # 保留两位小数
'3.14'
>>> amount = 1234567
>>> f'{amount:,}' # 添加千位分隔符
'1,234,567'
>>> name = "Python"
>>> f"|{name:<10}|" # 左对齐
'|Python    |'
>>> f"|{name:^10}|" # 居中
'|  Python  |'
>>> f"|{name:>10}|" # 右对齐
'|    Python|'
>>> from datetime import datetime
>>> rate = 0.256
>>> now = datetime(2026, 7, 25)
>>> f"{rate:.1%}" # 格式化百分比
'25.6%'
>>> f"{now:%Y年%m月%d日}" # 格式化日期
'2026年07月25日'
>>> f'输出大括号:{{1, 2, 3}}' # 连续写两个大括号输出大括号
'输出大括号:{1, 2, 3}'

转义字符串

如果字符串字面量里有引号,这个字面量改怎么写呢?

使用 \ 转义符号:

>>> 'the boy\'s mother'
"the boy's mother"

更推荐使用另一种类型的引号来括字符串:

>>> "the boy's mother"
"the boy's mother"
>>> '''the boy's mother'''
"the boy's mother"
>>> 'he said, "hello"'
'he said, "hello"'

5. 集合和字典

5.1 set 集合

Python 内置了一种 set 数据结构,类似 Java 里的 HashSet

  • 不包含重复元素
  • 内部元素没有顺序,不能通过索引访问
  • list 相比,判断一个元素是否在集合里的速度要快得多

使用 {} 包裹元素来表示 set

>>> vowels = {'a', 'e', 'i', 'o', 'u'}
>>> vowels
{'e', 'a', 'i', 'o', 'u'}
>>> vowels = {'a', 'e', 'i', 'o', 'u', 'a', 'e', 'i', 'o', 'u'}
>>> vowels
{'e', 'a', 'i', 'o', 'u'}

可以使用 set() 构造器将一个序列(listtuple)转换为集合。

如果你要创建一个空集合,需要使用 set() 构造器,但你不能只使用 {},因为这是 dict

集合运算

Python 为 set 提供了运算符重载:

运算 数学记法 Python 运算符 对象方法
并集 ABA \cup B a_set | b_set a_set.union(b_set)
交集 ABA \cap B a_set & b_set a_set.intersection(b_set)
差集 ABA \setminus B a_set - b_set a_set.difference(b_set)
对称差集 ABA \triangle B a_set ^ b_set a_set.symmetric_difference(b_set)
成员关系 ABA \in B x in b_set
非成员关系 ABA \notin B x not in b_set
真子集 ABA \subset B a_set < b_set
子集 ABA \subseteq B a_set <= b_set a_set.issubset(b_set)
真超集 ABA \supset B a_set > b_set
超集 ABA \supseteq B a_set >= b_set a_set.issuperset(b_set)

集合方法

set 是可变的,因此可以修改集合内部的值。

官方文档set 提供了许多方法,比如:

  • add(x):添加一个元素

  • update(iterable):批量添加多个元素

  • remove(x):删除元素;元素不存在时抛出 KeyError

  • discard(x):删除元素;元素不存在时不报错

  • pop():删除并返回任意一个元素;空集合会报错

  • clear():清空集合

5.2 dict 字典

Python 还内置了一种 dict 数据结构,类似 Java 里的 HashMap

>>> student_dict = {"00-01-30": "Ali", "00-02-11": "Simon","00-05-67": "Francesca", "00-09-88": "Cho"}
>>> student_dict
{'00-01-30': 'Ali', '00-02-11': 'Simon', '00-05-67': 'Francesca', '00-09-88': 'Cho'}

也可以将一个元组列表作为参数传入 dict() 函数来构造一个字典:

>>> student_dict = dict([("00-01-30", "Ali"), ("00-02-11", "Simon"),("00-05-67", "Francesca"), ("00-09-88", "Cho")])
>>> student_dict
{'00-01-30': 'Ali', '00-02-11': 'Simon', '00-05-67': 'Francesca', '00-09-88': 'Cho'}

还可以使用 {} 声明一个空字典,然后不断添加键值对:

>>> student_dict = {}
>>> student_dict["00-01-30"] = "Ali"
>>> student_dict["00-02-11"] = "Simon"
>>> student_dict["00-05-67"] = "Francesca"
>>> student_dict["00-09-88"] = "Cho"
>>> student_dict
{'00-01-30': 'Ali', '00-02-11': 'Simon', '00-05-67': 'Francesca', '00-09-88': 'Cho'}
>>> student_dict["00-01-30"] = "Josiah"
>>> student_dict["00-01-30"]
'Josiah'

字典的 key 必须是唯一且不可变的(比如数字、字符串、元组),因此不能使用 list 作为 key

在 Python 3.6 以上的版本,dict 的行为更像 Java 里的 LinkedHashMap,让 key 的顺序保持为插入时的顺序。尽管如此,仍然建议总是将 key 视为无序的。

从字典里检索

如果使用一个不存在的 key 去查找字典里的值,会报错:

>>> student_dict['xx']
Traceback (most recent call last):
  File "<python-input-12>", line 1, in <module>
    student_dict['xx']
    ~~~~~~~~~~~~^^^^^^
KeyError: 'xx'

因此在使用 key 去查找值之前,需要先判断 key 是否存在:

>>> if "xx" in student_dict:
...     xx = student_dict['xx']
...

或者使用 get() 方法来查找值,如果 key 不存在,输出一个默认值:

>>> xx = student_dict.get('xx', 'default_xx')
>>> xx
'default_xx'

字典方法

字典里有三个常用的方法:keys()values()items()

>>> student_dict = {"00-01-30": "Ali", "00-02-11": "Simon","00-05-67": "Francesca", "00-09-88": "Cho"}
>>> ids = student_dict.keys()
>>> ids
dict_keys(['00-01-30', '00-02-11', '00-05-67', '00-09-88'])
>>> type(ids)
<class 'dict_keys'>
>>> ids[0]
Traceback (most recent call last):
  File "<python-input-4>", line 1, in <module>
    ids[0]
    ~~~^^^
TypeError: 'dict_keys' object is not subscriptable
>>> list(student_dict)
['00-01-30', '00-02-11', '00-05-67', '00-09-88']
>>> names = student_dict.values()
>>> names
dict_values(['Ali', 'Simon', 'Francesca', 'Cho'])
>>> type(names)
<class 'dict_values'>
>>> pairs = student_dict.items()
>>> pairs
dict_items([('00-01-30', 'Ali'), ('00-02-11', 'Simon'), ('00-05-67', 'Francesca'), ('00-09-88', 'Cho')])
>>> type(pairs)
<class 'dict_items'>
>>> entries = list(pairs)
>>> entries
[('00-01-30', 'Ali'), ('00-02-11', 'Simon'), ('00-05-67', 'Francesca'), ('00-09-88', 'Cho')]
>>> entries[0]
('00-01-30', 'Ali')

使用字典对数据分组

观察 dict 的写法,它和 JSON 差不多,因此可以把 dict 当成一种对象结构。比如,使用一个 dict 表示单个学生,用 key 来表示该学生的属性:

>>> student = {"name": "Josiah","id": "00-02-11","degree": "MEng Computing"}

可以将任何对象作为值,因此嵌套结构也非常常见。

也可以使用元组来对数据进行分组:

>>> course1 = ("60012", "Introduction to Machine Learning")
>>> course2 = ("60006", "Computer Vision")
>>> student_record = ("Josiah", "00-02-11", "MEng Computing", [course1, course2])
>>> student_record[1]
'00-02-11'
>>> student_record[3]
[('60012', 'Introduction to Machine Learning'), ('60006', 'Computer Vision')]
>>> student_record[3][0]
('60012', 'Introduction to Machine Learning')

6. 控制流

6.1 条件分支

除了基本的 ifif-else 语句外,Python 还提供了 if-elif-else 语句。elif 只是 else if 的缩写。

在 Python 3.10 版本以前,Python 没有 switch 语句。为此可以使用 if-elif-else 语句,或者 dict

在 3.10 版本以后,Python 推出 结构化模式匹配,包含 match...case 语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Python >=3.10 only!
key = "c"

match key:
    case "a":
        result = 1
    case "b":
        result = 2
    case "c":
        result = 3
    case _:
        result = -1

print(result)

6.2 循环

Python 里的 while 循环和 Java 一样,这里不再讨论。

Python 里没有 do-while 循环。

Python 里的 for 循环不同于 Java 中的 fori 循环,更像 for-each 循环。

Python 里的 for 循环会遍历一个可迭代对象,比如 list

1
2
3
words = ["I", "have", "a", "dream"]
for word in words:
    print(word)

默认情况下,for 会遍历 dictkey

1
2
3
height_dict = {"Tom": 163, "Dick": 178, "Harry": 182}
for key in height_dict:
    print(f"{key}'s height is {height_dict[key]}")

也可以使用 dict 中的 items() 方法来同时遍历键值:

1
2
for (key, value) in height_dict.items():
    print(f"{key}'s height is {value}")

6.3 range

如果想实现 Java 中的 fori 循环,可以改为遍历一个 range 序列对象。

range 对象是一个可迭代对象,它能够在 for 循环的每次迭代中「动态地」生成下一个值:

1
2
for i in range(0, 10):
    print(i)

上述代码会输出从 0 到 9 的数字序列。

range() 的第一个参数 start 是可选的,默认值为 0,因此 range(10) 等价于 range(0, 10)

range() 还有一个可选的 step 参数,表示步长,控制每次循环后计数器增加多少。

如果想得到一个包含数字序列的列表,可以直接将 range 转换为一个 list

1
numbers = list(range(0, 10))

6.4 enumerate

使用 enumerate() 遍历可迭代对象时,可以同时获取索引和值。

1
2
3
4
names = ["a", "b", "c"]

for index, name in enumerate(names):
    print(index, name)
0 a
1 b
2 c

从 0 开始不好看?

使用 start 参数指定起始编号:

1
2
for index, name in enumerate(names, 1):
    print(index, name)
1 a
2 b
3 c

6.5 zip

zip() 用于把多个可迭代对象中对应位置的元素配对组合起来。

>>> x = [1, 2, 3]
>>> y = [4, 5, 6]
>>> list(zip(x, y))
[(1, 4), (2, 5), (3, 6)]

之所以叫作 zip ,是因为它的作用就像拉链(zipper)一样,把左右两侧的「齿」交替地拉合起来。

zipper

如果各个序列长度不同,默认以最短的序列为准:

>>> x = [1, 2, 3]
>>> y = [4, 5]
>>> list(zip(x, y))
[(1, 4), (2, 5)]

还可以使用 * 对配对后的数据进行「解压」(或者说「转置」):

>>> pairs = [("张三", 85), ("李四", 92)]
>>> names, scores = zip(*pairs)
>>> names
('张三', '李四')
>>> scores
(85, 92)

Python 中的 * 除了表示乘法之外,还常用于处理「一组位置值」:

  • 一组可迭代值:使用 * 后展开为多个独立值
  • 多个独立值:使用 * 后收集为一个集合对象

6.6 comprehension

列表推导式

List comprehension(列表推导式)是 Python 中一种创建列表的简洁写法,把循环、取值和筛选方式写在一行中。

基本语法如下:

1
[表达式 for 元素 in 可迭代对象 if 条件]

比如,把 1 到 5 的每个数平方:

>>> squares = [x ** 2 for x in range(1, 6)]
>>> squares
[1, 4, 9, 16, 25]

还可以添加条件,比如只保留偶数的平方:

>>> res = [x ** 2 for x in range(1, 6) if x % 2 == 0]
>>> res
[4, 16]

列表推导式适合简单的转换和筛选,如果逻辑复杂、需要处理异常,使用普通的 for 循环更好。

集合推导式

集合推导式的写法几乎和列表推导式一样,只不过外层使用 {}

1
{表达式 for 元素 in 可迭代对象 if 条件}

字典推导式

字典推导式则是在集合推导式上更进一步,表达式中需要同时指定键和值的表达式:

1
{键表达式: 值表达式 for 元素 in 可迭代对象 if 条件}
>>> squares = {x: x ** 2 for x in range(1, 6)}
>>> squares
{1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

7. 函数

7.1 函数

在 Python 中,可以使用 def 关键字来定义一个函数。与 Java 不同,无需将函数绑定到某个类上。

>>> def compute_something(x, y, z):
...     a = x + 2 * y
...     a = z * a + 3 * x
...     return a
...
>>> compute_something(5, 6, 3)
66

如果函数不包含 return 语句,自动返回 None

7.2 参数

默认参数

在调用函数时,除了按每个参数的位置指定参数外,Python 还允许提供默认参数,这样就可以不必显式地为每个形参提供值。

>>> def do_something(a, b, c=2, d="great", e=5, f="john", g="python", h=-1, i=0, j=[]):
...     print(f"{a}; {b}; {c}; {d}; {e}; {f}; {g}; {h}; {i}; {j}")
...     return None
...
>>> do_something('first', 'second')
first; second; 2; great; 5; john; python; -1; 0; []
>>> do_something("first", "second", "arg for c", "arg for d")
first; second; arg for c; arg for d; 5; john; python; -1; 0; []

注意,没有默认值的参数必须放在有默认值的参数前面,所以下面这种写法是错的:

1
2
def my_function(a="first", b, c, d="default d"):
    print(f"{a}; {b}; {c}; {d}")

关键字参数

对于一个提供了默认参数的函数,调用者可以使用关键字参数为这些可选参数提供值,而不必考虑参数的位置。

以先前的 do_something() 为例:

>>> do_something("first", "second", i=2, f="josiah")
first; second; 2; great; 5; josiah; python; -1; 2; []

注意,位置参数必须写在关键字参数之前,所以下面这种调用方式是错的:

do_something(d="no good", "first", "second")

如果一个函数的参数有很多,并且难以从类型上区分各个参数,应该使用关键字参数来调用函数,以提升代码的可读性。

7.3 内置函数

Python 提供了许多内置函数,在 官方文档 里可以看到有哪些可用的内置函数。

前面已经介绍过一些内置函数,比如 len()print()isinstance()id() 等。

有时会发现 any()all() 非常有用:

>>> all([True, True, True, True])
True
>>> all([True, False, True, True])
False
>>> any([True, False, True, True])
True

除此之外,使用:

  • sorted() 对一个序列进行排序
  • reversed() 反转一个序列

7.4 函数作为对象

Python 里一切皆对象,函数也不例外。

>>> type(len)
<class 'builtin_function_or_method'>
>>> type(print)
<class 'builtin_function_or_method'>
>>> def func():
...     return 1
...
>>> type(func)
<class 'function'>
>>> type(func())
<class 'int'>

所以可以把一个函数赋值给一个变量:

>>> my_len = len
>>> type(my_len)
<class 'builtin_function_or_method'>
>>> my_len([1, 2, 3])
3

当然,也可以把一个函数作为输入参数传递给另一个函数。

7.5 任意数量的位置参数

有时定义一个函数时可能并不知道调用者会提供多少个参数,比如 max() 内置函数可以传入任意数量个参数。

那这要怎么实现呢?

在参数名前使用 *,Python 会将这些参数包装成一个元组,然后赋值给参数:

>>> def enroll(*courses):
...     print(type(courses))
...     for course in courses:
...         print(course)
...
>>> enroll("Probabilistic Inference", "Introduction to Machine Learning","Computer Vision", "Graphics")
<class 'tuple'>
Probabilistic Inference
Introduction to Machine Learning
Computer Vision
Graphics

7.6 任意数量的关键字参数

如果允许用户输入大量关键字参数,这可能会变得难以管理。

为了便于管理,可以使用 ** 来接收任意数量的关键字参数,这次 Python 会将这些参数包装成一个 dict,后续可以根据需要处理这些参数。

>>> def parse_args(options):
...     for key, value in options.items():
...         if key == "background":
...             print(f'background {value}')
...         elif key == "width":
...             print(f'width {value}')
...         elif key == "avatar":
...             print(f'avatar {value}')
...         else:
...             print(f"Unknown keyword {key}.")
...
>>> def customise_page(**kwargs):
...     parse_args(kwargs)
...
>>> customise_page(background="red", width=500, avatar="selfie.jpg")
background red
width 500
avatar selfie.jpg

8. 面向对象编程

总体的面向对象编程概念与 Java 相同,但 Python 中有许多具体细节的实现方式不同,需要采用在 Java 中接触过的不同思维方式。

还有一点,在开始定义一个类之前,想清楚需要的究竟是一个真正的类,还是仅仅需要类的结构。如果是后者,那使用 dict 更好。

8.1 类构造器

怎么在创建一个 Person 对象时输出一条消息?

需要在类里定义一个 __init__() 方法:

1
2
3
4
5
6
class Person:
    def __init__(self):
        print("I created a person! It's alive!")

manager = Person()
print(manager)

执行到 manager = Person() 时,输出 I created a person! It's alive!;后续打印 manager 对象时,又会输出类似 <__main__.Person object at 0x0000026794564440> 的信息。

在 Python 中,__init__() 方法充当构造器。两侧的连续两个下划线是 Python 的规定,表示这是一个会执行特殊操作的魔术方法(或 dunder 方法,dunderdouble underscores)。

在创建一个类的实例时(执行到 manager = Person() 时),Python 会在堆里创建一个新的对象(类型是 Person),并执行 __init__() 方法,因此可以在 __init__() 方法里定义创建对象后要发生的操作。

__init__() 方法的第一个参数应该 始终是 self,表示刚刚创建的 Person 对象,正因如此,后续可以在该方法内部操作 self 对象。self 就类似 Java 里的 this 关键字。

细节

执行到 manager = Person() 时,Python 会:

  1. 先调用 Person.__new__() 方法在内存中为新对象分配一些空间;
  2. 然后再调用 Person.__init__(self) 方法,通常在这个方法向对象添加属性,就像这个方法的名字一样,进行 init,即做初始化操作;
  3. 最后将这个创建的新对象返回给调用者。

constructor

__init__() 方法有默认实现,因此不显式定义它也能创建出新对象,不过大多数时候都需要定义它。

8.2 属性

这其实和 Java 差不多,只不过不用在构造器外显式声明每个属性的类型和名字:

1
2
3
4
5
6
7
8
9
10
11
class Person:
    def __init__(self, name, age, nationality="uk"):
        self.name = name
        self.age = age
        self.nationality = nationality

manager = Person("Josiah", 20, "malaysia")
print(manager.name)
print(manager.nationality)
manager.age = 50
print(manager.age)

执行到 manager = Person("Josiah", 20, "malaysia") 时,为 Person 实例分配新的堆内存空间,然后把 Josiah20malaysia 传递给 Person.__init__()

在执行 __init__() 时(上述代码中的 3 - 5 行),为该对象分配属性和值。可以把 self 当成一个可变对象,能够直接向其添加属性。当该方法执行完后,Python 自动将 self 返回给调用者。

默认情况下,属性都是 public 的,那私有属性要怎么办呢?

8.3 方法

Python 里的方法和函数差不多,只不过方法的第一个参数 必须self,表示当前对象的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Person:
    def __init__(self, name, age, nationality="uk"):
        self.name = name
        self.age = age
        self.nationality = nationality

    def introduce_self(self):
        print(f"Hey man! Name's {self.name}, from {self.nationality}!")

    def lie_about_age(self):
        print(f"I'm {self.age - 5} years old. Really.") 

    def emigrate(self, new_country):
        self.nationality = new_country

    def think_random_thought(self):
        return "Why is London called London?"


stranger = Person("Josiah", 20, "malaysia")

stranger.emigrate("uk")
stranger.introduce_self()
stranger.lie_about_age()

thought = stranger.think_random_thought()
print(thought)
Hey man! Name's Josiah, from uk!
I'm 15 years old. Really.
Why is London called London?

在调用 stranger.emigrate("uk") 方法时,Python 实际会转换为 Person.emigrate(stranger, "uk") 进行调用,这就是为什么在调用方法时不用再为 self 参数提供值。当然,也可以自己直接调用 Person.emigrate(stranger, "uk"),但更建议用传统的面向对象风格的方法调用来保持可读性。

显式使用 self 能够让代码有更少的歧义且更易读,这是 Python 设计哲学中「显式优于隐式」的体现。与 Java 相比,除非有歧义,可以不使用 this 关键字。

8.4 魔术方法

这有点像 Java 里 Object 类中的方法。除 __init__() 方法外,Python 还提供了许多魔术方法,让自定义类像 Python 内置类一样运行。

__str__()

用于打印对象或将对象转换为字符串,在调用 str(x) 函数时就会调用这个方法,类似 Java 里的 toString() 方法。

常用数学运算符重载

魔术方法 重载的运算符
__add__() +
__sub__() -
__mul__() *
__truediv__() /
__pow__() **

常用布尔运算符重载

魔术方法 重载的运算符
__lt__() <
__le__() <=
__gt__() >
__ge__() >=
__eq__() ==
__ne__() !=

集合相关魔术方法

  • __len__()len(x) 会调用 x.__len__()
  • __contains__()item in x 会调用 x.__contains__(item)
  • __getitem__()x[key] 会调用 x.__getitem__(key)
  • __setitem__()x[key] = item 会调用 x.__setitem__(key, item)
  • __iter__():允许类实例在 for 循环中使用

8.5 继承

与 Java 一样,Python 也支持继承,并且所有类默认也都是 object 的子类。

看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class GameCharacter:
    def __init__(self, name, health=50, strength=30, defence=20):
        self.name = name
        self.health = health
        self.strength = strength
        self.defence = defence

    def attack(self, victim):
        victim.health = victim.health - self.strength
        print(f"Bam! {self.name} attacked {victim.name}.", 
             f"{victim.name}'s health is now {victim.health}")

    def defend(self, attacker):
        self.health = self.health - attacker.strength * 0.25
        print(f"{self.name} defended against {attacker.name}.",
             f"{self.name}'s health is now {self.health}")

    def __str__(self):
        return (f"{self.name} is a GameCharacter (health: {self.health}, "
                f"strength: {self.strength}, defence: {self.defence})"
               )


class Villain(GameCharacter):
    def __init__(self, name, health=50, strength=30, defence=20, evilness=50):
        # call the constructor of the superclass
        super().__init__(name, health, strength, defence)
        self.evilness = evilness

    def evil_laugh(self):
        print("Heheheheheeeeee!!")    

    def __str__(self):
        return (super().__str__().replace("GameCharacter", "Villain").replace(")", ",") 
                + f" evilness: {self.evilness})" 
               )

boy = GameCharacter("Boy", 100, 20, 10)
evilman = Villain("Voldemort", 30, 50, 40, 100)
print(boy)
print(evilman)
evilman.attack(boy)
boy.defend(evilman)
boy.evil_laugh()  # ERROR

24 行表明 VillainGameCharacter 的子类,Python 里的继承语法就是在子类后面的括号里写上父类:

1
2
3
4
5
6
class 父类:
    pass

# Python 支持多继承
class 子类(父类1, 父类2):
    pass

如果子类自己定义了 __init__(),父类的 __init__() 不会自动执行,需要使用 super() 显式调用父类的构造器,否则子类不会继承父类的属性,就像 27 行那样。

同样,如果在子类里重写了父类里的方法,也可以通过 super() 访问父类里的原始方法,就像 34 行那样。

8.6 封装

前面提到,Python 里的属性默认都是 public 的。

那怎么定义一个私有属性呢?

在 Java 里,通常把字段声明为 private,然后通过 gettersetter 来暴露这些私有属性。说实话,有些「脱裤子放屁」,不然也不会有 Lombok 组件库。

Python 里没有真正不可访问的私有属性,更常用「公开」 public 和「非公开」 non-public 来区分。「非公开」也不是说从技术层面上就真的没法访问了,而是告诉其他开发者:这是内部实现,不保证以后不会修改或删除。

一个属性是否公开,取决于它是不是类对外承诺的稳定接口。

PEP 8 – Style Guide for Python Code 中建议:如果拿不准该不该公开,那就设为非公开,因为把非公开改成公开比公开改为非公开更容易。

如今的主流观点是,除非某个属性显然是非公开的,否则默认将属性都设置为公开的。如果未来要控制公开属性的读写,可以使用 property

私有属性

对于已经存在的 Person 类,现在有一个需求:

  • 不直接暴露某个人的年龄,要求得到的年龄总是比实际年龄小 2 岁
  • 可以修改年龄小于 30 岁的人的年龄

按 Javaer 的思考方式,可以有以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Person:
    def __init__(self, name, age):
        self.name = name
        self.__age = age

    def get_age(self):
        return self.__age - 2

    def set_age(self, new_age):
        if new_age < 30:
            self.__age = new_age
        else:
            print("Never! I am always under 30!")


person = Person("Josiah Wang", 20)  
print(person.get_age())
person.set_age(10)
print(person.get_age())
person.set_age(70)
print(person.get_age())
print(person.__age)  ## this won't work!

在 Python 里,可以在一个属性(或方法)前面加两个下划线触发名称改写(name mangling),比如第 4 行里的 self.__age,所以第 22 行是无法运行的。

Person 类中,__age 会被改写为 _Person__age,主要用于避免子类意外使用同名属性覆盖父类属性。从外部直接访问时,它看起来像是一个非公开属性,但这不是真正的访问控制,使用 person._Person__age 仍然能够访问它,不过并不推荐这么做。

除开第 22 行,上述代码能够正常运行,真正的问题是:

  • 与最初的 person.age 相比,person.get_age()person.set_age() 的可读性太差
  • 原来使用 person.age 的地方,也要修改成 get_set_ 方式

使用 Python 里的装饰器(装饰器属于高级内容,不在这里展开,可以参考 Python Decorators)来解决这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    @property
    def age(self):
        return self.__age - 2

    @age.setter
    def age(self, new_age):
        if new_age < 30:
            self.__age = new_age
        else:
            print("Never! I am always under 30!")


person = Person("Josiah Wang", 20) 
print(person.age)
person.age = 10
print(person.age)
person.age = 70
print(person.age)
print(person.__age)  ## this won't work!

再看 18 - 23 行,写法不变,还是使用 person.age 获取属性值或设置属性值,而且又能控制属性的行为。

在第 6 行,引入了 @property 装饰器,这个装饰器能够将第 7 行里的 age() 方法转换成一个 公开可读的 age 属性。当尝试使用 person.age 读取属性值时(比如第 19 行),实际会执行第 7 行的 age() 方法。

同样,在第 10 行,使用 @age.setter(语法是 @property_name.setter)装饰器将 11 行中的 age() 方法转换为一个 公开可写的 age 属性。当尝试使用 person.age 修改属性值时(比如第 20 行),实际会执行第 11 行代码。对了,__init__() 方法中的 self.age = age 也使用了这一点。

如果移除 10 - 15 行,此时 age 就变为了一个只读属性。

怎么实现 protected 呢?

找下规律,以 age 属性为例:

  • 如果是公开的,直接使用 age,前面没有任何下划线
  • 如果是非公开的,使用 __age,前面两个下划线

protected 介于这两者之间,那使用 _age

还真是这样。

不过,这依旧只是一份约定,而且是一个薄弱的约定。无论是否在子类里,依旧可以直接访问这些属性。_age 前面加一个下划线只是提醒其他人,这是一个 protected 级别的属性,不要使用它。

Python 没有通过严密的保护机制来确保开发者不会违反规则,而是假定大多数开发者都是负责且理性的(难说哦 🤔)。

从软件工程的角度来看,考虑是否真的需要 protected 级别的属性,或者是,考虑是否真的需要使用继承。一般来说,组合大于继承,或者将实例作为方法参数传递,或许会更好。

8.7 静态方法

与 Java 类似,Python 里也有静态方法,但 Python 不是用 static 关键字实现,而是使用 @staticmethod 装饰器。

1
2
3
4
5
6
7
8
9
10
11
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    @staticmethod
    def calculate_sum(num1, num2):
        return num1 + num2

total = Person.calculate_sum(5, 3)
print(total)

8.8 类方法

Python 还有一个类方法,使用 @classmethod 实现。

普通方法绑定实例对象,第一个参数必须是 self;类方法绑定当前类,第一个参数必须是 cls

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person:
    count = 0

    def __init__(self, name, age):
        self.name = name
        self.age = age
        Person.count += 1

    @classmethod
    def get_population(cls):
        return f"Population: {cls.count}"

lecturer = Person("Josiah", 20)
print(Person.get_population())
print(lecturer.get_population())
Population: 1
Population: 1

由于类方法绑定当前类,因此它具有多态性。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person:
    def __init__(self, name):
        self.name = name

    @classmethod
    def anonymous(cls):
        return cls("匿名用户")

class Student(Person):
    pass

student = Student.anonymous()
print(type(student))  # Student

因为 Student.anonymous() 中的 clsStudent,所以最终创建的是 Student 实例,而不是 Person 实例。

9. 模块

9.1 导入模块

除了内置函数和类型外,Python 标准库里还提供了许多模块和包。

和 Java 类似,使用 import 关键字导入需要使用的模块或包。

可以像这样理解,Python 里的内置函数和类型被默认导入了,就像 Java 中 java.lang 包下的类一样,在写代码时可以不用显式导入,其他模块或包没有被默认导入,自然就需要 import 了。

尝试导入 math 模块并使用:

>>> import math
>>> math.sqrt(9)
3.0
>>> math.log10(50)
1.6989700043360187
>>> math.cos(30 * math.pi / 180)
0.8660254037844387

在命令行里,可以使用 help() 来查看某个模块的用法,比如 help(math)

也可以将导入的模块重命名:

>>> import math as m
>>> m.sqrt(9)

或者只导入特定的函数、变量或类,然后像使用内置函数那样使用:

>>> from math import sqrt, log10, cos, pi
>>> sqrt(9)

math 外,标准库里还有许多有用的模块或包,比如:

  • collections
  • os
  • os.path
  • shutil

9.2 自定义模块

Python 支持自行编写一个模块,然后在另一个脚本里导入它。

现有一个 libmonster.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
FACTOR = 5

class Monster:
    def __init__(self, name="Me", food="cookies"):
        self.name = name
        self.food = food


def spawn_monsters():
    return [Monster("Happy", "carrots"), 
            Monster("Bashful", "ice-creams"), 
            Monster("Wild", "cookies")]


def calculate_growthrate(adjustment=3):
    return 25 * FACTOR + adjustment

使用 python libmonster.py 命令运行它,不会产生任何可见输出,因为里面都是定义。

可以再编写一个 game.py,像下面这样导入 libmonster 模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
import libmonster

print(libmonster.FACTOR)

print(libmonster.calculate_growthrate(2))

monsters = libmonster.spawn_monsters()

new_monster = libmonster.Monster("Crazy", "sashimi")
monsters.append(new_monster)

for monster in monsters:
    print(f"{monster.name} love {monster.food}!")

然后再运行 python game.py 就有输出内容了:

5
127
Happy love carrots!
Bashful love ice-creams!
Wild love cookies!
Crazy love sashimi!

__name__

libmonster.py 本身就是一个脚本,在它的末尾加两行代码:

1
2
monster = Monster("Silly", "pizza")
print(f"{monster.name} love {monster.food}!")

使用 python libmonster.py 运行它时,会打印出:

Silly love pizza!

如果再尝试运行 python game.py,会发现一开始也会打印出相同的内容,因为 game.py 里导入了 libmonster 模块。

那有没有什么办法使得:

  • 直接运行 libmonster.py 时,能够打印出内容
  • 而在运行 game.py 时,不打印内容

将代码作为脚本运行时,Python 会将 __name__ 设置为 "__main__",所以在满足这个条件时才打印内容就可以了:

1
2
3
4
5
6
7
def _test_library():
    monster = Monster("Silly", "pizza")
    print(f"{monster.name} love {monster.food}!")


if __name__ == "__main__":
    _test_library()

10. 文件

10.1 处理文本文件

Python 中,文件的读取和写入非常简单。与 Java 相比,不用考虑这个 Stream、那个 Stream,也不用考虑究竟是输入、还是输出。

假设现在有一个名为 test.txt 的文件,可以使用以下方式一次性读取整个文件:

1
2
with open("test.txt", "r") as infile: 
    content = infile.read()

如果文件很大,不推荐使用这种方式。

使用 with 语句,能够在文件操作完毕后自动关闭文件,就像 Java 里的 try-with-resources 一样。

更推荐使用以下方式逐行读取文件:

1
2
3
4
5
# 注意,每行末尾都以 `\n` 结尾
# 使用 `line.strip()` 方法来移除头部或尾部的空格
with open("test.txt", "r") as infile: 
    for line in infile:
        print(line.strip()) 

如果要写入文件,以写入模式打开文件,然后使用文件对象的 write() 方法:

1
2
3
4
with open("output.txt", "w") as outfile:
    outfile.write("First line\n")
    outfile.write(str(2) + "\n")
    outfile.write(f"{3}rd line\n")

10.2 JSON 文件

JSON 就不介绍了,稍微搞过一点 Java 开发都知道这是啥玩意。

在 Python 里,使用 json 模块从 JSON 文件里加载数据或者向 JSON 文件里写入数据。

读取

json 模块能够将 JSON 文件加载为其对应的 Python 对象,通常是 listdict

要加载 JSON 文件里的数据,使用 json.load(file_object)

1
2
3
4
5
6
7
import json

with open("input.json", "r") as jsonfile: 
    data = json.load(jsonfile)

print(type(data))
# 接下来像操作 list 或 dict 一样操作 data

也可以从 JSON 字符串加载,这时需要使用 json.loads(string),这里的 loadsload string 的缩写:

1
2
3
4
5
6
7
import json

json_string = '[{"id": 2, "name": "Basilisk"}, {"id": 6, "name": "Nagaraja"}]'
data = json.loads(json_string)

print(data[0])  # {'id': 2, 'name': 'Basilisk'}
print(data[1]["name"]) # Nagaraja

写入

要将数据写入 JSON 文件,使用 json.dump()

1
2
3
4
5
6
import json

data = {"course": "Introduction to Machine Learning", "term": 1}

with open("output.json", "w") as jsonfile: 
    json.dump(data, jsonfile)

也可以将数据写入字符串,使用 json.dumps()

1
2
json_string = json.dumps(data)
print(json_string)  # {"course": "Introduction to Machine Learning", "term": 1}

10.3 pickle

pickle 是 Python 中的一个模块,用于将 Python 对象以二进制文件的形式序列化到磁盘上,或者从磁盘上反序列化到内存中。

注意:

  • pickle 是 Python 特有的,如果要在不同的编程语言之间共享数据,不应该使用它
  • pickle 在不同的 Python 中可能无法正常工作,确保序列化、反序列化都使用的相同版本的 Python
  • 不要反序列化来源不明的数据,警惕恶意代码

就像处理 JSON 文件一样,使用 pickle.dump(obj, file) 来将数据序列化,使用 pickle.load(file) 来反序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import pickle

courses = {
   70053: {"lecturer": "Josiah Wang", "title": "Python Programming"}, 
   70051: {"lecturer": "Robert Craven", "title": "Symbolic AI"}
}

# Save courses to disk. Note binary mode!
with open("courses.pkl", "wb") as file:
    pickle.dump(courses, file)

# Load courses from disk. Again, it is a binary file!
with open("courses.pkl", "rb") as file: 
    pickled_courses = pickle.load(file)

print(pickled_courses)
## {70053: {'lecturer': 'Josiah Wang', 'title': 'Python Programming'}, 
## 70051: {'lecturer': 'Robert Craven', 'title': 'Symbolic AI'}} 

print(type(pickled_courses)) ## <class 'dict'> 

print(courses == pickled_courses) ## True

10.4 CSV 文件

CSV,即 Comma Separated Value。

CSV 文件是一种纯文本文件,它以表格形式来组织数据。通常,CSV 文件以逗号 , (Comma)来分隔每个数据值。当然,也可以使用其他分隔符,比如制表符 \t、冒号 : 和分号 ;

CSV 文件的第一行通常是列名,可以理解为表格的表头,接下来的才是记录。比如这个 students.csv 文件:

1
2
3
4
5
name,faculty,department
Alice Smith,Science,Chemistry
Ben Williams,Eng,EEE
Bob Jones,Science,Physics
Andrew Taylor,Eng,Computing

当然也可以没有表头,直接全是记录。

读取到 list

操作 CSV 文件需要使用 csv 模块:

1
2
3
4
5
6
7
8
9
10
11
12
import csv

with open("students.csv") as csv_file: 
    # 构造一个 csv.reader 对象,指定分隔符为逗号
    reader = csv.reader(csv_file, delimiter=",") 
    # 从 csv.reader 对象里获取下一项,即表头
    header_row = next(reader) 
    print(f"Column names are {", ".join(header_row)}")

    # 遍历后续的每一行(每行中都是以 str 组成的列表)
    for row in reader: 
        print(f"Student {row[0]} is from the Faculty of {row[1]}, {row[2]} dept.")
Column names are name, faculty, department
Student Alice Smith is from the Faculty of Science, Chemistry dept. 
Student Ben Williams is from the Faculty of Eng, EEE dept. 
Student Bob Jones is from the Faculty of Science, Physics dept. 
Student Andrew Taylor is from the Faculty of Eng, Computing dept.

读取到 dict

如果读取成列表,获取每列的值时需要确定所在列的索引,如果列太多,这很麻烦。

Python 允许使用 csv.DictReader 对象将 CSV 文件读取到 dict 中,然后使用列名为键来访问每一列:

1
2
3
4
5
6
7
8
import csv

with open("students.csv") as csv_file: 
    reader = csv.DictReader(csv_file)

    for row in reader: 
        print(f"Student {row['name']} is from the Faculty of {row['faculty']}, "
              f"{row['department']} dept.")

如果 CSV 文件不包含标题行,可以自行指定键,Python 会按顺序进行映射:

1
2
headers = ["name", "faculty", "department"] 
reader = csv.DictReader(csv_file, fieldnames=headers)

list 中写入

使用 csv.writer 对象将 list 写入到 CSV 文件里:

  • 使用 writerow() 方法写入单行
  • 使用 writerows() 方法一次性写入多行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import csv

header = ["name", "faculty", "department"]

data = [
        ["Alice Smith", "Science", "Chemistry"], 
        ["Ben Williams", "Eng", "EEE"],
        ["Bob Jones", "Science", "Physics"],
        ["Andrew Taylor", "Eng", "Computing"]
       ]

with open("output_students.csv", "w") as csv_file: 
    writer = csv.writer(csv_file) 
    writer.writerow(header)
    writer.writerows(data)

dict 中写入

如果来源数据存在于 dict 中,则需要使用 csv.DictWriter 对象,写入方式与前面一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import csv

header = ["name", "faculty", "department"]

data = [
        {"name": "Alice Smith", "faculty": "Science", "department": "Chemistry"}, 
        {"name": "Ben Williams", "faculty": "Eng", "department": "EEE"},
        {"name": "Bob Jones", "faculty": "Science", "department": "Physics"}
       ]

extra_student = {"name": "Andrew Taylor", 
                 "faculty": "Eng", 
                 "department": "Computing"}

with open("output_students.csv", "w") as csv_file: 
    writer = csv.DictWriter(csv_file, fieldnames=header, 
                            # 使用 | 进行分隔
                            delimiter="|") 
    writer.writeheader() 
    writer.writerows(data)
    writer.writerow(extra_student)