Python文件与上下文管理器
路径符号
在我们打开电脑的文件目录的时候,不同的目录之间是用“\”斜杠来区分的,但是直接照搬到Python中是会出错的
Windows系统的文件路径:C:\编程\Python代码
因为Python中有一些特殊符号是通过"“配合特定字母来表示的,即对普通字母进行转义,获得具有特殊含义的符号,比如”\n"表示换行、"\r"表示回车,如果我们的文件路径中恰切出现了这些特殊字符,那么就会出错
解决方法
- 使用反斜杠来替代斜杠:“C:/sample/ABC/suchas/a.txt”
- 使用双斜杠来代指斜杠(二次转义):“C:\sample\ABC\suchas\a.txt”
- 使用r或者R符号来禁止转义:R"C:\sample\ABC\suchas\a.txt"
在Python中,建议使用os.path.join来构建路径,这样可以确保代码在不同操作系统上的兼容性
Python绝对路径
绝对路径指从文件系统的根目录(如Windows系统上的C:\或UNIX/Linux系统上的/)开始的完整路径。绝对路径提供了到达指定文件或目录的完整地址,可以明确无误地指向文件位置,不受当前工作目录的影响
Windows系统:C:\Users\Username\Documents\file.txt
Linux/UNIX系统:/home/username/documents/file.txt
Python相对路径
相对路径基于当前工作目录,不是从根目录开始。使用相对路径时,路径的起点是当前正在访问的文件夹,当前路径是输入运行命令,执行 python 脚本的路径,而不是指 py 文件所在的路径。相对路径更灵活,易于在不同机器或文件夹结构间移植代码,简化文件访问命令
当前工作目录
获取当前工作目录
import os
print(os.getcwd()) #获取当前工作目录,Linux 等系统中查看当前路径的命令是 pwd
被导入模块中的路径问题
# 主脚本test.py是在目录D:\编程\Python代码\test中使用命令执行的
# example_module.py,位于 D:\编程\Python代码 目录下
import os
print("example_module.py代码的相对路径中的 . 表示:", os.path.abspath("."))
print("被导入模块example_module.py中的当前工作目录", os.getcwd())
# test.py,位于 D:\编程\Python代码 目录下
import os
import example_module
print("test.py代码的相对路径中的 . 表示:", os.path.abspath("."))
print("主脚本test.py中的当前工作目录", os.getcwd())
#输出结果:
"""
example_module.py代码的相对路径中的 . 表示: D:\编程\Python代码\test
被导入模块example_module.py中的当前工作目录 D:\编程\Python代码\test
test.py代码的相对路径中的 . 表示: D:\编程\Python代码\test
主脚本test.py中的当前工作目录 D:\编程\Python代码\test
"""
当前工作目录的案例
file_list = os.listdir('.') #得到当前工作目录中的第一层文件和文件夹
print(file_list)
fp = open('test.txt', 'w') #得到当前工作目录中的名称为'test.txt'的文件
fp = open('./test.txt', 'w') #得到当前工作目录中的名称为'test.txt'的文件
当前工作目录的父目录
当前工作目录的父目录的案例
file_list = os.listdir('..') #得到当前工作目录的父目录中的第一层文件和文件夹
print(file_list)
fp = open('../test.txt', 'w') #得到当前工作目录的父目录中的名称为'test.txt'的文件
当前工作目录的根目录
当前工作目录的根目录的案例
写法一
file_list = os.listdir('/') #得到当前工作目录的根目录中的第一层文件和文件夹
print(file_list)
fp = open('/test.txt', 'w') #得到当前工作目录的根目录中的名称为'test.txt'的文件
写法二
file_list = os.listdir('\\') #得到当前工作目录的根目录中的第一层文件和文件夹
print(file_list)
fp = open('\\test.txt', 'w') #得到当前工作目录的根目录中的名称为'test.txt'的文件
open函数及文件的访问模式
作用
在 Python 中,可以通过 open()函数来访问和管理文本文件
打开一个文件并创建一个文件对象(与磁盘上的真实文件一一对应),IO流会将磁盘文件中的内容和程序中的文件对象中的内容进行同步,通过文件对象自带的多种函数和方法,可以对被打开的文件执行一系列访问和管理操作
newline
- 通用换行模式(Universal new line mode),该模式会把所有的换行符(\r \n \r\n)替换为\n
- 在Python 3,可以通过open函数的newline参数来控制换行模式(Universal new line mode)
- 读取时候,不指定newline,则默认开启Universal new line mode,所有\n, \r, or \r\n被默认转换为\n
- 写入时,不指定newline,则换行符为各系统默认的换行符(\n, \r, or \r\n, ),指定为newline=’\n’,则都替换为\n(相当于Universal new line mode)
- 不论读或者写时,newline=""都表示不转换,即关闭Universal new line mode
返回值
_io.TextIOWrapper类型对象,即被创建的文件对象,为一个迭代器
函数形参
-
file
- 传入一个字符串类型对象,用于指定要打开或者创建的文件的名称
-
mode
-
传入一个字符串类型对象,用于指定模式,x、r、w、a只能选择一个使用
-
r: 读模式(默认),如果文件不存在,则抛出异常;如果文件已存在,则文件指针位于文件开头。只能进行读操作
- "rb"对于任何类型的文件(文本文件和二进制文件)都可以进行读取,因为无论是文本文件还是二进制文件,在存储时都是以二进制形式进行存储,本质都是二进制数据
- "r"需要对要读取的文件内容(二进制数据)进行翻译(编码),但是像图片、视频、压缩包这样的二进制文件是不能够被编码的
- 打开文件的时候,建议使用"rb"而不要使用"r"
-
w:写模式,如果文件不存在,则创建文件;如果文件已存在,则打开时自动清空原有内容。只能进行写操作
-
x:只写模式,如果文件不存在,则创建文件;如果文件已存在,则抛出异常。只能进行写操作
-
a:追加模式,如果文件不存在,则创建文件;如果文件已存在,则不会清空内容,而是文件指针自动移动到文件末尾
-
+:读写模式(不可单独使用),其他模式与该模式联合使用时(比如w+,r+,a+),针对文件对象可以同时允许进行读和写的操作
- 在 r+ 模式下使用 write()函数,并不会先清空文件内容,新内容会添加在文件的开头部分,而且会覆盖开头部分原来已有的内容,其他内容保留
- 在 w+ 模式下使用 write()函数,原有的文件的内容将会先完全被清空,新内容会添加在文件的开头部分
- 在 a+ 模式下使用 write()函数,并不会先清空文件内容,新内容会添加在文件的末尾部分
-
b:二进制模式(不可单独使用),打开文件时不允许指定encoding形参
-
t:文本模式(默认,不可单独使用),打开文件时可以指定encoding形参
- 如果以文本模式打开文件并进行读取(即r),那么会自动对读取的二进制数据进行编码操作,这对于打开一些二进制文件来说是没有必要的,而且可能会出现编码错误的问题。类似随意拿一段二进制数据进行编码,二进制数据的长度可能都对不上,不一定能成功编码
- 如果以文本模式打开文件并进行写入(即w),那么会自动对写入的文本数据进行解码操作
-
U:通用换行模式(Universal new line mode)。该模式会把所有的换行符(\r \n \r\n)替换为\n
- U模式不能和 ‘x’, ‘w’, ‘a’, 或者 ‘+’ 中的任意一个结合使用,即只能和 ‘r’ 结合使用
- 在Python 2中如果需要读取文本文件的内容,那么请用rU来读取(强烈推荐)
- 在Python 3中可以通过open函数的newline参数来控制Universal new line mode,Python 3不推荐用rU模式,如果使用,那么会有警告 DeprecationWarning: ‘U’ mode is deprecated
-
-
buffering
- 传入一个int类型对象,用于指定读写文件的缓存模式。
- 0:表示不缓存
- 1:表示行缓存(仅限文本模式)
- 大于1:表示缓冲区的大小
- 传入一个int类型对象,用于指定读写文件的缓存模式。
-
encoding
- 传入一个字符串类型对象,用于指定对文本进行编码和解码的方式,只适用于文本模式,可以使用Python支持的任何格式,如GBK、utf8、CP936等等
产生异常的情况
由于指定文件不存在、访问权限不够、磁盘空间不够或者其他原因导致创建文件失败,则会抛出异常
案例
fp = open('test.txt', 'w+') #以读写模式打开,test.txt文件,打开时会自动清空内容
print(fp) #打印文件对象,会得到文件名称、open()函数的访问模式及编码方式
#输出结果:<_io.TextIOWrapper name='test.txt' mode='w+' encoding='cp936'>
回车符\r与换行符\n
打字机的回车和换行
在计算机还没有出现之前,有一种叫做电传打字机(Teletype Model 33)的玩意,每秒钟可以打10个字符。但是它有一个问题,就是打完一行,需要进行回车换行这两个操作,此过程,要用去0.2秒,正好可以打两个字符。要是在这0.2秒里面,又有新的字符传过来,那么这个字符将丢失
于是,研究人员想了个办法解决这个问题,就是在每行后面加两个表示结束的字符。一个叫做“回车\r”,告诉打字机把打印头定位在左边界;另一个叫做“换行\n”,告诉打字机把纸向下移一行
打字机的回车换行与计算机的回车换行
首先,打字机打完一行,意味着,从左到右把一行的字打完了,此时打印机的打印头处于最右边,所以,想要继续打字,需要
- 先将把打印头移动到最左边,这个动作被称为回车
- 然后打字所用的纸张,换到下一行,这个动作被称为换行(其是通过打字机的滚筒滚动,将纸向前滚动,就相当于打印头换了一行,处在新一行的最左边的开始的位置了,即所谓的走纸)
把打字机的回车换行对应到计算机系统中
- 回车:将当前光标移动到同一行中的最左边(假设是从左到右的输入方式)
- 换行:保持当前光标的水平位置位置不变,换到下一行
- 将回车和换行联合起来,才是我们所常理解的含义:输入完一行后,回车换行到下一行,即光标不仅仅是回到了最左边,而且也换到了下一行
各种系统中表示回车换行含义所用字符
系统类型 | 回车换行所用字符 |
---|---|
Linux/Unix | \n = Newline = 0x0A = 10 = LF =Line Feed = 换行 |
Mac | \r = Return = 0x0D = 13 = CR = Carriage Return = 回车 |
Windows/Dos | \r \n = 0x0D 0x0A = CR LF = 回车 换行 |
有些操作系统中只用一个换行符\n的原因
- 那时,计算机的存储器还很贵,一些科学家认为在每行结尾加两个字符太浪费了,加一个就可以
回车换行在不同系统中所用不同字符所导致的问题
- Linux中打开Windows中编写的文件
- 由于Linux用\n=0x0A=10=LF来表示换行,所以,打开Windows中编写的文件的时候,如果其中有换行,即其中有\r \n= 0x0D 0x0A,此时,就会被处理为:只将\n字符理解为换行,而把\r字符看作为一个单独的字符进行显示
- 同时由于\r字符对应ASCII的值是0x0D=13,是个控制字符,对应的是用Ctrl+M=一个Ctrl加上M字符输入得到的,所以最终在Linux的文本编辑器中,如Vi中,\r字符显示为一个"^M"
- Mac中打开Windows中编写的文件
- 由于Mac用\r=0x0D=13=CR来表示换行,所以,打开Windows中编写的文件的时候,如果其中有换行,即其中有\r \n= 0x0D 0x0A,此时,就会被处理为:只将\r字符理解为换行,而把\n字符看作为一个单独的字符进行显示
- 同时由于\n字符对应ASCII的值是0x0A=10,是个控制字符,对应的是用Ctrl+J=一个Ctrl加上J字符输入得到的,所以最终在Mac的文本编辑器中,\n字符显示为一个"^J"
- Windows中打开Mac或Linux中的文件
- 经过测试,发现,在windows中,估计把原先的0x0D 0x0A去掉其中任何一个的话,然后用windows下的文本类编辑器再去打开,除了最简单的Windows自带的记事本,无法正常识别回车换行,内容被显示到同一行中之外;其他一些编辑器,如Notepad2,Notepad++,Windows自带的记事本WordPad,都可以正常识别成回车换行,不会将内容都输出在同一行
文件对象
文件对象是一个迭代器,可以直接使用for循环进行遍历
属性讲解
- buffer
- 返回当前文件的缓存区对象
- closed
- 判断文件是否关闭,如果已经关闭,返回True
- fileno
- 文件号(不重要)
- mode
- 返回文件的打开模式
- name
- 返回文件名
方法讲解
案例中test.txt文件中的内容为
1.tell()
作用
得到当前的指针相对于文件开始的偏移量(偏移量的单位是字节)
返回值
int类型对象 一个偏移量
案例
with open('test.txt', 'r') as fp: #使用读模式打开文件
print(fp.tell())
#输出结果:0
with open('test.txt', 'a') as fp: #使用写模式打开文件
print(fp.tell())
#输出结果:24
2.seek()
作用
进行文件指针的重定位(定位的单位是字节)。如果参数为正,则指针往后移动;如果参数为负,则指针往前移动
可以指定参考位置,默认为以文件开头作为参考
- 0:从文件头开始计算
- 1:从当前位置开始计算
- 2:从文件尾开始计算
在文本文件中,没有使用b模式(二进制模式)选项打开的文件,只允许将文件头作为参考计算相对位置,从当前位置或者文件尾计算时就会引发异常
返回值
int类型对象 文件指针当前位置
案例
with open('test.txt', 'r') as fp:
print('此时的偏移量为:', fp.tell())
fp.seek(1) #以文件开始处为参考点,向后移动一个字节
print('此时的偏移量为:', fp.tell())
#输出结果:
"""
此时的偏移量为: 0
此时的偏移量为: 1
"""
with open('test.txt', 'r') as fp: #没有使用b模式选项打开
fp.seek(1, 1)
#引发异常:can't do nonzero cur-relative seeks
with open('test.txt', 'r') as fp: #没有使用b模式选项打开
fp.seek(1, 2)
#引发异常:can't do nonzero end-relative seeks
3.read()
作用
对文件进行内容的读取
可以传入参数,来指定所读的最大字符数(针对于文本文件)或者 字节数(针对二进制文件)
如果传入的参数大于实际可以读取的最大数量,则会在文件结尾自动结束读取
如果不传入参数,默认表示读取该文件中的所有内容
值得注意的是,在进行读取的过程中,文件指针也会进行相应的移动
返回值
字符串类型对象 读取到的文件内容
案例
with open('test.txt', 'r') as fp:
print(fp.read())
#输出结果:
"""
12345678910
01987654321
"""
with open('test.txt', 'r') as fp:
print(fp.read(100))
#输出结果:
"""
12345678910
01987654321
"""
with open('test.txt', 'r') as fp:
print(fp.read(3))
#输出结果:123
with open('test.txt', 'r') as fp:
print('此时的偏移量为:', fp.tell())
print(fp.read(3))
print('此时的偏移量为:', fp.tell())
#输出结果:
"""
此时的偏移量为: 0
123
此时的偏移量为: 3
"""
with open('test.txt', 'r') as fp:
print(fp.read(3))
fp.seek(0) #将文件指针移回文件开始处
print(fp.read(3))
print(fp.read(3))
#输出结果:
"""
123
123
456
"""
with open('test.txt', 'r', newline='') as fp: # newline=''表示关闭通用换行模式(Universal new line mode)
print(repr(fp.read())) # 在Windows系统中,换行符为'\r\n',而在Linux/Unix系统中,换行符为'\n'
print('此时的偏移量为:', fp.tell())
#输出结果:
"""
'12345678910\r\n01987654321'
此时的偏移量为: 24
"""
#以文本模式打开图片,那么会自动对读取的数据进行编码操作,此时可能会出现编码错误的问题
#因为图片为一个二进制文件,不一定能成功编码
with open(R"dog.jpg", "r") as f:
file_data = f.read()
#引发异常:UnicodeDecodeError: 'gbk' codec can't decode byte 0xff in position 0: illegal multibyte sequence
4.readline()
作用
针对文件指针所在行的内容(可能是部分)进行读取
可以传入参数,来指定所读的最大字符数(针对于文本文件)或者 字节数(针对二进制文件)
如果传入的参数大于实际的该行的最大可读取数,则会在行尾自动结束读取
如果不传入参数,默认表示读取该行的所有内容
在进行读取的过程中,文件指针也会进行相应的移动
返回值
字符串类型对象 读取到的内容
案例
# 内置函数repr用于进行二次转化,可以用于显示文件内容中的不可见字符,比如\n
# 比如\n原来表示换行,为一个字符,二次转义以后的\\\n表示两个字符\\和n
with open('test.txt', 'r', newline='') as fp:
print(repr("第一行内容:<%s>" % fp.readline()))
print(repr("第二行内容:<%s>" % fp.readline()))
print(repr("第三行内容:<%s>" % fp.readline()))
#输出结果:
"""
'第一行内容:<12345678910\r\n>'
'第二行内容:<01987654321>'
'第三行内容:<>'
"""
with open('test.txt', 'r') as fp:
print(list(fp.readline()))
#输出结果:['1', '2', '3', '4', '5', '6', '7', '8', '9', '1', '0', '\n']
with open('test.txt', 'r') as fp:
print(fp.readline(100))
#输出结果:['1', '2', '3', '4', '5', '6', '7', '8', '9', '1', '0', '\n']
with open('test.txt', 'r') as fp:
print(fp.readline(3))
#输出结果:123
with open('test.txt', 'r') as fp:
fp.seek(13) # 因为每一行有11个可见字符 12345678910,2两个不可见字符 \r\n
print(fp.readline())
#输出结果:01987654321
5.readlines()
作用
针对文件指针所在位置的后面各行的内容(可能是部分)进行读取(按照\n分隔每一行的),返回值在必要的时候会自动添加上""来表示转义字符
可以传入参数,来指定所读的最大行数
如果传入的参数大于实际的该行的最大可读取的行数,则会在文件末尾自动结束读取
如果不传入参数,默认表示读取后面各行的所有内容
在进行读取的过程中,文件指针会进行相应的移动
返回值
列表类型对象 一个元素为字符串(每一行一个字符串)的列表
案例
with open('test.txt', 'r') as fp:
print(fp.readlines())
#输出结果:['12345678910\n', '01987654321']
with open('test.txt', 'r') as fp:
print(fp.readlines(100))
#输出结果:['12345678910\n', '01987654321']
with open('test.txt', 'r') as fp:
print(fp.readlines(1))
#输出结果:['12345678910\n']
with open('test.txt', 'r') as fp:
fp.seek(13)
print(fp.readlines(1))
#输出结果:['01987654321']
with open('test.txt', 'r') as fp:
lines = fp.readlines(100)
lines_content = list(map(str.strip, lines)) # 通常我们使用strip方法去除末尾的换行符
print(lines_content)
#输出结果:['12345678910', '01987654321']
6.write()
作用
对文件进行内容写入,传入的必须是字符串类型对象
值得注意的是,在进行写入的过程中,文件指针也会进行相应的移动
返回值
int类型对象 写入文件的字符串的字节数
案例
with open('test.txt', 'w') as fp:
print(fp.write('00\n00'))
#输出结果:5
#文件内容:
"""
00
00
"""
with open('test.txt', 'w') as fp:
print(fp.tell())
fp.write('00\n00')
print(fp.tell())
#输出结果:
"""
0
6
"""
with open("1.txt", "r", encoding="utf-8") as f:
file_data = f.read()
print("文件数据:", file_data)
with open("123.txt", "w", encoding="gbk") as f:
f.write(file_data)
#输出结果:
"""
文件数据: Hello_world!(你好世界!)
"""
#文件1.txt的内容:Hello_world!(你好世界!)
#文件123.txt的内容:Hello_world!(ÄãºÃÊÀ½ç!)
with open("1.txt", "rb") as f:
file_data = f.read()
print("文件数据:", file_data)
with open("123.txt", "wb") as f:
f.write(file_data)
#输出结果:
"""
文件数据: b'Hello_world!(\xe4\xbd\xa0\xe5\xa5\xbd\xe4\xb8\x96\xe7\x95\x8c!)\r\n\r\n\r\n'
"""
#文件1.txt的内容:Hello_world!(你好世界!)
#文件123.txt的内容:Hello_world!(你好世界!)
7.writelines()
作用
对文件进行内容写入,传入一个序列,该序列可以是存储字符串的任何可迭代对象,通常是一个字符串列表
在写入内容的过程中,不会自动在各个字符串元素之间添加上换行符
在进行写入的过程中,文件指针也会进行相应的移动
返回值
空类型对象 None
案例
with open('test.txt', 'w') as fp:
fp.writelines(['1', '2', '3'])
#文件内容:123
with open('test.txt', 'w') as fp:
seq = map(str, [1, 2, 3])
fp.writelines(seq)
#文件内容:123
8.seekable()
作用
用来测试当前文件对象是否支持随机访问,如果不支持,则调用文件对象的方法seek()、tell()、truncate()时会出现异常
返回值
布尔类型对象 True 或 False
案例
with open('test.txt', 'w') as fp:
print(fp.seekable())
#输出结果:True
9.flush()
作用
把缓冲区的内容写入文件,但是不关闭文件
返回值
空类型对象 None
案例
fp = open('test.txt', 'w')
fp.flush()
print(fp.closed) #打印文件关闭情况
#输出结果:False
10.close()
作用
把缓冲区的内容写入文件,同时关闭文件,释放文件对象相关资源
返回值
空类型对象 None
案例
fp = open('test.txt', 'w')
fp.close()
print(fp.closed) #打印文件关闭情况
#输出结果:True
上下文管理器
资源关闭
对系统资源比如文件、数据库连接、socket来说,应用程序打开这些资源并执行完业务逻辑以后,必须要做的一件事就是要关闭(断开)该资源
以Python文件为例,如果不对文件进行关闭,在极端情况下会出现错误,提示有太多打开的文件了,因为操作系统允许你打开的最大文件数目是确定的,即不可能打开无限多个文件
问题
如果正常地使用文件对象的close方法进行文件关闭,可能会因为对文件进行读写操作的过程中出现异常而导致close方法无法被正常调用,资源就一直无法得到释放,之前所作的修改都无法保存
fp = open('test.txt', 'w')
fp.write('msg')
1/0 #在调用文件对象的close方法之前,引发异常
fp.close()
#代码效果:文件'test.txt'中空空如也
解决方法
代码的改进方法就是加上一个异常处理机制,实现异常捕获,使得后续代码可以正常执行,资源可以被正常关闭。但是一种更加简洁、优雅的方法就是使用with关键字,即使用上下文管理器进行资源的自动关闭
上下文 context
所谓的上下文,其实就是环境,比如代码"a += 1",此时将之前的代码"a = 0"称为"上文",后面的代码"print(a)“称为"下文”
再比如MySQL中,我们use不同的数据库后,再输入show tables;来查询当前数据库的所有数据表,同样的命令输入,但是所处的数据库不同(即上下文 / 环境不同),导致终端打印出来的信息不同
a = 0 #上文
a += 1 #作为上下文参照的代码
print(a) #下文
上下文管理器 ContextManager
如果一个类对象定义了特殊方法__enter__和__exit__,则我们称该类对象遵守了上下文管理协议,同时使用这个类对应创建的实例对象就被称为上下文管理器(Python面向对象未学习过的同学,请移步Python——面向对象,__enter__和__exit__方法未学习过的同学,请移步Python——魔法方法)
对于一个上下文管理器,可以使用with语句自动调用该类对象的__enter__和__exit__方法,实现资源的自动开启和关闭(即实现对资源的自动管理)。更为重要的是,可以实现不论因为什么原因(哪怕是代码引发了异常)而跳出with块,总能保证__exit__方法被自动调用(即文件被正常关闭),并且可以在代码块执行完毕后自动还原进入该代码块时的上下文
上下文管理语句 with语句
with语句常用于文件操作、数据库连接、网络连接、多线程与多进程同步时的锁对象管理等场合,在实际开发中,读写文件应优先考虑使用上下文管理语句
在文件读写过程中未引发异常的代码
class File: #一个具有__enter__方法和__exit__方法的类对象
def __init__(self, file_name, mode):
print("对象正在被初始化")
self.file_name = file_name
self.mode = mode
def __enter__(self):
print("enter方法被调用")
self.fp = open(self.file_name, self.mode)
return self.fp
def __exit__(self, *args):
print("exit方法被调用", args)
self.fp.close()
with File('test.txt', 'w') as fp: #使用with语句使用上下文管理器
print("开始进行文件读写操作")
fp.write('123')
#输出结果:
"""
对象正在被初始化
enter方法被调用
开始进行文件读写操作
exit方法被调用 (None, None, None)
"""
#文件内容:123
在文件读写过程中引发异常的代码
class File:
def __init__(self, file_name, mode):
print("对象正在被初始化")
self.file_name = file_name
self.mode = mode
def __enter__(self):
print("enter方法被调用")
self.fp = open(self.file_name, self.mode)
return self.fp
def __exit__(self, *args):
print("exit方法被调用", args)
self.fp.close()
with File('test.txt', 'w') as fp:
print("开始进行文件读写操作")
fp.write('123')
1/0
fp.write('456')
#输出结果:
"""
对象正在被初始化
enter方法被调用
开始进行文件读写操作
exit方法被调用 (<class 'ZeroDivisionError'>, ZeroDivisionError('division by zero'), <traceback object at 0x000002065871F980>)
"""
#引发异常:division by zero
#文件内容:123
代码运行过程解析
- 首先先执行代码"File(‘test.txt’, ‘w’)“,创建了一个上下文管理器,期间自动调用init方法,会打印"对象正在被初始化”
- 然后自动调用该上下文管理器的__enter__方法,会打印"enter方法被调用",并把该方法的返回值赋值给fp
- 接着执行读写代码,会打印"开始进行文件读写操作"
- 最后自动调用该上下文管理器的__exit__方法,会打印"exit方法被调用"
- 如果读写代码运行过程中没有出现异常,最后就会打印(None, None, None)
- 如果读写代码运行过程中出现了异常,会对异常信息进行保存,最后就会打印出那些异常信息了
对于上下文管理语句可以实现即使在触发异常的情况下,依旧可以正常关闭资源的原理
- 实际上是引发异常的时候,内部的__exit__方法得到了异常信息,在执行完毕__exit__方法中的代码后,直接进行了资源关闭,然后按照通常抛异常的方式再抛出这个异常,所以程序引发异常之前所做的所有修改能够进行保存
所以with语句集成了自动打开文件(即 __enter__方法实现了资源的调用),自动关闭文件(即__exit__方法,实现了资源的释放),异常捕获这三种功能
上下文管理器的另一种实现方法——装饰器
from contextlib import contextmanager
@contextmanager
def my_open(path, mode):
print("enter方法被调用")
f = open(path, mode)
yield f
print("exit方法被调用")
f.close()
with my_open('test.txt', 'w') as fp:
print("开始进行文件读写操作")
fp.write('123')
#输出结果:
"""
enter方法被调用
开始进行文件读写操作
exit方法被调用
"""
代码运行过程解析
- 首先先执行代码"my_open(‘test.txt’, ‘w’)",调用被装饰的函数my_open,运行其中代码
- 函数调用的内部过程中,会使用生成器,打印"enter方法被调用",使用open函数创建一个文件对象,执行yield语句,返回文件对象f,并赋值给fp
- 接着执行读写代码,会打印"开始进行文件读写操作"
- 最后再次使用生成器,从原来的yield语句继续执行代码,会打印"exit方法被调用" ,调用文件对象的close方法,文件关闭
使用装饰器方式构造的上下文管理器本身不能捕获异常
from contextlib import contextmanager
@contextmanager
def my_open(path, mode):
print("enter方法被调用")
f = open(path, mode)
yield f
print("exit方法被调用")
f.close()
with my_open('test.txt', 'w') as fp:
print("开始进行文件读写操作")
fp.write('123')
1/0
fp.write('456')
#输出结果:
"""
enter方法被调用
开始进行文件读写操作
"""
#引发异常:division by zero
#文件内容:文件'test.txt'中空空如也
可以通过异常处理机制来捕获异常,使得资源正常关闭
from contextlib import contextmanager
@contextmanager
def my_open(path, mode):
print("enter方法被调用")
f = open(path, mode)
try:
yield f
except Exception as e:
print("发生异常:", e)
finally:
print("exit方法被调用")
f.close()
with my_open('test.txt', 'w') as fp:
print("开始进行文件读写操作")
fp.write('123')
1/0
fp.write('456')
#输出结果:
"""
enter方法被调用
开始进行文件读写操作
发生异常: division by zero
exit方法被调用
"""
#文件内容:123