前面的Python基础文章中简单介绍了Python的基础语法和一些常见的操作

这篇文章则会更加深入Python的各项机制

1. 对象与内存

Python中几乎一切都是对象

比如:

1
2
3
4
x = 10
s = "hello"
lst = [1, 2, 3]
d = {"name", "Tom"}

这里的10、"hello"、[1, 2, 3]、字典对象本身,都是对象

这里的

1
x = 10

相当于创建一个整数对象10,然后让名字x指向这个对象

所以在python中,变量更像是标签

更具体的例子:

1
2
x = 10
y = x

这里并不是复制一个新的10,而是让y也指向同一个对象10

1
2
3
x  ───┐
├──> 整数对象 10
y ───┘

1.1 可变对象和不可变对象

1.1.1. 不可变对象

对象创建后,内容不能被原地修改

常见类型有:int、float、bool、str、tuple、forzenset

不可变对象最重要的特点是不能被原地修改:

1
2
3
a = 10

a = a + 1

很多人会以为a = a + 1是把原来的10改成11,时则不然

因为int是不可变对象,所以10这个对象本身不会变成11

实际过程:

1
2
3
4
5
a -> 10

a = a + 1

a -> 11

a只是从指向10,变成了指向新的对象11。

所以这里并不是原来的10被修改了,而是生成了一个新的int:11,然后让a指向新对象。

所以:

1
2
3
4
5
a = 10
print(id(a))

a = a + 1
print(id(a))

输出的两次id不一样

1.1.2. 可变对象

对象创建后,内容可以被原地修改

常见类型:list、dict、set、大多数自定义对象

与不可变对象相反,可变对象是可以被原地修改的

例子:

1
2
3
4
5
lst = [1, 2, 3]
print(id(lst))

lst.append(4)
print(id(lst))

这里输出的两个id是一样的

所以修改可变对象会修改原来的对象,而不是创建一个全新的对象。

1.1.3. 重新赋值与原地修改的区别

重新赋值:

1
2
3
4
5
6
a = [1, 2, 3]
b = a

a = [4, 5, 6]

print(b)

输出:

1
[1, 2, 3]

关系变化:

1
2
3
4
5
6
7
8
9
10
一开始:
a ───┐
├──> [1, 2, 3]
b ───┘

执行 a = [4, 5, 6] 后:

a -> [4, 5, 6]

b -> [1, 2, 3]

原地修改:

1
2
3
4
5
6
a = [1, 2, 3]
b = a

a.append(4)

print(b)

输出:

1
[1, 2, 3, 4]

关系:

1
2
3
a ───┐
├──> [1, 2, 3, 4]
b ───┘

1.2 浅拷贝和深拷贝

拷贝问题主要出现在容器对象上

1.2.1. 浅拷贝

浅拷贝会创建一个新的外层容器,但里面的元素仍然与原来共享

例子:

1
2
3
4
5
6
7
a = [1, 2, 3]
b = a.copy()

b.append(4)

print(a)
print(b)

输出:

1
2
[1, 2, 3]
[1, 2, 3, 4]

说明a和b已经不是同一个列表了

但是注意,浅拷贝只拷贝最外层

例子:

1
2
3
4
5
6
7
a = [[1, 2], [3, 4]]
b = a.copy()

b[0].append(99)

print(a)
print(b)

输出:

1
2
[[1, 2, 99], [3, 4]]
[[1, 2, 99], [3, 4]]

因为浅拷贝之后外面的列表是新的,但是里面的子列表还是同一批对象

1.2.2. 深拷贝

深拷贝会递归地拷贝对象内部的对象

使用copy.deepcopy():

1
2
3
4
5
6
7
8
9
import copy

a = [[1, 2], [3, 4]]
b = copy.deepcopy(a)

b[0].append(99)

print(a)
print(b)

结果:

1
2
[[1, 2], [3, 4]]
[[1, 2, 99], [3, 4]]

1.3. is

1.3.1. is 和 ==

== 比较值

1
2
3
4
a = [1, 2, 3]
b = [1, 2, 3]

print(a == b)

输出:

1
True

is 比较身份

1
2
3
4
a = [1, 2, 3]
b = [1, 2, 3]

print(a is b)

输出:

1
False

1.3.2. 小整数和字符串驻留问题

1
2
3
4
a = 10
b = 10

print(a is b)

结果可能是:

1
True

但是在Python中,一些小整数,短字符串可能会被缓存或复用。

所以

1
2
3
4
a = 10
b = 10

print(a is b)

在不同环境下结果可能是不一样的,所以判断数值是否相等就用==

1.3.3. is常见使用场景

最常见的场景:判断是否为None

1
2
if x is None:
...

因为None是单例对象,整个python程序里通常只有一个None对象。

1.4. 函数传参机制

python的函数传参既不是传统意义上的值传递,也不是传统意义上的引用传递,而是对象引用的传递

这个引用本身是按赋值方式绑定给形参的。

1.4.1. 不可变对象传参

1
2
3
4
5
6
7
def change(x):
x = 20

a = 10
change(a)

print(a)

输出:

1
10

调用过程可以这样理解:
进入函数时:

1
2
3
a ───┐
├──> 10
x ───┘

函数内部执行:

1
x = 20 

结果:

1
2
3
a -> 10

x -> 20

1.4.2. 可变对象传参

1
2
3
4
5
6
7
def change(lst):
lst.append(4)

a = [1, 2, 3]
change(a)

print(a)

输出:

1
[1, 2, 3, 4]

调用过程:
进入函数时:

1
2
3
a   ───┐
├──> [1, 2, 3]
lst ───┘

函数内部执行:

1
lst.append(4)

这是对列表函数进行原地修改

所以函数外面也能看到变化

1.4.3. 形参重新赋值,不影响实参

1
2
3
4
5
6
def change(lst):
lst = [4, 5, 6]

a = [1, 2, 3]

print(a)

输出:

1
[1, 2, 3]

这次没有变,因为:

1
lst = [4, 5, 6]

是让形参lst指向一个新列表,而不是修改原来的列表,所以外面的a没有变化

1.4.4. +=的特殊情况

+=对可变对象和不可变对象的表现不一样

对不可变对象:

1
2
3
4
5
6
7
def change(x):
x += 1

a = 10
change(a)

print(a)

输出:

1
10

因为整数不可变,x += 1 等价于创建新整数,然后让 x 指向它。

对可变对象:

1
2
3
4
5
6
7
def change(lst):
lst += [4]

a = [1, 2, 3]
change(a)

print(a)

输出:

1
[1, 2, 3, 4]

对列表来说:

1
lst += [4]

通常是原地扩展列表,所以会影响外部列表

1.5. 函数默认参数

1
2
3
4
5
6
7
def add_item(item, lst=[]):
lst.append(item)
return lst

print(add_item(1))
print(add_item(2))
print(add_item(3))

会输出:

1
2
3
[1]
[1, 2]
[1, 2, 3]

会有人认为每次调用函数时都会创建一个新的空列表

但是实际上,默认参数只会在函数定义时创建一次

正确写法:

