前面的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)

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