当前位置:   article > 正文

Python 中 with 用法详解

def函数中with

浅谈 Python 的 with 语句:https://developer.ibm.com/zh/articles/os-cn-pythonwith/

python3,浅谈with的神奇魔法:https://blog.csdn.net/lxy210781/article/details/81176687

Python 的 with 语句详解:https://www.jb51.net/article/51045.htm

深入理解 Python 的 With-as 语句:https://cloud.tencent.com/developer/article/1083148

python with statement 进阶理解:https://www.iteye.com/blog/jianpx-505469

Python 中的with关键字使用详解:https://www.jb51.net/article/92387.htm

由来

with…as 是 python 的控制流语句,像 if ,while一样。with…as 语句是简化版的 try except finally语句。

先理解一下 try…except…finally 语句是干啥的。实际上 try…except 语句和 try…finally 语句是两种语句,用于不同的场景。但是当二者结合在一起时,可以“实现稳定性和灵活性更好的设计”。

1. try…except 语句

用于处理程序执行过程中的异常情况,比如语法错误、从未定义变量上取值等等,也就是一些python程序本身引发的异常、报错。比如你在python下面输入 1 / 0:

  1. >>> 1/0
  2. Traceback (most recent call last):
  3. File "<stdin>", line 1, in <module>
  4. ZeroDivisionError: division by zero

系统会给你一个 ZeroDivisionError 的报错。说白了就是为了防止一些报错影响你的程序继续运行,就用try语句把它们抓出来(捕获)。

try…except 的标准格式:

  1. try:
  2. ## normal block
  3. except A:
  4. ## exc A block
  5. except:
  6. ## exc other block
  7. else:
  8. ## noError block

程序执行流程是:

  1. >执行normal block
  2. >发现有A错误,执行 exc A block(即处理异常)
  3. >结束
  4. 如果没有A错误呢?
  5. >执行normal block
  6. >发现B错误,开始寻找匹配B的异常处理方法,发现A,跳过,发现except others(即except:),执行exc other block
  7. >结束
  8. 如果没有错误呢?
  9. >执行normal block
  10. >全程没有错误,跳入else 执行noError block
  11. >结束

Tips: 我们发现,一旦跳入了某条except语句,就会执行相应的异常处理方法(block),执行完毕就会结束。不会再返回try的normal block继续执行了。

  1. try:
  2. a = 1 / 2 #a normal number/variable
  3. print(a)
  4. b = 1 / 0 # an abnormal number/variable
  5. print(b)
  6. c = 2 / 1 # a normal number/variable
  7. print(c)
  8. except:
  9. print("Error")

结果是,先打出了一个0,又打出了一个Error。就是把ZeroDivisionError错误捕获了。

先执行 try 后面这一堆语句,由上至下:

  • step1: a 正常,打印a. 于是打印出0.5 (python3.x以后都输出浮点数)
  • step2: b, 不正常了,0 不能做除数,所以这是一个错误。直接跳到except报错去。于是打印了Error。
  • step3: 其实没有step3,因为程序结束了。c是在错误发生之后的b语句后才出现,根本轮不到执行它。也就看不到打印出的c了

但这还不是 try/except 的所有用法

except后面还能跟表达式的!

所谓的表达式,就是错误的定义。也就是说,我们可以捕捉一些我们想要捕捉的异常。而不是什么异常都报出来。

异常分为两类:

  • python标准异常
  • 自定义异常

我们先抛开自定义异常(因为涉及到类的概念),看看 except 都能捕捉到哪些 python 标准异常。请查看菜鸟笔记

https://www.runoob.com/python/python-exceptions.html

2. try…finallly 语句

用于无论执行过程中有没有异常,都要执行清场工作。

  1. try:
  2. execution block ##正常执行模块
  3. except A:
  4. exc A block ##发生A错误时执行
  5. except B:
  6. exc B block ##发生B错误时执行
  7. except:
  8. other block ##发生除了A,B错误以外的其他错误时执行
  9. else:
  10. if no exception, jump to here ##没有错误时执行
  11. finally:
  12. final block ##总是执行