1
2
3
4
5
def add_item(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst

这样每次没有传入 lst 时,都会在函数内部新建一个列表。

2. 作用域与命名空间

Python里“作用域”和“命名空间”是理解变量查找、函数嵌套、闭包、global、nonlocal的基础

2.1. 命名空间

在Python中,变量名本质上是一个“名字”,它会绑定到某个对象

1
x = 10

可以理解成:

1
名字 x -> 整数对象 10

这些名字需要被存放在某个地方,这个“存放名字的地方”叫做命名空间

常见命名空间:

1
2
3
4
内置命名空间:print、len、range、int、str ...
全局命名空间:模块级变量、函数名、类名
局部命名空间:函数内部的变量
嵌套函数命名空间:外层函数中的局部变量

例如:

1
2
3
4
5
6
7
8
x = 10

def func():
y = 20
print(x)
print(y)

func()

命名空间:

1
2
3
4
5
6
全局命名空间:
x -> 10
func -> 函数对象

func 的局部命名空间:
y -> 20

2.2. 作用域

作用域指的是一个名字在代码中可以被访问的范围

1
2
3
4
5
6
def func():
x = 10
print(x)

func()
print(x)

运行结果:

1
NameError: name 'x' is not defined

因为x定义在func()函数内部,所以x是局部变量,只能在函数内部访问

也就是说x的作用域是func函数的内部

2.2.1. 局部作用域

函数内部定义的变量,默认属于局部作用域
例如:

1
2
3
4
5
def func():
x = 10
print(x)

func()

执行流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
调用 func()
↓
创建 func 的局部命名空间
↓
x = 10
↓
局部命名空间中出现:
x -> 10
↓
print(x)
↓
先在局部命名空间中找 x
↓
找到 x = 10

函数执行完毕后,局部命名空间通常会被销毁

2.2.2. 全局作用域

在函数外部定义的变量,属于全局作用域

1
2
3
4
5
6
x = 10

def func():
print(x)

func()

输出:

1
10

这里x定义在函数外部,所以它是全局变量

执行流程:

1
2
3
4
5
6
7
8
9
print(x)
↓
先在 func 的局部作用域找 x
↓
没找到
↓
去全局作用域找 x
↓
找到 x = 10

所以函数内部可以读取全局变量

2.3. 函数内部修改外部变量

2.3.1. 函数内部修改全局变量

先看一个例子:

1
2
3
4
5
6
7
8
x = 10

def func():
x = 20
print(x)

func()
print(x)

输出:

1
2
20
10

这里可以看到,函数里面的x = 20并没有修改外面的x = 10

原因是在函数内部写赋值语句时,Python默认认为这个x是新的局部变量,所以这里实际上是两个不同的x

如果真的想在函数内部修改全局变量,就需要使用global

1
2
3
4
5
6
7
8
x = 10

def func():
global x
x = 20

func()
print(x)

输出:

1
20

这里:

1
global x

表示:函数内部的x不再是局部变量,而是全局变量

global执行过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
全局命名空间:
x -> 10

调用 func()
↓
遇到 global x
↓
说明函数内部的 x 指向全局命名空间中的 x
↓
x = 20
↓
修改全局命名空间:
x -> 20

其实并不推荐在函数内大量使用global,这会让函数更加依赖外部环境,使代码更难维护

global的常见用途:统计函数被调用了多少次

1
2
3
4
5
6
7
8
9
10
count = 0

def visit():
global count
count += 1
print(f"函数被调用了{count}次")

visit()
visit()
visit()

输出:

1
2
3
函数被调用了 1 次
函数被调用了 2 次
函数被调用了 3 次

2.3.2. 函数内部修改外层函数变量

nonlocal用于嵌套函数中

它表示:当前函数中的变量不是局部变量,而是外层函数作用域中的变量

例如:

1
2
3
4
5
6
7
8
9
10
11
def outer():
x = 10

def inner():
nonlocal x
x = 20

inner()
print(x)

outer()

输出:

1
20

2.4. LEGB规则

Python查找名字时遵循LEGB规则

LEGB是四个单词的缩写:

1
2
3
4
L:Local,本地作用域,也就是当前函数内部
E:Enclosing,外层函数作用域,也就是嵌套函数的外层函数
G:Global,全局作用域,也就是当前模块
B:Built-in,内置作用域,比如 print、len、range

查找顺序:

1
Local -> Enclosing -> Global -> Built-in

也就是:

1
2
3
4
5
6
7
当前函数内部
↓
外层函数内部
↓
当前 Python 文件
↓
Python 内置命名空间

如果内层作用域和外层作用域有同名变量,内层名字会遮蔽外层名字

3. 迭代器与生成器

前面的文章有提到过可迭代对象,比如range:

1
2
for x in range(3):
print(x)

只要一个对象可以被iter()处理,并能不断通过next()取出下一个值,它就可以参与迭代

3.1. 可迭代对象

可迭代对象,英文叫iterable

简单来说:可以被for循环遍历的对象,就是可迭代对象

常见的可迭代对象有:

1
2
3
4
5
6
7
list
tuple
str
dict
set
range
file

一个对象只要实现了__iter__()方法,它就是可迭代对象

例如:

1
2
3
4
5
nums = [10, 20, 30]

it = iter(nums)

print(it)

这里的:

1
iter(nums)

本质上会调用:

1
nums.__iter__()

它会返回一个迭代器对象

可迭代对象本身不一定能直接一个一个取值,它通常需要先通过iter()转成迭代器

判断一个对象是否可以迭代:

1
2
3
4
5
from collections.abc import Iterable

print(isinstance([1, 2, 3], Iterable)) # True
print(isinstance("hello", Iterable)) # True
print(isinstance(123, Iterable)) # False

3.2. 迭代器

迭代器,英文叫iterable

迭代器是一个可以不断取出下一个值的对象

它满足两个条件:

  1. 有__iter__()方法
  2. 有__next__()方法

也就是说:

1
2
3
4
5
6
7
from collections.abc import Iterator

nums = [10, 20, 30]
it = iter(nums)

print(isinstance(nums, Iterator)) # False
print(isinstance(it, Iterator)) # True

这里要注意nums是可迭代对象,但它不是迭代器,但它不是迭代器

用next()手动取值

1
2
3
4
5
6
7
nums = [10, 20, 30]

it = iter(nums)

print(next(it)) # 10
print(next(it)) # 20
print(next(it)) # 30

迭代器是一次性的,迭代器会记录自己当前的位置
例如:

1
2
3
4
5
6
7
8
nums = [10, 20, 30]
it = iter(nums)

print(next(it)) # 10
print(next(it)) # 20

for x in it:
print(x)

输出:

1
2
3
10
20
30

这里会发现,for循环只打印了30,因为前面两次next(it)已经把10和20取走了

3.3. for循环的本质

我们平时写的for循环:

1
2
3
4
nums = [10, 20, 30]

for x in nums:
print(x)

python内部大概会把它理解成下面这样:

1
2
3
4
5
6
7
8
9
10
nums = [10, 20, 30]

it = iter(nums)

while True:
try:
x = next(it)
print(x)
except StopIteration:
break

也就是说,for循环的本质:

  1. 先调用iter()得到迭代器
  2. 不断调用next()取值
  3. 遇到StopIteration就结束循环

这里用try except是因为当迭代器没有元素时不会返回None,而是会抛出StopIteration,所以需要这样写

这个try except是异常处理的内容,后面会详细介绍

3.4. 生成器和yield

生成器,英文叫generator本质上是一种特殊的迭代器

生成器不一次性把所有数据都生成出来,而是用到一个,生成一个

yield是生成器的核心

如果一个函数里面输出了yield,它就不再是普通函数,而是生成器函数

例如:

1
2
3
4
5
6
7
8
def my_gen():
yield 10
yield 20
yield 30

g = my_gen()

print(g)

它不会直接输出10、20、30,而是得到一个生成器对象:

1
<generator object my_gen at ...>

完整例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def my_gen():
print("开始")
yield 10

print("继续")
yield 20

print("结束")
yield 30

g = my_gen()

print(next(g))
print(next(g))
print(next(g))

输出:

1
2
3
4
5
6
开始
10
继续
20
结束
30

执行流程:

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
g = my_gen()
↓
不会执行函数体,只创建生成器对象

第一次 next(g)
↓
执行到 yield 10
↓
暂停,并返回 10

第二次 next(g)
↓
从上次暂停的位置继续执行
↓
执行到 yield 20
↓
暂停,并返回 20

第三次 next(g)
↓
从上次暂停的位置继续执行
↓
执行到 yield 30
↓
暂停,并返回 30
对比 return yield
作用 返回结果并结束函数 返回一个值并暂停函数
是否保存状态 不保存 保存
是否能多次返回 通常一次 可以多次
返回对象 普通值 生成器对象

3.5. 生成器表达式

生成器表达式和列表推导式很像

生成器推导式:

1
2
g = (x * x for x in range(5))
print(g)

输出:

1
<generator object <genexpr> at ...>

它可以这样取值:

1
2
3
print(next(g))  # 0
print(next(g)) # 1
print(next(g)) # 4

也可以用for循环:

1
2
3
4
g = (x * x for x in range(5))

for x in g:
print(x)

输出:

1
2
3
4
5
0
1
4
9
16

生成器是一次性的,所以不可以重复遍历

1
2
3
4
5
6
7
g = (x * x for x in range(5))

for x in g:
print(x)

for x in g:
print(x)

第二个for循环不会输出任何内容

因为生成器已经被第一个for消耗完了

生成器表达式经常和这些函数一起用:

1
2
3
4
5
sum()
max()
min()
any()
all()

例如:

1
2
result = sum(x * x for x in range(5))
print(result)

输出:

1
30

4. 函数进阶

4.1. 函数对象

在Python里,函数不仅能被调用,它本身也是一个对象
比如:

1
2
def say_hello():
print("hello")

这里的say_hello不是函数本体的全部含义,而是一个变量名,它指向了一个函数对象

1
say_hello  ->  函数对象

所以也可以这样做:

1
2
3
4
5
def say_hello():
print("hello")

f = say_hello
f()

输出:

1
hello

这里不是复制了一份函数,而是让f也指向同一个函数对象

1
2
3
say_hello ───┐
├──> 函数对象
f ───┘

4.1.1. 函数名加括号和不加括号的区别

函数名不加括号:

1
say_hello

表示函数对象本身

函数名加括号:

1
say_hello()

表示调用这个函数

例子:

1
2
3
4
5
def add(a, b):
return a + b

print(add)
print(add(2, 3))

输出:

1
2
<function add at 0x...>
5

4.1.2. 函数可以放进列表、字典

因为函数是对象,所以可以放到数据结构里

1
2
3
4
5
6
7
8
9
10
def add(a, b):
return a + b

def sub(a, b):
return a - b

funcs = [add, sub]

print(funcs[0](10, 3)) # 13
print(funcs[1](10, 3)) # 7

也可以放进字典里:

1
2
3
4
5
6
7
8
9
10
11
12
13
def add(a, b):
return a + b

def sub(a, b):
return a - b

ops = {
"add": add,
"sub": sub
}

print(ops["add"](10, 3)) # 13
print(ops["sub"](10, 3)) # 7

4.2. 高阶函数

高阶函数定义:一个函数A接收另一个函数B做为参数,或者返回一个函数C,A就是高阶函数

4.2.1. 函数作为参数

例如:

1
2
3
4
5
6
7
def apply(func, x):
return func(x)

def square(n):
return n * n

print(apply(square, 5))

输出:

1
25

4.2.2. 函数作为返回值

函数可以返回另一个函数

1
2
3
4
5
6
7
8
def get_func():
def inner():
print("inner function")

return inner

f = get_func()
f()

输出:

1
inner function

4.2.3. 常见高阶函数:map

把一个函数依次作用到一个可迭代对象的每个元素上

1
2
3
4
5
nums = [1, 2, 3, 4]

result = map(lambda x: x * x, nums)

print(list(result))

输出:

1
[1, 4, 9, 16]

4.2.4. 常见高阶函数:filter

根据函数返回的真假值,筛选函数

1
2
3
4
5
nums = [1, 2, 3, 4, 5, 6]

result = filter(lambda x: x % 2 == 0, nums)

print(list(result))

输出:

1
[2, 4, 6]

4.2.5. 常见高阶函数:sorted

也可以经常配合函数使用

比如按字符串长度排序:

1
2
3
4
5
words = ["python", "c", "java", "go"]

result = sorted(words, key=len)

print(result)

输出:

1
['c', 'go', 'java', 'python']

也可以自定义函数:

1
2
3
4
5
6
7
8
9
10
11
12
students = [
{"name": "Tom", "score": 80},
{"name": "Jack", "score": 95},
{"name": "Alice", "score": 88}
]

def get_score(student):
return student["score"]

result = sorted(students, key=get_score)

print(result)

4.3. lambda 表达式

lambda 用来创建一个简单的匿名函数

基本格式:

1
lambda 参数: 表达式

例如:

1
2
3
f = lambda x: x * x

print(f(5))

输出:

1
25

也可以多参数:

1
2
3
add = lambda a,b: a + b

print()

4.3.1. lambda的特点

没有函数名,所以叫匿名函数

只能写一个表达式

表达式的结果会自动作为返回值

适合写短小的临时函数

lambda表达式常用于高阶函数

4.4. 闭包

内部函数引用了外部函数的变量,并且外部函数返回了内部函数

例子:

1
2
3
4
5
6
7
8
9
10
def outer():
x = 10

def inner():
print(x)

return inner

f = outer()
f()

输出:

1
10

执行流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
调用 outer()
↓
创建局部变量 x = 10
↓
定义 inner()
↓
inner 里面使用了外部变量 x
↓
outer 返回 inner
↓
f = inner
↓
调用 f()
↓
还能访问 x

按照普通理解,outer()执行完后,它的局部变量x应该销毁。

但是因为inner还在使用x,所以Python会把x保存起来

闭包的条件:

  1. 外部函数内部定义了内部函数
  2. 内部函数引用了外部函数的变量
  3. 外部函数返回了内部函数

4.5. 装饰器

在不修改原函数代码的情况下,给函数增加额外功能

装饰器其实就是闭包的应用

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
def decorator(func):
def wrapper():
print("开始执行")
func()
print("执行结束")

return wrapper

def say_hello():
print("hello")

new_func = decorator(say_hello)
new_func()

输出:

1
2
3
开始执行
hello
执行结束

执行流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
decorator(say_hello)
↓
func = say_hello
↓
创建 wrapper 函数
↓
return wrapper
↓
new_func = wrapper
↓
new_func()
↓
执行 wrapper()
↓
打印 "开始执行"
↓
调用原函数 say_hello()
↓
打印 "hello"
↓
打印 "执行结束"

这里的decorator就是一个装饰器

4.5.1. @语法

关于装饰器,Python提供了更简洁的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
def decorator(func):
def wrapper():
print("开始执行")
func()
print("执行结束")

return wrapper

@decorator
def say_hello():
print("hello")

say_hello()

输出:

1
2
3
开始执行
hello
执行结束

这段:

1
2
3
@decorator
def say_hello():
print("hello")

等价于:

1
2
3
4
def say_hello():
print("hello")

say_hello = decorator(say_hello)

装饰器语法@decorator本质上就是:原函数=decorator(原函数)

4.5.2. 有参数的情况

如果原函数有参数,装饰器就要接收参数

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def add(a, b):
return a + b

def decorator(func):
def wrapper(a, b):
print("开始执行")
result = func(a, b)
print("执行结束")
return result

return wrapper


@decorator
def add(a, b):
return a + b


print(add(2, 3))

输出:

1
2
3
开始执行
执行结束
5

4.5.3. *args和**kwargs

这样无论原函数参数是什么,都可以处理

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def decorator(func):
def wrapper(*args, **kwargs):
print("开始执行")
result = func(*args, **kwargs)
print("执行结束")
return result

return wrapper


@decorator
def add(a, b):
return a + b


@decorator
def say(name, age):
print(f"我叫{name},今年{age}岁")


print(add(2, 3))
say("Tom", 18)

输出:

1
2
3
4
5
6
开始执行
执行结束
5
开始执行
我叫Tom,今年18岁
执行结束

4.5.4. 保留原函数信息:functools.warps

普通装饰器无法保留原函数信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)

