Home > Article > Backend Development > Context managers in Python
In Python, the object that calls the __enter__ method before entering the code block and calls the __exit__ method after leaving the code block is used as a context manager. In this article, we will deeply analyze the context manager in Python and take a look at the context. The role and usage of the manager:
1. What is the context manager?
For example, when you write Python code, you often put a series of operations in a statement block:
(1) When a certain condition is true – execute this statement Block
(2) When a certain condition is true - loop to execute this statement block
Sometimes we need to maintain a certain state when the program is running in the statement block, and when leaving the statement block This state ends later.
So, in fact, the task of the context manager is - prepare before the code block is executed, and clean up after the code block is executed.
The context manager is a feature added in Python 2.5, which can make your code more readable and have fewer errors. Next, let's take a look at how to use it.
2. How to use context manager?
Looking at code is the best way to learn. Let's see how we usually open a file and write "Hello World"?
filename = 'my_file.txt' mode = 'w' # Mode that allows to write to the file writer = open(filename, mode) writer.write('Hello ') writer.write('World') writer.close()
In lines 1-2, we specify the file name and how to open it (write).
Line 3 opens the file, lines 4-5 write "Hello world", and line 6 closes the file.
If this is enough, why do we need a context manager? But we overlooked a small but important detail: what happens if we never get to line 6 to close the file?
For example, the disk is full, so an exception will be thrown when we try to write to the file on line 4, and line 6 will not have a chance to execute at all.
Of course, we can use try-finally statement blocks for packaging:
writer = open(filename, mode) try: writer.write('Hello ') writer.write('World') finally: writer.close()
The code in the finally statement block regardless of the try statement block Whatever happens will be executed. Therefore it is guaranteed that the file will be closed. Is there any problem with doing this? Of course not, but when we do something more complex than writing "Hello world", try-finally statements can get ugly. For example, if we want to open two files, one for reading and one for writing, and perform copy operations between the two files, then the with statement can ensure that both can be closed at the same time.
OK, let’s break things down:
(1) First, create a file variable named “writer”.
(2) Then, perform some operations on the writer.
(3) Finally, close the writer.
Isn’t this more elegant?
with open(filename, mode) as writer: writer.write('Hello ') writer.write('World')
Let’s dig a little deeper, “with” is a new keyword and always appears with context managers. "open(filename, mode)" appeared in the previous code. "as" is another keyword that refers to the content returned from the "open" function and assigns it to a new variable. "writer" is a new variable name.
2-3 lines, indent to open a new code block. In this code block, we can do any operation on the writer. This way we use the "open" context manager, which ensures that our code is both elegant and safe. It accomplishes the try-finally task brilliantly.
The open function can be used both as a simple function and as a context manager. This is because the open function returns a file type variable, and this file type implements the write method we used before, but if you want to use it as a context manager, you must also implement some special methods, which I will discuss next. introduced in the section.
3. Custom context manager
Let’s write an “open” context manager.
To implement a context manager, two methods must be implemented - one is responsible for the preparation operation of entering the statement block, and the other is responsible for the aftermath operation of leaving the statement block. At the same time, we need two parameters: file name and opening method.
The Python class contains two special methods, named: __enter__ and __exit__ (double underscore as prefix and suffix).
When an object is used as a context manager:
(1) The __enter__ method will be called before entering the code block.
(2) The __exit__ method is called after leaving the code block (even if an exception is encountered in the code block).
The following is an example of a context manager that prints when entering and leaving a code block.
class PypixContextManagerDemo: def __enter__(self): print 'Entering the block' def __exit__(self, *unused): print 'Exiting the block' with PypixContextManagerDemo(): print 'In the block' #Output: #Entering the block #In the block #Exiting the block
Note something:
(1) No parameters are passed.
(2) The "as" keyword is not used here.
We will discuss the parameter settings of the __exit__ method later.
How do we pass parameters to a class? In fact, in any class, you can use the __init__ method, and here we will rewrite it to receive two necessary parameters (filename, mode).
When we enter the statement block, the open function will be used, just like in the first example. And when we leave the statement block, everything opened in the __enter__ function will be closed.
The following is our code:
class PypixOpen: def __init__(self, filename, mode): self.filename = filename self.mode = mode def __enter__(self): self.openedFile = open(self.filename, self.mode) return self.openedFile def __exit__(self, *unused): self.openedFile.close() with PypixOpen(filename, mode) as writer: writer.write("Hello World from our new Context Manager!")
Let’s see what changes:
(1)3-5 OK, two parameters are received through __init__.
(2) Lines 7-9, open the file and return.
(3)12行,当离开语句块时关闭文件。
(4)14-15行,模仿open使用我们自己的上下文管理器。
除此之外,还有一些需要强调的事情:
4.如何处理异常
我们完全忽视了语句块内部可能出现的问题。
如果语句块内部发生了异常,__exit__方法将被调用,而异常将会被重新抛出(re-raised)。当处理文件写入操作时,大部分时间你肯定不希望隐藏这些异常,所以这是可以的。而对于不希望重新抛出的异常,我们可以让__exit__方法简单的返回True来忽略语句块中发生的所有异常(大部分情况下这都不是明智之举)。
我们可以在异常发生时了解到更多详细的信息,完备的__exit__函数签名应该是这样的:
def __exit__(self, exc_type, exc_val, exc_tb)
这样__exit__函数就能够拿到关于异常的所有信息(异常类型,异常值以及异常追踪信息),这些信息将帮助异常处理操作。在这里我将不会详细讨论异常处理该如何写,以下是一个示例,只负责抛出SyntaxErrors异常。
class RaiseOnlyIfSyntaxError: def __enter__(self): pass def __exit__(self, exc_type, exc_val, exc_tb): return SyntaxError != exc_type
捕获异常:
当一个异常在with块中抛出时,它作为参数传递给__exit__。三个参数被使用,和sys.exc_info()返回的相同:类型、值和回溯(traceback)。当没有异常抛出时,三个参数都是None。上下文管理器可以通过从__exit__返回一个真(True)值来“吞下”异常。例外可以轻易忽略,因为如果__exit__不使用return直接结束,返回None——一个假(False)值,之后在__exit__结束后重新抛出。
捕获异常的能力创造了有意思的可能性。一个来自单元测试的经典例子——我们想确保一些代码抛出正确种类的异常:
class assert_raises(object): # based on pytest and unittest.TestCase def __init__(self, type): self.type = type def __enter__(self): pass def __exit__(self, type, value, traceback): if type is None: raise AssertionError('exception expected') if issubclass(type, self.type): return True # swallow the expected exception raise AssertionError('wrong exception type') with assert_raises(KeyError): {}['foo']
5. 谈一些关于上下文库(contextlib)的内容
contextlib是一个Python模块,作用是提供更易用的上下文管理器。
(1)contextlib.closing
假设我们有一个创建数据库函数,它将返回一个数据库对象,并且在使用完之后关闭相关资源(数据库连接会话等)
我们可以像以往那样处理或是通过上下文管理器:
with contextlib.closing(CreateDatabase()) as database: database.query()
contextlib.closing方法将在语句块结束后调用数据库的关闭方法。
(2)contextlib.nested
另一个很cool的特性能够有效地帮助我们减少嵌套:
假设我们有两个文件,一个读一个写,需要进行拷贝。
以下是不提倡的:
with open('toReadFile', 'r') as reader: with open('toWriteFile', 'w') as writer: writer.writer(reader.read())
可以通过contextlib.nested进行简化:
with contextlib.nested(open('fileToRead.txt', 'r'), open('fileToWrite.txt', 'w')) as (reader, writer): writer.write(reader.read())
在Python2.7中这种写法被一种新语法取代:
with open('fileToRead.txt', 'r') as reader, \ open('fileToWrite.txt', 'w') as writer: writer.write(reader.read()) contextlib.contextmanager
对于Python高级玩家来说,任何能够被yield关键词分割成两部分的函数,都能够通过装饰器装饰的上下文管理器来实现。任何在yield之前的内容都可以看做在代码块执行前的操作,而任何yield之后的操作都可以放在exit函数中。
这里我举一个线程锁的例子:
下面是线程安全写函数的例子:
import threading lock = threading.Lock() def safeWriteToFile(openedFile, content): lock.acquire() openedFile.write(content) lock.release()
接下来,让我们用上下文管理器来实现,回想之前关于yield和contextlib的分析:
@contextlib.contextmanager def loudLock(): print 'Locking' lock.acquire() yield print 'Releasing' lock.release() with loudLock(): print 'Lock is locked: %s' % lock.locked() print 'Doing something that needs locking' #Output: #Locking #Lock is locked: True #Doing something that needs locking #Releasing
特别注意,这不是异常安全(exception safe)的写法。如果你想保证异常安全,请对yield使用try语句。幸运的是threading。lock已经是一个上下文管理器了,所以我们只需要简单地:
@contextlib.contextmanager def loudLock(): print 'Locking' with lock: yield print 'Releasing'
因为threading.lock在异常发生时会通过__exit__函数返回False,这将在yield被调用是被重新抛出。这种情况下锁将被释放,但对于“print ‘Releasing'”的调用则不会被执行,除非我们重写try-finally。
如果你希望在上下文管理器中使用“as”关键字,那么就用yield返回你需要的值,它将通过as关键字赋值给新的变量。下面我们就仔细来讲一下。
6.使用生成器定义上下文管理器
当讨论生成器时,据说我们相比实现为类的迭代器更倾向于生成器,因为它们更短小方便,状态被局部保存而非实例和变量中。另一方面,正如双向通信章节描述的那样,生成器和它的调用者之间的数据流可以是双向的。包括异常,可以直接传递给生成器。我们想将上下文管理器实现为特殊的生成器函数。事实上,生成器协议被设计成支持这个用例。
@contextlib.contextmanager def some_generator(<arguments>): <setup> try: yield <value> finally: <cleanup>
contextlib.contextmanager装饰一个生成器并转换为上下文管理器。生成器必须遵循一些被包装(wrapper)函数强制执行的法则——最重要的是它至少yield一次。yield之前的部分从__enter__执行,上下文管理器中的代码块当生成器停在yield时执行,剩下的在__exit__中执行。如果异常被抛出,解释器通过__exit__的参数将之传递给包装函数,包装函数于是在yield语句处抛出异常。通过使用生成器,上下文管理器变得更短小精炼。
让我们用生成器重写closing的例子:
@contextlib.contextmanager def closing(obj): try: yield obj finally: obj.close()
再把assert_raises改写成生成器:
@contextlib.contextmanager def assert_raises(type): try: yield except type: return except Exception as value: raise AssertionError('wrong exception type') else: raise AssertionError('exception expected')
这里我们用装饰器将生成函数转化为上下文管理器!
更多Python中的上下文管理器相关文章请关注PHP中文网!