tips: 注意顺序不能乱,否则会有语法错误。如果用 else 就必须有 except,否则会有语法错误。

  1. try:
  2. a = 1 / 2
  3. print(a)
  4. print(m) # 抛出 NameError异常, 此后的语句都不在执行
  5. b = 1 / 0
  6. print(b)
  7. c = 2 / 1
  8. print(c)
  9. except NameError:
  10. print("Ops!!") # 捕获到异常
  11. except ZeroDivisionError:
  12. print("Wrong math!!")
  13. except:
  14. print("Error")
  15. else:
  16. print("No error! yeah!")
  17. finally: # 是否异常都执行该代码块
  18. print("Successfully!")

1. with 语句的原理

  • 上下文管理协议(Context Management Protocol):包含方法 __enter__()__exit__(),支持该协议的对象要实现这两个方法。
  • 上下文管理器(Context Manager):支持上下文管理协议的对象,这种对象实现了 __enter__() 和 __exit__() 方法。上下文管理器定义执行 with 语句时要建立的运行时上下文,负责执行 with 语句块上下文中的进入与退出操作。通常使用 with 语句调用上下文管理器,也可以通过直接调用其方法来使用。

说完上面两个概念,我们再从 with 语句的常用表达式入手,一段基本的 with 表达式,其结构是这样的:

  1. with context_expression [as target(s)]:
  2. ...
  3. with-body
  4. ...

其中 context_expression 可以是任意表达式;as target(s) 是可选的。

with 语句执行过程 。在语义上等价于:

  1. context_manager = context_expression
  2. exit = type(context_manager).__exit__
  3. value = type(context_manager).__enter__(context_manager)
  4. exc = True # True 表示正常执行,即便有异常也忽略;False 表示重新抛出异常,需要对异常进行处理
  5. try:
  6. try:
  7. target = value # 如果使用了 as 子句
  8. with-body # 执行 with-body
  9. except:
  10. # 执行过程中有异常发生
  11. exc = False
  12. # 如果 __exit__ 返回 True,则异常被忽略;如果返回 False,则重新抛出异常
  13. # 由外层代码对异常进行处理
  14. if not exit(context_manager, *sys.exc_info()):
  15. raise
  16. finally:
  17. # 正常退出,或者通过 statement-body 中的 break/continue/return 语句退出
  18. # 或者忽略异常退出
  19. if exc:
  20. exit(context_manager, None, None, None)
  21. # 缺省返回 None,None 在布尔上下文中看做是 False

可以看到,with 和 try finally 有下面的等价流程:

  1. try:
  2. 执行 __enter__的内容
  3. 执行 with_block.
  4. finally:
  5. 执行 __exit__内容
  1. 执行 context_expression ,生成上下文管理器 context_manager
  2. 调用上下文管理器的 __enter__() 方法;如果使用了 as 子句,则将 __enter__() 方法的 返回值 赋值给 as 子句中的 target(s)
  3. 执行语句体 with-body
  4. 不管执行过程中是否发生了异常,执行上下文管理器的 __exit__() 方法,__exit__() 方法负责执行 "清理" 工作,如释放资源等。如果执行过程中没有出现异常,或者语句体中执行了语句 break/continue/return,则以 None 作为参数调用 __exit__(None, None, None) ;如果执行过程中出现异常,则使用 sys.excinfo 得到的异常信息为参数调用  __exit__(exc_type, exc_value, exc_traceback)
  5. 出现异常时,如果 __exit__(type, value, traceback) 返回 False,则会重新抛出异常,让 with 之外的语句逻辑来处理异常,这也是通用做法;如果返回 True,则忽略异常,不再对异常进行处理

那么__enter____exit__是怎么用的方法呢?我们直接来看一个栗子好了。