return wrapper


@decorator
def say_hello():
"""这是 say_hello 函数"""
print("hello")


print(say_hello.__name__)

输出:

1
wrapper

可以看到say_hello被替换成了wrapper

保留函数的信息可以使用:

1
from functools import wraps

完整写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from functools import wraps

def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)

return wrapper


@decorator
def say_hello():
"""这是 say_hello 函数"""
print("hello")


print(say_hello.__name__)
print(say_hello.__doc__)

输出:

1
2
say_hello
这是 say_hello 函数

5. 面向对象编程

面向对象编程,英文叫Object-Oriented Programming,简称 OOP。

核心思想是:把现实世界中的事物抽象成程序中的对象,对象内部保存数据,并提供操作这些数据的方法。

Python是一门支持面向对象编程的语言。在Python中,Python中几乎一切都是对象

每个对象通常都有三个核心特征:

1
2
3
id(x) # 对象的身份
type(x) # 对象的类型
x # 对象的值

5.1. 类和对象

类:对象的模板

对象:根据类创建出来的具体实例

例子:

1
2
class Student:
pass

这里的student就是一个类

创建对象:

1
2
stu1 = Student()
stu2 = Student()

5.1.1. 属性

对象中的数据叫做属性

例如:

1
2
3
4
5
6
7
8
9
10
class Student:
pass

