---
title: '《Python有什么好学的》之上下文管理器'
pubDate: 2018-11-19
tags:
  - 'python'
description: '《Python有什么好学的》之上下文管理器'
---

“Python有什么好学的”这句话可不是反问句，而是问句哦。

主要是煎鱼觉得太多的人觉得Python的语法较为简单，写出来的代码只要符合逻辑，不需要太多的学习即可，即可从一门其他语言跳来用Python写（当然这样是好事，谁都希望入门简单）。

于是我便记录一下，如果要学Python的话，到底有什么好学的。记录一下Python有什么值得学的，对比其他语言有什么特别的地方，有什么样的代码写出来更Pythonic。一路回味，一路学习。

## 引上下文管理器

太极生两仪，两仪为阴阳。

道有阴阳，月有阴晴，人有生死，门有开关。

你看这个门，它能开能关，就像这个对象，它能创建能释放。（扯远了

编程这行，几十年来都绕不开内存泄露这个问题。内存泄露的根本原因，就是把某个对象创建了，但是却没有去释放它。直到程序结束前那一刻，这个未被释放的对象还一直占着内存，即使程序已经不用这个对象了。泄露的量少的话还好，量大的话就直接打满内存，然后程序就被kill了。

聪明的程序员经过了这十几年的努力，创造出很多高级编程语言，这些编程语言已经不再需要让程序员过度关注内存的问题了。但是在编程时，一些常见的对象释放、流关闭还是要程序员显式地写出来。

最常见的就是文件操作了。

## 常见的文件操作方式

原始的Python文件操作方式，很简单，也很common（也很java）：

```python
def read_file_1():
    f = open('file_demo.py', 'r')
    try:
        print(f.read())
    except Exception as e:
        pass
    f.close()
```

就是这么简简单单的，先open然后读写再close，中间读写加个异常处理。

其中close就是释放资源了，在这里如果不close，可能：

1. 资源不释放，直到不可控的垃圾回收来了，甚至直到程序结束
2. 中间对文件修改时，修改的信息还没来得及写入文件
3. 整个代码显得不规范

因此写上close函数理论上已经必须的了，可是`xxx.close()`这样写上去，在逻辑复杂的时候让人容易遗漏，同时也显得不雅观。

这时，各种语言生态有各种解决方案。

像Java，就直接jvm+依赖注入，直接把对象的生命周期管理接管了，只留下对象的使用功能给程序员；像golang，defer一下就好。而python最常用的则是with，即上下文管理器

## 使用上下文管理器

用with之后的文件读写会变成：

```python
def read_file_2():
    with open('file_demo.py', 'r') as f:
        print(f.read())
```

我们看到用了with之后，代码没有了open创建，也没有了close释放。而且也没有了异常处理，这样子我们一看到代码，难免会怀疑它的健壮性。

为了更好地理解上下文管理器，我们先实现试试。

## 实现上下文管理器

我们先感性地对with进行猜测。

从调用with的形式上看，with像是一个函数，包裹住了open和close：

```python
# 大概意思而已 with = open + do + close
def with():
    open(xxxx)
    doSomething(xxxx)
    close(xxxx)
```

而Python的库中已有的方案（contextmanager）也和上面的伪代码具有一定的相似性：

```python
from contextlib import contextmanager

@contextmanager
def c(s):
    print(s + 'start')
    yield s
    print(s + 'end')
```