程序无错的例子:

  1. class Sample(object): # object类是所有类最终都会继承的类
  2. def __enter__(self): # 类中函数第一个参数始终是self,表示创建的实例本身
  3. print("In __enter__()")
  4. return "Foo"
  5. def __exit__(self, type, value, trace):
  6. print("In __exit__()")
  7. def get_sample():
  8. return Sample()
  9. with get_sample() as sample:
  10. print("sample:", sample)
  11. print(Sample) # 这个表示类本身 <class '__main__.Sample'>
  12. print(Sample()) # 这表示创建了一个匿名实例对象 <__main__.Sample object at 0x00000259369CF550>
  13. '''
  14. In __enter__()
  15. sample: Foo
  16. In __exit__()
  17. <class '__main__.Sample'>
  18. <__main__.Sample object at 0x00000226EC5AF550>
  19. '''

步骤分析:
–> 调用get_sample()函数,返回Sample类的实例;
–> 执行Sample类中的__enter__()方法,打印"In__enter_()"字符串,并将字符串“Foo”赋值给as后面的sample变量;
–> 执行with-block码块,即打印"sample: %s"字符串,结果为"sample: Foo"
–> 执行with-block码块结束,返回Sample类,执行类方法__exit__()。因为在执行with-block码块时并没有错误返回,所以type,value,trace这三个arguments都没有值。直接打印"In__exit__()"

程序有错的例子:

  1. class Sample:
  2. def __enter__(self):
  3. return self
  4. def __exit__(self, type, value, trace):
  5. print("type:", type)
  6. print("value:", value)
  7. print("trace:", trace)
  8. def do_something(self):
  9. bar = 1 / 0
  10. return bar + 10
  11. with Sample() as sample:
  12. sample.do_something()
  13. '''
  14. type: <class 'ZeroDivisionError'>
  15. value: division by zero
  16. trace: <traceback object at 0x0000019B73153848>
  17. Traceback (most recent call last):
  18. File "F:/机器学习/生物信息学/Code/first/hir.py", line 16, in <module>
  19. sample.do_something()
  20. File "F:/机器学习/生物信息学/Code/first/hir.py", line 11, in do_something
  21. bar = 1 / 0
  22. ZeroDivisionError: division by zero
  23. '''

步骤分析:
–> 实例化Sample类,执行类方法__enter__(),返回值self也就是实例自己赋值给sample。即sampleSample的一个实例(对象);
–>执行with-block码块: 实例sample调用方法do_something();
–>执行do_something()第一行 bar = 1 / 0,发现ZeroDivisionError,直接结束with-block代码块运行
–>执行类方法__exit__(),带入ZeroDivisionError的错误信息值,也就是type,valuetrace,并打印它们。