stu = Student()

stu.name = "Tom"
stu.age = 18

print(stu.name)
print(stu.age)

输出:

1
2
Tom
18

这里的name和age就是对象stu的属性

5.1.2. __init__()初始化方法

__init__()是Python类中的一个特殊方法

它会在创建对象时自动调用

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

stu = Student("Tom", 18)

print(stu.name)
print(stu.age)

输出:

1
2
Tom
18

执行这句代码时:

1
stu = Student("Tom", 18)

python大致会做两件事:

  1. 创建一个Student对象

  2. 自动调用__init__(),给对象初始化属性

5.1.3. self

self表示当前正在操作的那个对象本身,这里与c++中的this指针类似

1
2
3
class Student:
def __init__(self, name):
self.name = name

把传进来的 name 保存到当前对象自己的 name 属性中

self一定要作为__init__()的第一个参数,python会自动把新建的实例对象作为第一个实参传给__init__,这个接收实例的形参,约定俗成的名字为self(其实把self换成别的名字也行,只是可读性会大大下降)

5.2. 方法和属性

5.2.1. 实例属性和类属性

实例属性:属于某个具体对象

1
2
3
class Student:
def __init__(self, name):
self.name = name

这里的self.name是实例属性,每个对象都有自己的一份

类属性:属于整个类,所有对象共享

1
2
3
4
5
6
7
8
9
10
11
12
class Student:
school = "XJTLU"

def __init__(self, name):
self.name = name

stu1 = Student("Tom")
stu2 = Student("Jack")

print(stu1.school)
print(stu2.school)
print(Student.school)

输出:

1
2
3
XJTLU
XJTLU
XJTLU

school是类属性,所有对象共享

5.2.2. 实例方法

定义在类中,并且第一个参数是self的方法,叫做实例方法

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

def introduce(self):
print(f"大家好,我叫{self.name},今年{self.age}岁。")

stu = Student("Tom",18)
stu.introduce()

输出:

1
大家好,我叫 Tom,今年 18 岁。

调用时:

1
stu.introduce()

看起来没有传参数,但实际上Python会自动把stu传给self。

所以方法定义时必须要写:

1
2
def introduce(self):
pass

5.2.3. 类方法

类方法使用@classmethod修饰,第一个参数通常叫cls

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

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

@classmethod
def get_count(cls):
return cls.count

stu1 = Student("Tom")
stu2 = Student("Jack")

print(Student.get_count())

输出:

1
2

cls表示当前类本身

类方法常用于操作类属性,或者创建对象的其他构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day

@classmethod
def from_string(cls, date_str):
year, month, day = map(int, date_str.split("-"))
return cls(year, month, day)

d = Date.from_string("2026-06-30")
print(d.year, d.month, d.day)

输出:

1
2026 06 30

5.2.4. 静态方法

静态方法使用@staticmethod修饰

1
2
3
4
5
6
class MathTool:
@staticmethod
def add(a, b):
return a + b

print(MathTool.add(3, 5))

输出:

1
8

静态方法适合放一些和这个类相关,但不依赖对象状态的函数

5.2.5. 私有属性

Python没有c++那种严格的private,但是可以通过命名约定(这里并不是真正的禁止外部访问,而是一种约定)

单下划线开头:_name

1
2
3
class Student:
def __init__(self, name):
self._name = name

单下划线表示:这是内部属性,不建议外部直接访问

但它实际上仍然可以被访问

双下划线开头:__name

1
2
3
class Student:
def __init__(self, name):
self.__name = name

双下划线会触发名字改写,外部不能直接访问:

1
stu.__name # 会报错

这里的名字改写是Python会把__name改成_Student__name

所以实际上:

1
stu._Student__name

是可以访问的

5.2.6. @property

