不仅是关于 "with "的说法
与上下文管理器context managers相对的 "with "语句是初学者最容易混淆的 Python 特性之一。因为并不是每种编程语言都有这种语法。
因此,不幸的是,Python 开发人员的许多面试问题都围绕着这个主题。仅列举几个:
似乎很吓人?
完全不用担心。本文将使用适合初学者的示例,逐步揭开这一主题的神秘面纱。
由于每台机器的计算资源都是有限的,因此每个程序都需要考虑资源管理。比如文件处理,最好在使用完文件后将其关闭。
例如,如果我们在一个循环中打开超过 1000000 个文件,并且像下面的程序一样从不关闭任何文件:
files = []
for x in range(1000000):
files.append(open('test.txt', 'w'))
将出现错误:
OSError: [Errno 24] Too many open files: 'test.txt'
因此,只要不再需要,就应关闭每个已打开的文件。
Python 有相对的内置函数built-in functions,因此我们可以方便地打开和关闭文件。
f = open("test.txt", 'w')
f.write("Hi,Yang!")
f.close()
上述程序是可行的。但是,如果 open()
和 close()
之间的处理过程变得更加复杂,并且出现错误,那么没有人能够保证文件最终会被关闭。
更不用说,如果我们总是需要手动关闭打开的文件,那么当程序变得越来越复杂时,我们很可能会忘记关闭其中的一些文件,而如果 Python 不能自动为我们关闭这些文件,那就真的不够优雅了。
这里介绍了Python中的上下文管理器context managers和 “with” 语句。简单地说,它们是Python中简化资源管理的特性。
在深入研究 “with” 语句之前,让我们先来看看一种经典的方法。
显然,我们需要确保无论在文件处理过程中发生了什么,文件最终都会被关闭。与其他编程语言一样,我们可以编写 "try…except…finally "结构来实现这一点。
例如,下面的程序会打开一个文件,无论 “writing” 操作成功与否,它都会关闭该文件:
f = open("test.txt",'w')
try:
f.write("Yang is writing!")
except Exception as e:
print(f"An error occurred while handling the file: {e}")
finally:
# Always close a file after using it
f.close()
上面的例子是管理资源的经典方法。这种方法非常有效,但如果程序中出现过多的 “try…except…finally”,就显得很难看。
感谢Python 中 “with” 的出现。让我们不需要使用上述方法来管理资源。
“with” 语句是一种语法糖syntax sugar,可使代码更简洁。使用该语句,您无需明确编写 close()
函数,因为打开的资源会自动关闭。
因此,我们可以在不使用 close()
函数的情况下编写一个更优雅的程序,如下所示:
with open("test.txt",'w') as f:
f.write("Yang is writing!")
我们甚至可以在一个 “with” 语句下打开任意多个文件:
with open("test.txt",'w') as f1, open("test2.txt",'w') as f2:
f1.write("Yang is still writing!")
f2.write("Yang is also reading!")
上述使用 “with” 语句的程序运行良好,因为 Python 中的 open()
函数会返回一个上下文管理器context manager。
上下文管理器是一个对象,它定义了执行 with
语句时要建立的运行时上下文。上下文管理器处理进入entry和退出exit所需的运行时上下文,以便执行代码块。- Python 官方文档
并不是说只有在打开文件时才能使用 “with” 语句。只要有上下文管理器context managers,“with” 语句也可以管理其他资源,如数据库连接、HTTP 连接等。
好消息是,我们可以自己定义上下文管理器。
如定义所述,上下文管理器需要处理入口entry和出口exit点。
因此,只要一个 Python 类有 __enter__
和 __exit__
方法,它就是一个上下文管理器context manager,可以与 “with” 语句一起工作。
现在,让我们自己来实现一个基于类的文件上下文管理器:
class FileManager:
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
self.file = None
def __enter__(self):
print("The file is opening...")
self.file = open(self.filename, self.mode)
return self.file
def __exit__(self, exc_type, exc_value, exc_traceback):
print("The file is closing...")
self.file.close()
with FileManager('test.txt', 'w') as f:
f.write('Yang is writing!')
输出结果如下
The file is opening...
The file is closing...
如上例所示,我们可以定义一个 Python 类作为上下文管理器,并在 “with” 语句中使用它。
在 Python 中定义上下文管理器有两种方法。一种是基于类class-based,另一种是基于函数function-based。
基于函数的上下文管理器function-based context manager需要添加一个名为 @contextmanager
的内置装饰器,而在函数内部,必须有一个 yield
语句使其成为一个生成器generator。
如果您还不了解 生成器generators 和 装饰器decorators 的概念。不用担心,你可以这样把基于函数的上下文管理器当作基于类的上下文管理器:在 yield
语句之前是 __enter__
方法,而在 yield
语句之后是 __exit__
方法。
现在,让我们把之前基于类的上下文管理器改为基于函数的上下文管理器:
from contextlib import contextmanager
@contextmanager
def file_manager(filename, mode):
print("The file is opening...")
file = open(filename,mode)
yield file
print("The file is closing...")
file.close()
with file_manager('test.txt', 'w') as f:
f.write('Yang is writing again!')
输出是一样的:
The file is opening...
The file is closing...
在某些情况下,输入input和输出output(I/O)操作非常耗时。异步 IO 是加快软件运行速度的常用方法。
自 Python 3.4 起,编写异步 Python 函数变得非常方便。内置的 “with” 语句还可以与 async
关键字配合使用,最大限度地提高程序的速度。
简单地说,异步进程asynchronous processes意味着我们不需要等待一个任务完成后再开始另一个任务。我们可以在等待现有任务的同时启动另一个任务。
让我们做一个简单的计算。假设Assume我们需要向几个文件中写入一些内容。哪种方式更快?
哪个方式更快?答案显而易见。
现在,让我们实现一个简单的异步程序asynchronous program,异步写入两个文件:
import asyncio
import aiofiles
async def write_file(filename):
async with aiofiles.open(filename, 'w') as f:
print(f"Entered a file named {filename}.")
await f.write("Yang is writing")
print(f'Finished {filename}.')
async def main():
await asyncio.gather(
write_file("test1.txt"),
write_file("test2.txt"),
)
asyncio.run(main())
输出结果如下:
Entered a file named test1.txt.
Entered a file named test2.txt.
Finished test1.txt.
Finished test2.txt.
这意味着在 “test1.txt” 处理完成之前,“test2.txt” 已经打开。这就是为什么我们说这个程序是异步的asynchronous。
顺便说一下,我们使用了 aiofiles
模块中的 aiofiles.open()
函数来替代内置的 built-in open()
函数。因为 open()
函数无法与 async
配合使用。您可以使用 pip
安装此模块:
$ pip install aiofiles
对于初学者beginners来说,上下文管理器context managers和 “with” 语句乍一看似乎很吓人。事实上,它们只是 Python 的语法糖syntax sugar,用于编写优雅、无错误的代码。
正确使用它们可以帮助你的代码变得更 Pythonic。此外,您还可以使用异步上下文管理器优化程序性能。