如果有多个项目,则会视作存在多个 with 语句嵌套来处理多个上下文管理器: ( https://docs.python.org/zh-cn/3/reference/compound_stmts.html#the-with-statement )

  1. with A() as a, B() as b:
  2. SUITE
  3. 在语义上等价于:
  4. with A() as a:
  5. with B() as b:
  6. SUITE

在 3.1 版更改: 支持多个上下文表达式。

参见:PEP 343 - "with" 语句。Python with 语句的规范描述、背景和示例。

2. 自定义上下文管理器

开发人员可以自定义支持上下文管理协议的类。自定义的上下文管理器要实现上下文管理协议所需要的 enter() 和 exit() 两个方法:

  • contextmanager.__enter__() :进入上下文管理器的运行时上下文,在语句体执行前调用。with 语句将该方法的返回值赋值给 as 子句中的 target,如果指定了 as 子句的话
  • contextmanager.__exit__(exc_type, exc_value, exc_traceback) :退出与上下文管理器相关的运行时上下文,返回一个布尔值表示是否对发生的异常进行处理。参数表示引起退出操作的异常,如果退出时没有发生异常,则3个参数都为None。如果发生异常时,返回True 表示不处理异常,否则会在退出该方法后重新抛出异常以由 with 语句之外的代码逻辑进行处理。如果该方法内部产生异常,则会取代由 statement-body 中语句产生的异常。要处理异常时,不要显示重新抛出异常,即不能重新抛出通过参数传递进来的异常,只需要将返回值设置为 False 就可以了。之后,上下文管理代码会检测是否 __exit__() 失败来处理异常

下面通过一个简单的示例来演示如何构建自定义的上下文管理器。

注意,上下文管理器必须同时提供 __enter__() 和 __exit__() 方法的定义,缺少任何一个都会导致 AttributeError;with 语句会先检查是否提供了 __exit__() 方法,然后检查是否定义了 __enter__() 方法。

  1. # coding = utf-8
  2. class DBManager(object):
  3. def __init__(self):
  4. pass
  5. def __enter__(self):
  6. print('__enter__')
  7. return self
  8. def __exit__(self, exc_type, exc_val, exc_tb):
  9. print('__exit__')
  10. return True
  11. def getInstance():
  12. return DBManager()
  13. with getInstance() as dbManagerIns:
  14. print('with demo')
  15. '''
  16. 运行结果:
  17. __enter__
  18. with demo
  19. __exit__
  20. '''

with 后面必须跟一个上下文管理器,如果使用了 as,则是把上下文管理器的 __enter__() 方法的返回值赋值给 target,target 可以是单个变量,或者由 "()" 括起来的元组(不能是仅仅由 "," 分隔的变量列表,必须加 "()")

结果分析:当我们使用 with 的时候,__enter__方法被调用,并且将返回值赋值给 as 后面的变量,并且在退出 with 的时候自动执行 __exit__ 方法

  1. class With_work(object):
  2. def __enter__(self):
  3. """进入with语句的时候被调用"""
  4. print('enter called')
  5. return "xxt"
  6. def __exit__(self, exc_type, exc_val, exc_tb):
  7. """离开with的时候被with调用"""
  8. print('exit called')
  9. with With_work() as as_f:
  10. print(f'as_f : {as_f}')
  11. print('hello with')
  12. '''
  13. enter called
  14. as_f : xxt
  15. hello with
  16. exit called
  17. '''

示例 2:

自定义支持 with 语句的对象 

  1. class DummyResource:
  2. def __init__(self, tag):
  3. self.tag = tag
  4. print(f'Resource [{tag}]')
  5. def __enter__(self):
  6. print(f'[Enter {self.tag}]: Allocate resource.')
  7. return self # 可以返回不同的对象
  8. def __exit__(self, exc_type, exc_value, exc_tb):
  9. """
  10. :param exc_type: 错误的类型
  11. :param exc_value: 错误类型对应的值
  12. :param exc_tb: 代码中错误发生的位置
  13. :return:
  14. """
  15. print(f'[Exit {self.tag}]: Free resource.')
  16. if exc_tb is None:
  17. print(f'[Exit {self.tag}]: Exited without exception.')
  18. else:
  19. print(f'[Exit {self.tag}]: Exited with exception raised.')
  20. return False # 可以省略,缺省的None也是被看做是False
  21. # 第一个 with 语句
  22. num = 50
  23. print('*' * num)
  24. with DummyResource('First'):
  25. print('[with-body] Run without exceptions.')
  26. print('*' * num)
  27. # 第二个 with 语句
  28. print('*' * num)
  29. with DummyResource('second'):
  30. print('[with-body] Run with exception.')
  31. raise Exception
  32. print('[with-body] Run with exception. Failed to finish statement-body!')
  33. print('*' * num)
  34. # 嵌套 with 语句
  35. print('*' * num)
  36. with DummyResource('Normal'):
  37. print('[with-body] Run without exceptions.')
  38. with DummyResource('With-Exception'):
  39. print('[with-body] Run with exception.')
  40. raise Exception
  41. print('[with-body] Run with exception. Failed to finish statement-body!')
  42. print('*' * num)

DummyResource 中的 __enter__() 返回的是自身的引用,这个引用可以赋值给 as 子句中的 target 变量;返回值的类型可以根据实际需要设置为不同的类型,不必是上下文管理器对象本身。

__exit__() 方法中对变量 exctb 进行检测,如果不为 None,表示发生了异常,返回 False 表示需要由外部代码逻辑对异常进行处理;注意到如果没有发生异常,缺省的返回值为 None,在布尔环境中也是被看做 False,但是由于没有异常发生,__exit__() 的三个参数都为 None,上下文管理代码可以检测这种情况,做正常处理。

执行结果:

  1. **************************************************
  2. Resource [First]
  3. [Enter First]: Allocate resource.
  4. [with-body] Run without exceptions.
  5. [Exit First]: Free resource.
  6. [Exit First]: Exited without exception.
  7. **************************************************
  8. **************************************************
  9. Resource [second]
  10. [Enter second]: Allocate resource.
  11. [with-body] Run with exception.
  12. [Exit second]: Free resource.
  13. [Exit second]: Exited with exception raised.
  14. Traceback (most recent call last):
  15. File "temp.py", line 30, in <module>
  16. raise Exception
  17. Exception

第1个 with  语句执行结果:可以看到,正常执行时会先执行完语句体 with-body,然后执行 __exit__() 方法释放资源。

第2个 with 语句的执行结果:可以看到,with-body 中发生异常时with-body 并没有执行完,但资源会保证被释放掉,同时产生的异常由 with 语句之外的代码逻辑来捕获处理。

因为第2个with语句发生异常,所以 嵌套 with 语句没有执行。。。

3. 自动关闭文件

我们都知道打开文件有两种方法:

  • f = open()
  • with open() as f:

这两种方法的区别就是第一种方法需要我们自己关闭文件;f.close(),而第二种方法不需要我们自己关闭文件,无论是否出现异常,with都会自动帮助我们关闭文件,这是为什么呢?

我们先自定义一个类,用with来打开它:

  1. class Foo(object):
  2. def __enter__(self):
  3. print("enter called")
  4. def __exit__(self, exc_type, exc_val, exc_tb):
  5. print("exit called")
  6. print("exc_type :%s" % exc_type)
  7. print("exc_val :%s" % exc_val)
  8. print("exc_tb :%s" % exc_tb)
  9. with Foo() as foo:
  10. print("hello python")
  11. a = 1 / 0
  12. print("hello end")
  13. '''
  14. enter called
  15. Traceback (most recent call last):
  16. hello python
  17. exit called
  18. exc_type :<class 'ZeroDivisionError'>
  19. exc_val :division by zero
  20. File "F:/workspaces/python_workspaces/flask_study/with.py", line 25, in <module>
  21. a = 1/0
  22. exc_tb :<traceback object at 0x0000023C4EDBB9C8>
  23. ZeroDivisionError: division by zero
  24. Process finished with exit code 1
  25. '''

执行结果的输入顺序,分析如下:

当我们 with Foo() as foo: 时,此时会执行 __enter__方法,然后进入执行体,也就是:

  1. print("hello python")
  2. a = 1/0
  3. print("hello end")

语句,但是在 a=1/0 出现了异常,with将会中止,此时就执行__exit__方法,就算不出现异常,当执行体被执行完毕之后,__exit__方法仍然被执行一次。

我们回到 with open("file")as f: 不用关闭文件的原因就是在 __exit__ 方法中,存在关闭文件的操作,所以不用我们手工关闭文件,with已将为我们做好了这个操作,这就可以理解了。

4. contextlib 模块

contextlib --- 为 with语句上下文提供的工具:https://docs.python.org/zh-cn/3/library/contextlib.html#contextlib.asynccontextmanager

contextlib 模块提供了3个对象,使用这些对象,可以对已有的生成器函数或者对象进行包装,加入对上下文管理协议的支持,避免了专门编写上下文管理器来支持 with 语句。

  • 装饰器 contextmanager
  • 函数 nested 
  • 上下文管理器 closing

装饰器 contextmanager

contextmanager 用于对生成器函数进行装饰,生成器函数被装饰以后,返回的是一个上下文管理器,其 enter() 和 exit() 方法由 contextmanager 负责提供,而不再是之前的迭代子。被装饰的生成器函数只能产生一个值,否则会导致异常 RuntimeError;产生的值会赋值给 as 子句中的 target,如果使用了 as 子句的话。下面看一个简单的例子。

  1. from contextlib import contextmanager
  2. @contextmanager
  3. def demo():
  4. print('[Allocate resources]')
  5. print('Code before yield-statement executes in __enter__')
  6. yield '*** contextmanager demo ***'
  7. print('Code after yield-statement executes in __exit__')
  8. print('[Free resources]')
  9. with demo() as value:
  10. print(f'Assigned Value: {value}')
  11. '''
  12. [Allocate resources]
  13. Code before yield-statement executes in __enter__
  14. Assigned Value: *** contextmanager demo ***
  15. Code after yield-statement executes in __exit__
  16. [Free resources]
  17. '''

可以看到,生成器函数中 yield 之前的语句在 enter() 方法中执行,yield 之后的语句在 exit() 中执行,而 yield 产生的值赋给了 as 子句中的 value 变量。

需要注意的是,contextmanager 只是省略了 enter() / exit() 的编写,但并不负责实现资源的”获取”和”清理”工作;”获取”操作需要定义在 yield 语句之前,”清理”操作需要定义 yield 语句之后,这样 with 语句在执行 enter() / exit() 方法时会执行这些语句以获取/释放资源,即生成器函数中需要实现必要的逻辑控制,包括资源访问出现错误时抛出适当的异常。

函数 nested

nested 可以将多个上下文管理器组织在一起,避免使用嵌套 with 语句。

  1. nested 语法
  2. with nested(A(), B(), C()) as (X, Y, Z):
  3.     # with-body code here
  4. 类似于:
  5. with A() as X:
  6.     with B() as Y:
  7.         with C() as Z:
  8.             # with-body code here
  9. 需要注意的是,发生异常后,如果某个上下文管理器的 exit() 方法对异常处理返回 False
  10. 则更外层的上下文管理器不会监测到异常。

上下文管理器 closing

closing 的实现如下:

  1. class closing(object):
  2. # help doc here
  3. def __init__(self, thing):
  4. self.thing = thing
  5. def __enter__(self):
  6. return self.thing
  7. def __exit__(self, *exc_info):
  8. self.thing.close()

上下文管理器会将包装的对象赋值给 as 子句的 target 变量,同时保证打开的对象在 with-body 执行完后会关闭掉。closing 上下文管理器包装起来的对象必须提供 close() 方法的定义,否则执行时会报 AttributeError 错误。

自定义支持 closing 的对象

  1. from contextlib import closing
  2. class ClosingDemo(object):
  3. def __init__(self):
  4. self.acquire()
  5. def acquire(self):
  6. print('Acquire resources.')
  7. def free(self):
  8. print('Clean up any resources acquired.')
  9. def close(self):
  10. self.free()
  11. with closing(ClosingDemo()):
  12. print('Using resources')
  13. '''
  14. Acquire resources.
  15. Using resources
  16. Clean up any resources acquired.
  17. '''

closing 适用于提供了 close() 实现的对象,比如网络连接、数据库连接等,也可以在自定义类时通过接口 close() 来执行所需要的资源”清理”工作。

5. 总结

with 是对 try…expect…finally 语法的一种简化,并且提供了对于异常非常好的处理方式。在Python有2种方式来实现 with 语法:class-based 和 decorator-based,2种方式在原理上是等价的,可以根据具体场景自己选择。

with 最初起源于一种block…as…的语法,但是这种语法被很多人所唾弃,最后诞生了with,关于这段历史依然可以去参考PEP-343和PEP-340

with 主要用在:自定义上下文管理器来对软件系统中的资源进行管理,比如数据库连接、共享资源的访问控制等。文件操作。进程线程之间互斥对象。支持上下文其他对象

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/183132
推荐阅读
相关标签
  

闽ICP备14008679号