@property是python类中的一个装饰器,作用是:把一个方法伪装成属性来访问

最简单的例子:

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

@property
def name(self):
return self._name

s = Student("Tom")

print(s.name)

输出:

1
Tom

这里是s.name而不是s.name(),因为name已经被@property包装成了一个属性

1
2
3
@property
def age(self):
return self._age

大致等价于:

1
2
3
4
def age(self):
return self._age

age = property(age)

完整写法一般有三个部分:@property、@xxx.setter、@xxx.deleter

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

@property
def age(self):
print("读取age")
return self._age

@age.setter
def age(self, value):
print("设置 age")
if value < 0:
raise ValueError("年龄不能是负数")
self._age = value

@age.deleter
def age(self):
print("删除 age")
del self._age

@property这个的函数名就是外部访问的属性接口

1
2
3
@property
def age(self):
...

定义了一个叫做age的属性接口

1
self.age = age

这里的self.age会调用age的setter

1
s.age # 这里是外部访问

这里的s.age会调用age的getter,也就是@property下面的部分

1
del s.age # 这里是删除

这里的del s.age会调用age的deleter

age是对外暴露的属性名,_age是真正保存数据的变量名

注意,__init__ 接收到的 age,会作为 @age.setter 方法的第二个形参传进去。

上面代码的对应关系相当于:

1
2
3
4
5
6
7
8
9
10
11
12
13
Student(18)
↓
__init__(self, age)
↓
age = 18
↓
self.age = age
↓
调用 @age.setter
↓
def age(self, value):
↓
value = 18

如果不写setter则无法在外部更改

如果不写deleter,就代表这个property不支持用del删除

5.3. 继承

继承表示:一个类可以复用另一个类的属性和方法

被继承的类叫父类,也叫基类

继承别人的类叫子类,也叫派生类

例子:

1
2
3
4
5
6
7
8
9
10
11
class Animal:
def eat(self):
print("动物在吃东西")

class Dog(Animal):
def bark(self):
print("狗在叫")

dog = Dog()
dog.eat()
dog.bark()

输出:

1
2
动物在吃东西
狗在叫

在类名后面的**()**中写父类的名字,就可以继承父类

子类可以复用父类的属性和方法

子类可以重写父类的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Animal:
def speak(self):
print("动物在叫")

class Dog(Animal):
def speak(self):
print("狗在叫")

class Cat(Animal):
def speak(self):
print("猫在叫")

dog = Dog()
cat = Cat()

dog.speak()
cat.speak()

输出:

1
2
狗在叫
猫在叫

这就叫方法重写

5.3.1. super()

如果子类重写了父类的方法,但又想复用父类的逻辑,可以用super()

或许有的人会认为:既然想复用父类的逻辑那就不应该重写

这里super()实际的应用场景是在父类原有的逻辑基础上,再加一点自己的逻辑

__init__是最常见的例子:

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

class Dog(Animal):
def __init__(self, name, age):
super().__init__(name)
self.age = age

dog = Dog("旺财", 3)

print(dog.name)
print(dog.age)

输出:

1
2
旺财
3

这里:

1
super().__init__(name)

表示调用父类的__init__(),如果不super()则:

1
self.name = name

这一句就不会自动执行

5.4. 多态

不同的对象调用同一个方法,可以表现出不同的行为

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Dog:
def speak(self):
print("汪汪汪")


class Cat:
def speak(self):
print("喵喵喵")


def make_sound(animal):
animal.speak()


dog = Dog()
cat = Cat()

make_sound(dog)
make_sound(cat)

输出:

1
2
汪汪汪
喵喵喵

这里 make_sound() 不关心传进来的到底是 Dog 还是 Cat。

它只关心这个对象有没有 speak() 方法。

这就是python典型的动态多态思想:鸭子类型(Duck Typing)
看起来像鸭子、走起来像鸭子、叫起来像鸭子,那它就是鸭子

5.5. 魔术方法

Python中有很多双下划线开头和结尾的方法,例如:

1
2
3
4
__init__
__str__
__len__
__eq__

等

它们叫做魔术方法,也可以叫特殊方法

这些方法可以让自定义对象支持Python内置语法

这里主要介绍几个常用的

2.5.1. __str__

控制对象被print()时的显示效果

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

def __str__(self):
return f"Student(name={self.name}, age={self.age})"

stu = Student("Tom", 18)

print(stu)

输出:

1
Student(name=Tom, age=18)

5.5.2. __len__

让对象可以使用len()

1
2
3
4
5
6
7
8
9
10
class Team:
def __init__(self, members):
self.members = members

def __len__(self):
return len(self.members)

team = Team(["Tom", "Jack", "Alice"])

print(len(team))

输出:

1
3

5.5.3. __eq__

控制两个对象使用==比较时的行为

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

def __eq__(self, other):
return self.name == other.name and self.age == other.age

stu1 = Student("Tom", 18)
stu2 = Student("Tom", 18)

print(stu1 == stu2)

输出:

1
True

6. 异常处理

先看一个例子:

1
print(10 / 0)

这里会报错:

1
ZeroDivisionError: division by zero

这些运行时出现的问题,就叫做异常。

异常处理的作用是在程序出错时,不让程序直接崩溃,而是让我们有机会处理错误

6.1. try/except

最基本的异常处理结构是:

1
2
3
4
try:
可能出错的代码
except 异常类型:
出错后执行的代码

例子:

1
2
3
4
5
6
7
try:
x = int(input("请输入一个数字:"))
print(10 / x)
except ValueError:
print("输入的不是合法数字")
except ZeroDivisionError:
print("不能除以 0")

执行流程:

1
2
3
4
5
6
7
8
进入 try
↓
执行 try 中的代码
↓
如果没有异常:
跳过 except
如果出现异常:
根据异常类型,进入对应的 except

捕获异常信息

有时候我们不仅想知道发生了什么类型的异常,还想拿到异常的具体信息。

1
2
3
4
5
try:
x = int("abc")
except ValueError as e:
print("发生 ValueError")
print(e)

输出:

1
2
发生 ValueError
invalid literal for int() with base 10: 'abc'

这里的e就是异常对象

捕获多个异常

1
2
3
4
try:
x = int(input("请输入数字:"))
print(10 / x)
except (ValueError, ZeroDivisionError)

这表示:如果出现 ValueError 或 ZeroDivisionError,都执行同一段处理逻辑。

捕获所有异常

1
2
3
4
5
try:
x = int(input("请输入数字:"))
print(10 / x)
except Exception as e:
print(e)

Exception是大多数常见异常的父类

比如:

1
2
3
4
5
6
ValueError
TypeError
ZeroDivisionError
FileNotFoundError
IndexError
KeyError

但不推荐一上来就写Exception

可以最后用Exception兜底

1
2
3
4
5
6
7
8
try:
...
except ValueError:
...
except ZeroDivisionError:
...
except Exception as e:
...

裸except

Python允许裸except:

1
2
3
4
try:
...
except:
...

但是它会捕获几乎所有异常,甚至包括一些不应该被普通程序捕获的异常,比如用户按下Ctrl + c产生的KeyboardInterrupt

所以一般不推荐这样写

6.2. else/finally

