Python之上下文管理器

阅读本文需要一定的python使用基础

文章目录

      • with 语法块
      • 上下文管理器
        • 什么是上下文管理器协议?
      • 总结

在python开发中,我们经常会使用到 with 语句,比如操作一个文件,为了防止未关闭文件句柄,导致资源泄露,就会使用到 with 语句。

你有木有思考过,with语句背后的实现逻辑?为什么它会帮我们处理一些逻辑?上下文管理器又是什么?

本篇文章我们一起了解一下 with 运行原理,以及Python的 上下文管理器

with 语法块

我们先以一个具体的实例进行演示,比如打开一个文件,读取文件的内容

# 打开文件
f = open("test.txt", "r")
for info in f.read():
  # do something
f.close()

很简单的例子,打开一个文件,读取文件内容进行操作,然后关闭资源。

但是,如果我们进行文件内容操作时,出现了异常,导致文件句柄无法释放,进而就会导致资源的泄露。

那如何避免这个问题?

我们可以使用try … finally …来优化

# 打开文件
f = open("test.txt", "r")
try:
  for info in f.read():
    # do something
finally:
	f.close()

这样我们就能保证文件在操作过程中,无论是否发生异常,都能正确关闭资源。

但是这么优化会导致代码很繁琐,可读性变得很差。

针对这种情况我们就可以使用 with 语法块来解决这个问题。

# 打开文件
with open("test.txt", "r") as f:
  for info in f.read():
    # do something

用with语法块进行优化的代码,实现之前相同的功能,而且可读性也很好。

那么with语句究竟怎么运行呢?

上下文管理器

首先,看一下__with__的语法格式。

with context_expression [as target(s)]:
  # do something

可以看到,语法非常简单,只需要一个 with 表达式,然后执行自定义的业务逻辑。

理清几个概念

  1. 上下文表达式: with context_expression [as target(s)] , 如处理文件对象 with open(“test.txt”, “r”) as f
  2. 上下文管理器: context_expression 如处理文件对象 open(“test.txt”, “r”)
  3. f 不是上下文管理器,而是资源对象

但是,with 后面的表达式, 也就是 __context_expression__是随意写的吗?

答案是否定的。with 后面的语法对象需要实现 上下文管理器协议

什么是上下文管理器协议?

在Python中,一个类方法,如果实现了以下方法,就实现了 上下文管理器协议

  • __enter__: 在进入__with__ 语句前调用,返回对象并赋值给 target
  • __exit__:在退出 with 语法块时调用,释放对象

为了便于理解,我们实现一段代码,用Python记录一段代码所花费时间。

  • 示例1

    # 比如我们要统计一个列表,加入10000个数据,需要的时间
    import time  # 导入时间模块
    start = int(time.time())  # 开始时间
    nums = []
    for i in range(10000):
      nums.append(i)
    end = int(time.time())  # 结束时间
    print(f"cost time: {end - start} seconds.")
    

这样就实现了我们想要的功能;但是,我想换种方式,使用 上下文管理器 该怎么做呢?

分三步走:

  1. 先定义一个类方法
  2. 实现 __enter__ 和 __exit__方法
  3. 使用with语法调用

请看具体代码:

  • 示例2

    import time
    
    class Timer:  # 定义类
      def __init__(self):
        self.elapsed = 0
        
      def __enter__(self):   # 实现enter方法
        self.start = int(time.time())
        return self  # 返回对象
        
      def __exit__(self, exc_type, exc_val, exc_tb):  # 实现exit方法
        self.end = int(time.time())
        self.elapsed = self.end - self.start 
        return False   # 与enter保持统一
      
    with Timer() as timer:  # with调用
      nums = []
      for i in range(10000):
        nums.append(i)
    
    print(f"cost time : {timer.elapsed} seconds")  # 打印时间
    

这样我们的需求就满足了。在这个例子中,我们实现了 Timer 类,它分别实现了 __enter__ 和 ____exit__ __方法。

具体解释一下执行流程:

__enter__ 在进行 with 语句前被调用,这个方法的返回值赋给了 with 之后的 timer变量

执行自定义代码

__exit__ 在业务代码块执行完后被调用

此外,如果__with__语句块内发生了异常,那么____exit____ 方法是可以拿到关于异常的信息的,可以选择是否打印出来,本示例未打印。

exc_type: 异常类型

exc_val:异常对象

exc_tb:异常堆栈信息

但是,不得不问了,这样写岂不是代码量增加了,本来我只需三行代码就完成了,为啥要写这么多?

我的回答是:

  1. 在简单的脚本测试中,示例1可以满足需求,完全没必要写这么复杂
  2. 在实际的项目开发,我认为示例2更好:
  • 可以以一种的优雅的方式,处理资源(比如文件操作)
  • 可以把部分处理逻辑写入这块代码,增加代码的可读性
  • 可以处理异常

此时,不得不问了,是不是有更简单的实现上下文管理器的方式?因为有的时候,实在没必要因为一个小功能写这么复杂的代码。

答案是肯定的。Python提供了 __contextlib__模块,使用这个模块可以把__上下文管理器__当做__装饰器__使用。不过在本文中,不再讲述,感兴趣的看官可以自行搜索了解。

总结

总结一下,使用上下文管理器有三个好处:

  1. 提高代码的复用率;
  2. 提高代码的优雅度;
  3. 提高代码的可读性;

主要应用场景:

  1. 资源的开关
  2. 资源的加锁,解锁
  3. 资源的改变、重置
  • 参考资料
    https://zhuanlan.zhihu.com/p/317360115
    https://blog.csdn.net/mydistance/article/details/82730014

你可能感兴趣的:(python)