• python 上下文管理器


    在写Python代码的时候经常将一系列操作放在一个语句块中:

    当某条件为真 – 执行这个语句块

    当某条件为真 – 循环执行这个语句块

    有时候我们需要在当程序在语句块中运行时保持某种状态,并且在离开语句块后结束这种状态。

    所以,事实上上下文管理器的任务是 – 代码块执行前准备,代码块执行后收拾。

    2. 如何使用上下文管理器?

    看代码是最好的学习方式,来看看我们通常是如何打开一个文件并写入”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() 

    1-2行,我们指明文件名以及打开方式(写入)。

    第3行,打开文件,4-5行写入“Hello world”,第6行关闭文件。

    这样不就行了,为什么还需要上下文管理器?但是我们忽略了一个很小但是很重要的细节:如果我们没有机会到达第6行关闭文件,那会怎样?

    举个例子,磁盘已满,因此我们在第4行尝试写入文件时就会抛出异常,而第6行则根本没有机会执行。

    当然,我们可以使用try-finally语句块来进行包装:

    writer = open(filename, mode)
    try:
        writer.write('Hello ')
        writer.write('World')
    finally:
        writer.close()

    finally语句块中的代码无论try语句块中发生了什么都会执行。因此可以保证文件一定会关闭。这么做有什么问题么?当然没有,但当我们进行一 些比写入“Hello world”更复杂的事情时,try-finally语句就会变得丑陋无比。例如我们要打开两个文件,一个读一个写,两个文件之间进行拷贝操作,那么通过 with语句能够保证两者能够同时被关闭。

    OK,让我们把事情分解一下:

    首先,创建一个名为“writer”的文件变量。

    然后,对writer执行一些操作。

    最后,关闭writer。

    这样是不是优雅多了?

    with open(filename, mode) as writer:
        writer.write('Hello ')
        writer.write('World')

    让我们深入一点,“with”是一个新关键词,并且总是伴随着上下文管理器出现。“open(filename, mode)”曾经在之前的代码中出现。“as”是另一个关键词,它指代了从“open”函数返回的内容,并且把它赋值给了一个新的变量。“writer” 是一个新的变量名。

    2-3行,缩进开启一个新的代码块。在这个代码块中,我们能够对writer做任意操作。这样我们就使用了“open”上下文管理器,它保证我们的代码既优雅又安全。它出色的完成了try-finally的任务。

    open函数既能够当做一个简单的函数使用,又能够作为上下文管理器。这是因为open函数返回了一个文件类型(file type)变量,而这个文件类型实现了我们之前用到的write方法,但是想要作为上下文管理器还必须实现一些特殊的方法,我会在接下来的小节中介绍。

    3. 自定义上下文管理器

    让我们来写一个“open”上下文管理器。

    要实现上下文管理器,必须实现两个方法 – 一个负责进入语句块的准备操作,另一个负责离开语句块的善后操作。同时,我们需要两个参数:文件名和打开方式。

    Python类包含两个特殊的方法,分别名为:__enter__以及__exit__(双下划线作为前缀及后缀)。

    当一个对象被用作上下文管理器时:

    __enter__ 方法将在进入代码块前被调用。

    __exit__ 方法则在离开代码块之后被调用(即使在代码块中遇到了异常)。

    下面是上下文管理器的一个例子,它分别进入和离开代码块时进行打印。

    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

    注意一些东西:

    • 没有传递任何参数。
    • 在此没有使用“as”关键词。
    • 稍后我们将讨论__exit__方法的参数设置。

    我们如何给一个类传递参数?其实在任何类中,都可以使用__init__方法,在此我们将重写它以接收两个必要参数(filename, mode)。

    当我们进入语句块时,将会使用open函数,正如第一个例子中那样。而当我们离开语句块时,将关闭一切在__enter__函数中打开的东西。

    以下是我们的代码:

    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!")

    来看看有哪些变化:

    3-5行,通过__init__接收了两个参数。

    7-9行,打开文件并返回。

    12行,当离开语句块时关闭文件。

    14-15行,模仿open使用我们自己的上下文管理器。

    除此之外,还有一些需要强调的事情:

    如何处理异常

    我们完全忽视了语句块内部可能出现的问题。

    如果语句块内部发生了异常,__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

    4. 谈一些关于上下文库(contextlib)的内容

    contextlib是一个Python模块,作用是提供更易用的上下文管理器。

    contextlib.closing

    假设我们有一个创建数据库函数,它将返回一个数据库对象,并且在使用完之后关闭相关资源(数据库连接会话等)

    我们可以像以往那样处理或是通过上下文管理器:

    with contextlib.closing(CreateDatabase()) as database:
        database.query()

    contextlib.closing方法将在语句块结束后调用数据库的关闭方法。

    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关键字赋值给新的变量。

  • 相关阅读:
    day 66 crm(3) 自创组件stark界面展示数据
    day 65 crm(2) admin源码解析,以及简单的仿造admin组件
    用 Python+nginx+django 打造在线家庭影院
    django -admin 源码解析
    day 64 crm项目(1) admin组件的初识别以及应用
    云链接 接口不允许 情况 解决方法 mysql Host is not allowed to connect to this MySQL server解决方法
    day 56 linux的安装python3 ,虚拟环境,mysql ,redis
    day55 linux 基础以及系统优化
    Codeforces 989 P循环节01构造 ABCD连通块构造 思维对云遮月参考系坐标轴转换
    Codeforces 990 调和级数路灯贪心暴力 DFS生成树两子树差调水 GCD树连通块暴力
  • 原文地址:https://www.cnblogs.com/crandy/p/5075859.html
Copyright © 2020-2023  润新知