完整的异常处理结构应该是:

1
2
3
4
5
6
7
8
try:
可能出错的代码
except 异常类型:
出错后执行的代码
else:
没有异常时执行的代码
finally:
无论是否异常都会执行的代码

else 只有在 try 里面没有发生异常时才会执行

finally 无论有没有异常,都会执行

6.3. raise

raise是用来主动抛出异常的

前面的哪些异常都是由Python自动产生的,而raise是程序员主动抛出异常

例如:

1
2
3
4
age = -1

if age < 0:
raise ValueError("年龄不能是负数")

输出:

1
ValueError: 年龄不能是负数

raise的作用是当程序发现某个情况不合法时,主动终止当前流程,并告诉调用者哪里出了问题。

例子:

1
2
3
4
5
6
def set_age(age):
if age < 0:
raise ValueError("年龄不能是负数")
print("年龄设置成功")

set_age(-1)

报错:

1
set_age(-1)

raise抛出异常后,调用者可以用前面所介绍的try/except捕获它

1
2
3
4
5
6
7
8
9
10
def divide(a, b):
if b == 0:
raise ValueError("除数不能为 0")
return a / b


try:
result = divide(10, 0)
except ValueError as e:
print("调用失败:", e)

输出:

1
调用失败:除数不能为0

重新抛出异常

有时候我们想先记录一下错误,然后继续把异常抛出去。

可以这样写:

1
2
3
4
5
try:
x = int("abc")
except ValueError as e:
print("记录日志:", e)
raise

最后会把捕获到的异常重新抛出去

6.4. 异常链

raise from是Python中异常链的语法

当你捕获一个异常后,又抛出另一个异常时,用

1
raise 新异常 from 原异常

表示:新异常是原异常导致的

例子:

1
2
3
4
try:
int("abc")
except ValueError as e:
raise RuntimeError("转换失败") from e

输出:

1
2
3
4
5
ValueError: invalid literal for int() with base 10: 'abc'

The above exception was the direct cause of the following exception:

RuntimeError: 转换失败

如果这里用普通的raise则不会明确表示RuntimeError 是 ValueError 直接导致的,raise from则会更加清楚

常见用途是隐藏底层错误,抛出更高级的错误

还有一种特殊写法:

1
raise 新异常 from None

抛出新异常,但隐藏原来的异常链。

这样可以不暴露底层细节

6.5. 自定义异常

自定义异常本质上就是定义一个类,并继承Exception

比如我们定义一个异常来表示用户余额不足:

1
2
3
4
5
6
7
8
9
10
11
12
class BalanceNotEnoughError(Exception):
pass

def withdraw(balance, amount):
if amount > balance:
raise BalanceNotEnoughError("余额不足")
return balance - amount

try:
new_balance = withdraw(100, 200)
except BalanceNotEnoughError as e:
print("取款失败:", e)

输出:

1
取款失败: 余额不足

自定义异常可以带属性

简单的自定义异常可以只写pass,如果想保存更多信息也可以加入属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class BalanceNotEnoughError(Exception):
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
super().__init__(f"余额不足:当前余额 {balance}")

def withdraw(balance, amount):
if amount > balance:
raise BalanceNotEnoughError(balance, amount)
return balance - amount

try:
withdraw(100, 200)
except BalanceNotEnoughError as e:
print(e)
print("当前余额:", e.balance)
print("取款金额:", e.amount)

7. 模块和包

用Python写大工程时,不可能把所有代码都放在一个.py文件里,所以Python中有两个组织代码的概念:

  1. 模块(module): 一个.py文件

  2. 包(package): 一个包含多个模块的文件

7.1. import的本质

搜索到一个模块,把它执行一遍,然后把此模块对象绑定到当前命名空间中

假设一个模块为math_utils.py:

1
2
3
4
5
6
7
8
# math_utils.py

print("math_utils已执行")

PI = 3.14159

def add(a, b):
return a + b

主文件:

1
2
3
4
5
6
# main.py

import math_utils

print(math_utils.PI)
print(math_utils.add(1, 2))

输出:

1
2
3
math_utils已执行
3.14159
3

import math_utils会执行math_utils里的顶层代码

顶层代码:模块文件被Python读取时,会直接从上到下执行的代码,不缩进在函数、类、if、for 等结构里面的代码

简单来说,所谓顶层代码,就是不在函数、不在类里面的代码。

顶层代码案例:

1
2
3
4
5
6
7
8
9
10
11
print("这是顶层代码")   # 顶层代码

x = 10 # 顶层代码

def func(): # 顶层代码:定义函数这个动作
print("这是函数内部代码") # 不是顶层代码,只有调用 func() 才会执行

class Student: # 顶层代码:定义类这个动作
pass # 类体内部代码,不算普通意义上的顶层业务代码

print("文件执行结束") # 顶层代码

最常见的写法:

1
2
3
4
import math_utils

print(math_utils.PI)
print(math_utils.add(1, 2))

这种方式会把模块名math_utils放进当前命名空间,所以访问里面的内容时,需要加模块名前缀

7.1.1. from import

也可以把模块里面的某些名字直接导入到当前命名空间:

1
2
3
4
from math_utils import PI, add

print(PI)
print(add(1, 2))

但是要注意,这种方式容易造成名字冲突。

7.1.2. import as

可以给模块起别名:

1
2
3
import math_utils as mu

print(mu.add(1, 2))

7.1.3. from module import *

也可以把模块中的很多名字都导入命名空间

1
from math_utils import *

这表示把 math_utils 中的很多名字都导入到当前命名空间,比如PI、add

但是一般不推荐这样写,因为很难看出当前文件的名字到底来自哪里

7.1.4. import的执行次数

1
2
3
import math_utils
import math_utils
import math_utils

执行结果通常是:

1
math_utils已执行

虽然写了三次import math_utils,但是模块顶层代码只执行了一次

原因是Python会把已经导入过的模块缓存到sys.module里

可以验证一下:

1
2
3
4
import sys
import math_utils

print("math_utils" in sys.modules)

输出:

1
True

sys.modules 就像 Python 的模块缓存表。

顺便说一下,sys.modules本质是一个字典

7.2. 模块搜索路径

当写下:

1
import math_utils

后,python会从哪里找math_utils呢?

答案是,它会从一个叫sys.path的列表里查询

这个列表的顺序是:

  1. 当前脚本所在目录

  2. 环境变量 PYTHONPATH 指定的目录

  3. Python 标准库目录

  4. 第三方库目录 site-packages

可以通过__file__来查看一个模块来自哪里

例子:

1
2
3
import random

print(random.__file__)

输出:

1
/usr/lib/python3.12/random.py

有时候可以看到有人这样写:

1
2
3
import sys

sys.path.append("/home/user/my_project")

这样可以临时把某个目录加入模块搜索路径,然后就可以导入这个目录下的模块。

7.3. __name__ == "__main__"

每个Python文件在运行时。都有一个特殊变量:

1
__name__

它的值取决于这个文件是怎么被使用的

7.3.1. 文件被直接执行时

文件test.py:

1
2
3
# test.py

print(__name__)

直接运行:

1
python3 test.py

输出:

1
__main__

