在 Python 中使用上下文管理器的 5 个层次

发布时间:2023年12月28日

在 Python 中使用上下文管理器的 5 个层次(5 Levels of Using Context Managers in Python)

不仅是关于 "with "的说法

与上下文管理器context managers相对的 "with "语句是初学者最容易混淆的 Python 特性之一。因为并不是每种编程语言都有这种语法。

因此,不幸的是,Python 开发人员的许多面试问题都围绕着这个主题。仅列举几个:

  • Python 中的 "with "语句有什么作用?
  • 你能把上下文管理器context manager写成一个 Python 类吗?
  • 您能将上下文管理器context manager写成一个 Python 函数吗?
  • 如何在 Python 中异步处理上下文管理器?

似乎很吓人?

完全不用担心。本文将使用适合初学者的示例,逐步揭开这一主题的神秘面纱。

1. 什么是上下文管理器?

由于每台机器的计算资源都是有限的,因此每个程序都需要考虑资源管理。比如文件处理,最好在使用完文件后将其关闭。

例如,如果我们在一个循环中打开超过 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” 语句之前,让我们先来看看一种经典的方法。

2. 使用 “try…except…finally” 结构

显然,我们需要确保无论在文件处理过程中发生了什么,文件最终都会被关闭。与其他编程语言一样,我们可以编写 "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” 的出现。让我们不需要使用上述方法来管理资源。

3. 使用 “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 连接等。

好消息是,我们可以自己定义上下文管理器。

4. 定义基于类Class-Based的上下文管理器

如定义所述,上下文管理器需要处理入口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” 语句中使用它。

5. 定义基于函数Function-Based的上下文管理器

在 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...

6. 使用异步上下文管理器Asynchronous Context Managers

在某些情况下,输入input和输出output(I/O)操作非常耗时。异步 IO 是加快软件运行速度的常用方法。

自 Python 3.4 起,编写异步 Python 函数变得非常方便。内置的 “with” 语句还可以与 async 关键字配合使用,最大限度地提高程序的速度。

简单地说,异步进程asynchronous processes意味着我们不需要等待一个任务完成后再开始另一个任务。我们可以在等待现有任务的同时启动另一个任务。

让我们做一个简单的计算。假设Assume我们需要向几个文件中写入一些内容。哪种方式更快?

  • 同步方式Synchronized way:写入一个文件并等待完成。然后在另一个文件上启动相同的进程,以此类推…
  • 异步方式Asynchronous way:写入一个文件,不要等它完成。然后开始处理另一个文件,以此类推…。

哪个方式更快?答案显而易见。

现在,让我们实现一个简单的异步程序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。此外,您还可以使用异步上下文管理器优化程序性能。

文章来源:https://blog.csdn.net/JENREY/article/details/135278055
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。