“打印start”相当于open，而“打印end”相当于close，yield语法和修饰器（@）不熟悉的同学可以复习一下这些文章：[生成器](https://www.mkshell.com/python-learning-generator-iterator/)和[修饰器](https://www.mkshell.com/python-learning-decorator/)。

然后我们调用这个上下文管理器试试，注意煎鱼还给上下文管理器加了参数s，输出的时候会带上：

```python
def test_context():
    with c('123') as cc:
        print('in with')
        print(type(cc))

if __name__ == '__main__':
    test_context()
```

![](https://source.mkshell.com/15425620119555.jpg)

我们看到，start和end前都有实参s=123。

现实一个上下文管理器就是这么简单。

### 异常处理

但是我们必须要注重异常处理，假如上面的上下文管理器中抛异常了怎么办呢：

```python
def test_context():
    with c('123') as cc:
        print('in with')
        print(type(cc))
        raise Exception
```

结果： ![](https://source.mkshell.com/15425622234213.jpg)

显然，这样弱鸡的异常处理，煎鱼时忍不了的。而且最重要的是，后面的close释放居然没有执行！

我们可以在实现上下管理器时，接入异常处理：

```python
@contextmanager
def c():
    print('start')
    try:
        yield
    finally:
        print('end')

def test_except():
    try:
        with c() as cc:
            print('in with')
            raise Exception

    except:
        print('catch except')
```

调用test_except函数输出： ![](https://source.mkshell.com/15425626029832.jpg)

我们在上下文管理器的实现中加入了try-finally，保证出现异常的时候，上下文管理器也能执行close。同时在调用with前也加入try结构，保证整个函数的正常运行。

然而，加入了这些东西之后，整个函数变得复杂又难看。

因此，煎鱼觉得，想要代码好看，抽象的逻辑需要再次升华，即从函数的层面升为对象（类）的层面。

## 实现上下文管理器类

其实用类实现上下文管理器，从逻辑理解上简单了很多，而且不需要引入那一个库：

```python
class ContextClass(object):
    def __init__(self, s):
        self.s = s

    def __enter__(self):
        print(self.s + 'call enter')
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print(self.s + 'call exit')

    def test(self):
        print(self.s + 'call test')
```

从代码的字面意思上，我们就能感受得出来，`__enter__`即为我们理解的open函数，`__exit__`就是close函数。

接下来，我们调用一下这个上下文管理器：

```python
def test_context():
    with ContextClass('123') as c:
        print('in with')
        c.test()
        print(type(c))
        print(isinstance(c, ContextClass))

    print('')
    c = ContextClass('123')
    print(type(c))
    print(isinstance(c, ContextClass))

if __name__ == '__main__':
    test_context()
```

输出结果：

![](https://source.mkshell.com/15425630649843.jpg)

功能上和直接用修饰器一致，只是在实现的过程中，逻辑更清晰了。

### 异常处理

回到我们原来的话题：异常处理。

直接用修饰器实现的上下文管理器处理异常时可以说是很难看了，那么我们的类选手表现又如何呢？

为了方便比较，煎鱼把未进行异常处理的和已进行异常处理的一起写出来，然后煎鱼调用一个不存在的方法来抛异常：

```python
class ContextClass(object):
    def __init__(self, s):
        self.s = s

    def __enter__(self):
        print(self.s + 'call enter')
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print(self.s + 'call exit')

class ContextExceptionClass(object):
    def __init__(self, s):
        self.s = s

    def __enter__(self):
        print(self.s + 'call enter')
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print(self.s + 'call exit')
        return True

def test_context():
    with ContextExceptionClass('123') as c:
        print('in with')
        t = c.test()
        print(type(t))

    # with ContextClass('456') as c:
        # print('in with')
        # t = c.test()
        # print(type(t))

if __name__ == '__main__':
    test_context()
```

输出不一样的结果： ![](https://source.mkshell.com/15425635741507.jpg)

![](https://source.mkshell.com/15425635915235.jpg)

结果发现，看了半天，两个类只有最后一句不一样：异常处理的类中`__exit__`函数多一句返回，而且还是return了True。

而且这两个类都完成了open和close两部，即使后者抛异常了。

而在`__exit__`中加`return True`的意思就是不把异常抛出。

如果想要详细地处理异常，而不是像上面治标不治本的隐藏异常，则需要在`__exit__`函数中处理异常即可，因为该函数中有着异常的信息。

不信？稍微再改改：

```python
class ContextExceptionClass(object):
    def __init__(self, s):
        self.s = s

    def __enter__(self):
        print(self.s + 'call enter')
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print(self.s + 'call exit')
        print(str(exc_type) + ' ' + str(exc_val) + ' ' + str(exc_tb))
        return True
```

输出与预期异常信息一致：

![](https://source.mkshell.com/15425639308612.jpg)

_先这样吧_

若有错误之处请指出，更多地请关注[造壳](https://www.mkshell.com)。