也就是说当一个 Python 文件被直接运行时,它的 __name__ 会被设置为 "__main__"

7.3.2. 文件被导入

文件test.py:

1
2
3
# test.py

print(__name__)

文件main.py:

1
2
# main.py
import test

输出:

1
test

当一个 Python 文件被作为模块导入时,它的 __name__ 是模块名

所以:

1
2
if __name__ == "__main__":
main()

的作用是:

如果当前文件是直接运行的,就执行 main()

如果当前文件是被别人导入的,就不要执行 main()

所以一般组织一个可运行文件的结构是:

1
2
3
4
5
6
def main():
# 程序主要逻辑
pass

if __name__ == "__main__":
main()

7.4. 包结构

当项目变大后,只有几个.py文件就不够了

这时就可以使用包结构组织代码

这是一个包结构的模板:

1
2
3
4
5
6
project/
├── main.py
└── package/
├── __init__.py
├── a.py
└── b.py

当包里的模块需要相互导入时,分为相对导入和绝对导入

绝对导入:

1
from package.a import func

相对导入:

1
from .a import func

绝对导入:从项目根包开始写,清晰稳定

相对导入:在包内部比较方便,但不能随便直接运行模块文件

一般推荐用绝对导入,相对导入更适合在包内部使用

7.5. __init__.py

__init__.py是包里面的一个特殊文件

例如:

1
2
3
4
mypackage/
├── __init__.py
├── a.py
└── b.py

传统上,只要一个文件夹里有 __init__.py,Python 就会把这个文件夹当成一个普通包。

现代 Python 中,即使没有 __init__.py,有些情况下也可以形成命名空间包,但是对于初学者和大多数普通项目来说,建议还是写上 __init__.py。

当导入包时,这个包的__init__.py就会执行

例如:

1
2
3
mypackage/
├── __init__.py
└── a.py

__init__.py:

1
2
3
# mypackage/__init__.py

print("mypackage 被初始化了")

main.py:

1
2
3
# main.py

import mypackage

运行结果:

1
mypackage 被初始化了

所以import mypackage本质上会执行__init__.py

__init__.py中可用来控制包对外暴露的接口
假设结构:

1
2
3
4
mypackage/
├── __init__.py
├── calculator.py
└── string_utils.py

calculator.py:

1
2
3
4
5
6
7
# calculator.py

def add(a, b):
return a + b

def sub(a, b):
return a - b

如果在__init__.py中写:

1
from .calculator import add, sub

外部就可以这样用:

1
2
3
from mypackage import add, sub

print(add(1, 2))

而不需要这样:

1
from mypackage.calculator import add, sub

__init__.py可以写一些包初始化的代码

例如:

1
2
3
4
# mypackage/__init__.py

print("初始化 mypackage")
VERSION = "1.0.0"

不过注意:__init__.py中不要写过于复杂的逻辑

8. 文件与上下文管理器

8.1. open()

open()是Python中用于打开文件的内置函数,open()的作用是打开文件,并进行读写

完整函数签名比较长,这里主要关注几个常见的参数:

1
2
3
4
open(file, mode = 'r', encoding = None)
# file:文件位置
# mode:打开模式
# encoding:文本编码

例子:

1
f = open("text.txt", "r", encoding = "utf-8")

打开test.txt文件

使用r,也就是只读模式

使用utf-8编码解析文本

返回一个文件对象f

8.2. 文件对象

open()返回的是文件对象

比如:

1
2
f = open("test.txt", "r", encoding="utf-8")
print(f)

输出:

1
<_io.TextIOWrapper name='test.txt' mode='r' encoding='utf-8'>

通过这个对象,可以调用很多方法来操作文件:

1
2
3
4
5
f.read()
f.readline()
f.readlines()
f.write()
f.close()

8.3. 文件打开模式

8.3.1. "r":只读模式

例子:

1
2
3
4
f = open("test.txt", "r", encoding="utf-8")
content = f.read()
print(content)
f.close()

如果test.txt不存在则会报错:

1
FileNotFoundError

"r"模式要求文件必须已经存在

8.3.2. "w":写入模式

如果文件不存在,则创建新文件

如果文件已经存在,先清空原来的内容,再写入新内容

例子:

1
2
3
f = open("test.txt", "w", encoding="utf-8")
f.write("new content")
f.close()

test.txt:

1
new content

8.3.3. "a":追加模式

"a"是append的意思

保留原文件中存在的内容,把新内容写到文件末尾

例子:

1
2
3
f = open("test.txt", "a", encoding="utf-8")
f.write("new line\n")
f.close()

修改前:
text.txt:

1
old line

修改后:
text.txt:

1
2
old line
new line

8.3.4. "b":二进制模式

处理图片,音频,视频等文件时不能用文本方式读,而是用二进制模式读

1
2
with open("image.jpg", "rb") as f:
data = f.read()

"rb"表示:

1
2
r:读取
b:二进制

同样的,二进制写入:

1
2
with open("copy.jpg", "wb") as f:
f.write(data)

二进制读写不需要写encoding,因为二进制数据不需要用文本编码

8.4. 读取文件:

8.4.1. read()

read():从当前文件指针位置开始,读取字符/字节

函数签名:

1
2
file.read(size=-1, /)
# size:最多读取多少个字符/字节,默认则是读取文章全部内容

假设test.txt:

1
2
3
hello
python
world

例子:

1
2
3
with open("test.txt", "r", encoding="utf-8") as f:
content = f.read()
print(content)

输出:

1
2
3
hello
python
world

read() 会把文件中的内容一次性读出来,返回一个字符串(二进制模式下返回字节序列),适合小文件

8.4.2. readline()

readline():从当前文件指针位置开始,读取一行内容

函数签名:

1
file.readline(size=-1, /)

假设test.txt:

1
2
3
hello
world
python

例子:

1
2
3
4
5
6
7
8
with open("test.txt", "r", encoding="utf-8") as f:
line1 = f.readline()
line2 = f.readline()
line3 = f.readline()

print(line1)
print(line2)
print(line3)

输出:

1
2
3
4
5
hello

world

python

注意中间会多空行,因为readline()读出来的内容通常包含行尾的换行符\n

当文件已经读完,再调用readline(),会返回空字符串

8.4.3. readlines()

readlines():从当前文件指针位置开始,读取剩余所有行,并把每一行作为列表中的一个元素

常见函数签名是:

1
file.readlines(hint=-1, /)

假设test.txt:

1
2
3
hello
world
python

例子:

1
2
3
with open("test.txt", "r", encoding="utf-8") as f:
lines = f.readlines()
print(lines)

输出类似:

1
['hello\n', 'python\n', 'world\n']

8.5. 写入文件

8.5.1. write()

write():把内容写入文件

函数签名:

1
2
file.write(s, /) -> int
# s:要写入的字符串/字节

返回值是实际写入的数量

例如:

1
2
with open("test.txt", "w", encoding="utf-8") as f:
f.write("hello")

test.txt:

1
hello

如果想换行,需要自己写入

8.5.2. writelines()

writelines():写入多行

函数签名:

1
2
file.writelines(lines, /)
# line:可迭代对象,里面的元素必须是字符串

例子:

1
2
3
4
lines = ["hello\n", "python\n", "world\n"]

with open("test.txt", "w", encoding="utf-8") as f:
f.writelines(lines)

注意,writelines()也不会自动添加换行符,也需要自己手动添加

8.6. 文件指针

文件对象内部有一个表示"当前位置"的对象,可以理解为文件指针

假设test.txt:

1
abcdef
1
2
3
4
5
with open("test.txt", "r", encoding="utf-8") as f:
a = f.read(2)
b = f.read(2)
print(a)
print(b)

输出:

1
2
ab
cd

第二次不是从头开始读的原因是,第一次f.read(2)读取了ab,所以文件指针移动到了c的位置,所以第二次就开始从c开始读

8.6.1. tell()

tell():用于获取文件指针现在停在什么位置

函数签名:

1
file.tell() -> int

返回一个整数,表示当前文件位置

假设test.txt:

1
abcdef
1
2
3
4
5
6
7
8
with open("test.txt", "r", encoding="utf-8") as f:
print(f.tell())
a = f.read(2)
print(a)
print(f.tell())
b = f.read(2)
print(b)
print(f.tell())

输出:

1
2
3
4
5
0
ab
2
cd
4

8.6.2. seek()

seek():移动文件指针

函数签名:

1
2
3
file.seek(offset, whence=0, /) -> int
# offset:偏移量,也就是移动到哪个位置
# whence:从哪里开始计算位置,默认是0

返回一个整数,表示移动后的文件位置

假设test.txt:

1
abcdef
1
2
3
4
5
6
with open("test.txt", "r", encoding="utf-8") as f:
print(f.read(2))

f.seek(0)

print(f.read(2))

输出:

1
2
ab
ab

8.7. close()

close():用于关闭文件

打开文件后,程序会占用操作系统资源

如果不关闭文件,可能会导致文件资源被占用;写入的内容可能还停留在缓冲区,没有直接写入磁盘;打开文件太多会报错等问题

函数签名:

1
file.close()

例子:

1
2
3
f = open("test.txt", "w", encoding="utf-8")
f.write("hello")
f.close()

当然,这也存在一些问题,比如中途报错会导致close()不执行,使得文件没有被正常关闭

这个问题可以使用之前讲过的try...finally解决,这样即使中间报错,文件也会被正常关闭

但是这样写比较麻烦,于是python提供了更简洁和优雅的写法:with

8.8 with

with是Python中专门用来管理资源的语法

with在前面的例子中已经有出现过了,这里先展示一个最常见的例子:

1
2
with open("test.txt", "r", encoding="utf-8") as f:
content = f.read()

进入with时,打开文件;在with代码块中,使用文件;离开with时,自动关闭文件

这里不管是正常离开,还是因为异常离开,都会自动关闭

很多人会误以为with是专门用来打开文件的,其实不是

with是用来管理上下文管理器对象的,文件刚好是上下文管理器,所以可以配合with使用

除了文件,很多别的东西也可以用with使用:

1
2
3
4
5
6
7
数据库连接
网络连接
线程锁
临时目录
计时器
事务操作
临时修改环境变量

8.9. 上下文管理器

上下文管理器(Context Manager)

一个对象只要实现了__enter__和__exit__方法,就可以被with语句管理

简单的上下文管理器例子:

1
2
3
4
5
6
7
8
9
10
class MyContext:
def __enter__(self):
print("进入with")
return self

def __exit__(self, exc_type, exc_value, traceback):
print("离开with")

with MyContext() as obj:
print("正在执行代码块")

输出:

1
2
3
进入with
正在执行代码块
离开with

执行流程:

1
2
3
4
5
6
7
8
9
10
11
MyContext()
↓
创建对象
↓
调用 __enter__()
↓
把 __enter__ 的返回值赋给 obj
↓
执行 with 代码块
↓
调用 __exit__()

8.9.1. __enter__

__enter__()在进入with代码块之前自动执行

它的返回值会赋值给as后面的变量:

1
2
3
4
5
6
7
8
9
10
11
class MyContext:
def __enter__(self):
print("调用 __enter__")
return "hello"

def __exit__(self, exc_type, exc_value, traceback):
print("调用 __exit__")


with MyContext() as value:
print(value)

输出:

1
2
3
调用 __enter__
hello
调用 __exit__

也就是说,as value中的value不是一定等于MyContext()创建出来的对象,而是等于__enter__()的返回值

但常见写法就是:

1
2
3
class MyContext:
def __enter__(self):
return self

这样__enter__就返回了上下文管理器对象本身

8.9.2. __exit__

__exit__()在离开with代码块时自动执行

函数形式:

1
2
3
4
5
def __exit__(self, exc_type, exc_value, traceback):
...
# exc_type:异常类型
# exc_value:异常对象
# traceback:异常调用栈

如果with代码块正常执行,没有报错,那么三个参数都是None

正常执行时:

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyContext:
def __enter__(self):
print("进入")
return self

def __exit__(self, exc_type, exc_value, traceback):
print("退出")
print("exc_type:", exc_type)
print("exc_value:", exc_value)
print("traceback:", traceback)

with MyContext():
print("正常执行")

输出:

1
2
3
4
5
6
进入
正常执行
退出
exc_type: None
exc_value: None
traceback: None

出现异常时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyContext:
def __enter__(self):
print("进入")
return self

def __exit__(self, exc_type, exc_value, traceback):
print("退出")
print("exc_type:", exc_type)
print("exc_value:", exc_value)


with MyContext():
print("准备报错")
10 / 0

输出:

1
2
3
4
5
6
7
进入
准备报错
退出
exc_type: <class 'ZeroDivisionError'>
exc_value: division by zero
Traceback ...
ZeroDivisionError: division by zero

__exit__()的返回值决定异常是否继续向外传播,默认返回None

返回None

如果with代码块里报错,异常会继续向外抛出

例如:

1
2
3
4
5
6
7
8
9
10
11
12
class MyContext:
def __enter__(self):
print("进入")
return self

def __exit__(self, exc_type, exc_value, traceback):
print("退出")
return False


with MyContext():
10 / 0

输出:

1
2
3
4
进入
退出
Traceback ...
ZeroDivisionError: division by zero

虽然__exit__()执行了,但异常没有被处理掉

返回True

如果__exit__()返回True,则异常不会继续抛出

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyContext:
def __enter__(self):
print("进入")
return self

def __exit__(self, exc_type, exc_value, traceback):
print("退出")
print("异常已经被处理")
return True


with MyContext():
10 / 0

print("程序继续执行")

输出:

1
2
3
4
进入
退出
异常已经被处理
程序继续执行

8.9.3. with open(...)的底层理解

这段代码:

1
2
with open("test.txt", "r", encoding="utf-8") as f:
content = f.read()

可以近似理解为:

1
2
3
4
5
6
7
8
9
10
11
12
manager = open("test.txt", "r", encoding="utf-8")

f = manager.__enter__()

try:
content = f.read()
except Exception as e:
suppress = manager.__exit__(type(e), e, e.__traceback__)
if not suppress:
raise
else:
manager.__exit__(None, None, None)

真实实现细节肯定更加严谨,但这个足以理解其原理