with语句和上下文管理器详解、最佳实践、示例

发布时间 2023-08-29 01:02:19作者: Allen_Hao

说明

with语句是Python中一种用于管理资源的机制,它与上下文管理器紧密相关。

上下文管理器是一个对象(因此自定义时需要创建一个类),它定义了在进入和退出特定代码块(称为上下文)时要执行的操作。

使用with语句和上下文管理器可以确保资源的正确分配和释放,以及在使用完资源后进行清理工作,从而提高代码的可读性和可维护性。

with语句

with语句是一种优雅的资源管理机制,它在特定代码块的进入和退出时自动调用上下文管理器的相关方法,以确保资源的正确分配和释放。以下是一些with语句的最佳实践、典型用例和详细解释:

最佳实践:

  1. 自动资源管理: 使用with语句可以确保资源(如文件、网络连接、数据库连接等)在使用后被正确关闭、释放或清理,从而避免资源泄露和错误。

  2. 代码清晰简洁: with语句可以显著提高代码的可读性和可维护性,因为它将资源的分配和释放操作封装在一个代码块中,减少了代码中的重复性。

  3. 异常处理: 上下文管理器的__exit__方法可以用于处理异常情况,确保资源在代码块内部发生异常时也能得到正确释放。

 

典型用例:

  1. 文件操作: 使用with语句来自动管理文件的打开和关闭操作。
    1 with open("1.txt", "r") as file:  # 使用with语句,不用手动执行f.close()方法,会自动执行
    2     file_data = file.read()
    3     print(file_data)

     

  2. 数据库连接: 使用with语句来管理数据库连接的打开和关闭。   

            

1 import sqlite3
2 
3 with sqlite3.connect("mydb.db") as db:  # 自动创建mydb.db文件
4     cursor = db.cursor()
5     cursor.execute("SELECT * FROM user")
6     result = cursor.fetchall()
7     print(result)
8 # 数据库连接会在上下文退出时自动关闭

 

CREATE TABLE "user" (
  "id" INTEGER NOT NULL,
  "username" TEXT,
  "password" TEXT,
  PRIMARY KEY ("id")
);

INSERT INTO "main"."user"("id", "username", "password") VALUES (1, 'Allen', '123456');

  

            

        

  1. 网络连接: 使用with语句来管理网络连接的打开和关闭。
    1 import socket
    2 
    3 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
    4     sock.connect(("127.0.0.1", 80))
    5     sock.sendall(b"Hello, server!")
    6 # 网络连接会在上下文退出时自动关闭

解释说明:

   with语句通过调用上下文管理器对象的__enter____exit__方法来实现资源的自动管理。在进入with代码块时,__enter__方法被调用,它可以进行资源的分配、初始化或其他必要的操作,并返回一个在上下文中使用的对象(可选)。

在退出with代码块时,无论代码块是正常退出还是由于异常退出,__exit__方法都会被调用。__exit__方法可以执行资源的释放、清理或其他必要的操作。如果代码块正常退出,exc_typeexc_valuetraceback参数都为None,表示没有发生异常。如果代码块由于异常退出,这些参数提供了关于引发的异常的信息,可以在__exit__方法中进行相应处理。

 1 class MyContext:  # 定义上下文管理器类
 2     def __enter__(self):
 3         print("Entering the context")
 4         return self
 5 
 6     def __exit__(self, exc_type, exc_value, traceback):
 7         print("Exiting the context")
 8         if exc_type:
 9             print(f"An exception of type {exc_type} occurred: {exc_value}")
10         # 返回False将异常继续传播,返回True将异常屏蔽
11         return False
12 
13 
14 with MyContext() as context:  # with 关键字 + 上下文管理器对象
15     print("Inside the context")
16     # 通过抛出异常来模拟异常情况
17     # raise ValueError("Something went wrong")
18 print("Outside the context")

输出:

Entering the context
Inside the context
Exiting the context
Outside the context

 

总之,with语句是一种优雅的资源管理工具,它有助于确保资源的正确使用和释放,提高代码的可读性和可维护性。在文件操作、数据库连接、网络连接等场景下,使用with语句可以大大简化代码,同时减少错误和异常情况的发生。 

上下文管理器

 

上下文管理器的构建:

上下文管理器需要定义两个特殊方法:__enter____exit__

  • __enter__(self):在进入代码块时执行,通常用于资源的分配和初始化操作。它可以返回一个与上下文相关的对象,如果不需要返回值,也可以省略返回语句。

  • __exit__(self, exc_type, exc_value, traceback):在退出代码块时执行,无论代码块是正常退出还是由于异常退出。通常用于资源的释放和清理操作。它的参数提供了关于引发的异常的信息,如果代码块正常退出,这些参数都为None

with语句的使用:

使用with语句可以自动调用上下文管理器的__enter____exit__方法,确保资源的适当分配和释放。

 1 class FileManager:  # 1. 自定义上下文管理器类
 2     def __init__(self, filename, mode):  # 1.1 通过__init__初始化上下文管理器对象
 3         self.filename = filename
 4         self.mode = mode
 5         self.file = None
 6 
 7     def __enter__(self):  # 1.2 执行with 语句之前自动执行
 8         print("__enter__")
 9         self.file = open(self.filename, self.mode)
10         return self.file
11 
12     def __exit__(self, exc_type, exc_value, traceback): # 1.3 执行with 语句之后自动执行
13         print("__exit__")
14         if self.file:
15             self.file.close()
16 
17 
18 # 2. 使用上下文管理器打开文件,不用手动关闭
19 with FileManager("1.txt", "r") as file:  # with  上下文管理器对象 as 别名
20     content = file.read()
21     print(content)
22 
23 # 文件已在上下文退出时自动关闭

输出结果:

__enter__
Allen
__exit__

  

最佳实践和示例:

  • 文件操作: 上下文管理器常用于处理文件操作,确保文件在使用后被正确关闭。
    with open("example.txt", "r") as file:
        content = file.read()
        print(content)
    # 文件已在上下文退出时自动关闭
  • 数据库连接: 使用上下文管理器可以确保数据库连接在使用后被关闭。
     1 import sqlite3
     2 
     3 class DatabaseManager:
     4     def __init__(self, db_name):
     5         self.db_name = db_name
     6         self.conn = None
     7     
     8     def __enter__(self):
     9         self.conn = sqlite3.connect(self.db_name)
    10         return self.conn
    11     
    12     def __exit__(self, exc_type, exc_value, traceback):
    13         if self.conn:
    14             self.conn.close()
    15 
    16 # 使用上下文管理器操作数据库
    17 with DatabaseManager("mydb.db") as db:
    18     cursor = db.cursor()
    19     cursor.execute("SELECT * FROM users")
    20     result = cursor.fetchall()
    21     print(result)
    22 # 数据库连接已在上下文退出时自动关闭
  • 临时修改环境: 使用上下文管理器可以在代码块中临时修改环境变量或配置,并在退出时恢复原始状态。
     1 import os
     2 
     3 class TempEnvironment:
     4     def __init__(self, variable, value):
     5         self.variable = variable
     6         self.value = value
     7         self.old_value = None
     8     
     9     def __enter__(self):
    10         self.old_value = os.environ.get(self.variable)
    11         os.environ[self.variable] = self.value
    12     
    13     def __exit__(self, exc_type, exc_value, traceback):
    14         if self.old_value is None:
    15             del os.environ[self.variable]
    16         else:
    17             os.environ[self.variable] = self.old_value
    18 
    19 # 使用上下文管理器临时修改环境变量
    20 with TempEnvironment("DEBUG_MODE", "1"):
    21     # 执行需要调试模式的操作
    22     pass
    23 # 离开上下文时,环境变量恢复原始状态

     

通过 @contextmanager 的装饰器实现上下文管理器

通过@contextmanager装饰器,可以更轻松地创建自定义的上下文管理器,而不需要显式定义类并实现__enter____exit__方法。这种方法基于生成器函数,利用yield语句将资源的分配和释放操作隔离开来。

@contextmanager装饰器详解:

@contextmanager装饰器来自于Python标准库中的contextlib模块。它用于将一个生成器函数转换为上下文管理器,其中生成器的yield语句之前的代码负责资源的分配,yield语句之后的代码负责资源的释放。

 1 from contextlib import contextmanager
 2 
 3 
 4 @contextmanager
 5 def my_context():
 6     # 进入上下文前的操作,相当于__enter__方法
 7     print("Entering the context")
 8     resource = "resource"
 9     try:
10         yield resource  # 将资源返回给with语句的as变量
11     finally:
12         # 退出上下文后的操作,相当于__exit__方法
13         print("Exiting the context")
14 
15 
16 # 使用@contextmanager装饰的生成器函数作为上下文管理器
17 with my_context() as res:
18     print(f"Inside the context: {res}")
19 # 离开上下文时,自动执行资源释放操作

输出结果:

Entering the context
Inside the context: resource
Exiting the context

  

示例1:简单文件读写操作

 1 from contextlib import contextmanager
 2 
 3 
 4 @contextmanager
 5 def open_file(filename, mode):
 6     file = open(filename, mode)
 7     try:
 8         yield file
 9     finally:
10         file.close()
11 
12 
13 # 使用自定义上下文管理器读写文件
14 with open_file("1.txt", "r") as file:
15     content = file.read()
16     print(content)
17 # 文件会在上下文退出时自动关闭

输出:Allen

示例2:数据库连接

 1 import sqlite3
 2 from contextlib import contextmanager
 3 
 4 
 5 @contextmanager
 6 def database_connection(db_name):
 7     conn = sqlite3.connect(db_name)
 8     try:
 9         yield conn
10     finally:
11         conn.close()
12 
13 
14 # 使用自定义上下文管理器操作数据库
15 with database_connection("mydb.db") as db:
16     cursor = db.cursor()
17     cursor.execute("SELECT * FROM user")
18     result = cursor.fetchall()
19     print(result)
20 # 数据库连接会在上下文退出时自动关闭

输出:[(1, 'Allen', '123456')]

最佳实践:

  1. 使用@contextmanager装饰器可以简化上下文管理器的定义,尤其适用于简单的资源管理场景。

  2. 在生成器函数中,通过yield语句将资源暴露给代码块,这样在进入和退出上下文时都能使用这个资源。

  3. try块中进行资源的分配,将资源返回给代码块后,finally块用于资源的释放和清理。

  4. 使用@contextmanager装饰器的上下文管理器没有__exit__方法,所以无法屏蔽异常,如果需要特定的异常处理,可以在生成器函数中进行处理。

总之,@contextmanager装饰器提供了一种简单且优雅的方法来定义上下文管理器,适用于许多资源管理场景。然而,对于更复杂的上下文管理器,仍然可能需要使用传统的类方式来实现。

 1 '''
 2 使用@contextmanager装饰器创建上下文管理器时,确实无法直接在生成器函数中实现完整的异常屏蔽和处理,
 3 因为没有__exit__方法来捕获异常。
 4 然而,你可以在生成器函数中使用try和except语句来捕获并处理特定的异常,从而实现类似的异常处理逻辑。
 5 '''
 6 
 7 from contextlib import contextmanager
 8 
 9 
10 @contextmanager
11 def safe_division(divisor):
12     try:
13         yield 1 / divisor
14     except ZeroDivisionError:
15         print("Division by zero occurred!")
16     
17 
18 # 使用自定义上下文管理器处理异常
19 with safe_division(0) as result:
20     if result is None:
21         print("Result is not available due to exception.")
22     else:
23         print("Result:", result)

 

 输出:

Traceback (most recent call last):
  File "D:\allen_class\python\base\base\042with语句和上下文管理器\06装饰器实现上下文管理器04-处理异常.py", line 19, in <module>
    with safe_division(0) as result:
  File "D:\Install_soft\python\Lib\contextlib.py", line 139, in __enter__
    raise RuntimeError("generator didn't yield") from None
RuntimeError: generator didn't yield
Division by zero occurred!

  

在上面的示例中,safe_division生成器函数允许你尝试执行除法操作,并在发生ZeroDivisionError异常时捕获并打印错误消息。尽管不能实现完全的异常屏蔽,但通过在生成器函数中进行异常处理,你可以为特定的异常情况提供处理逻辑,以便代码块内部仍能继续执行。

这种方法的限制在于,你只能为特定的异常提供处理逻辑,而不能实现完全的上下文管理器异常屏蔽和处理。如果需要更复杂的异常处理,可能需要使用传统的类方式来定义上下文管理器,以便完全控制异常处理逻辑。

总结:@contextmanager装饰器提供了一种轻量级的创建上下文管理器的方法,但在处理异常方面存在一些限制。如果需要更灵活的异常处理,可以考虑使用传统的上下文管理器类,以实现更复杂的异常屏蔽和处理逻辑。