代码之家  ›  专栏  ›  技术社区  ›  JL Peyret

打开contextmanager,但仅当调用方未提供时

  •  0
  • JL Peyret  · 技术社区  · 6 年前

    这是一个有点做作的例子,我的实际用例是数据库连接(大部分是只读的)——如果没有提供连接,请打开一个。

    我已经将其更改为一个由 with open(xxx) as fo 我的想法是,我有一些函数可能需要做一些工作——如果它们的调用者已经给了它们一个上下文管理的对象(本例中的文件句柄),请使用它。否则创建一个本地上下文管理器。

    从概念上讲,这就是我想要的,但它没有达到预期的效果:

    
    def write_it(name, fo=None):
        if not fo:
           with open(name,"w") as fo:
                #I'd want to keep `fo` but it wont work
                pass
    
        #assume this is a lot of complex code
    
        #if fo was opened here, it will have been closed already
        fo.write(name)
    
    
    name = "works_w_context.txt"
    with open(name, "w") as fo:
        write_it(name, fo)
        fo.write("\n and it still should be open")
    
    #this fails, as expected
    name = "error_wo_context.txt"
    write_it(name)
    
    

    正如所料,当我没有提供打开的文件时,我出现了一个错误。

    Traceback (most recent call last):
      File "test_182_context.py", line 21, in <module>
        write_it(name)
      File "test_182_context.py", line 12, in write_it
        fo.write(data)
    ValueError: I/O operation on closed file.
    
    (venv38) myuser@test_182_context$ dir *.txt
    -rw-r--r--  1 myuser  staff  52 Oct  1 17:26 works_w_context.txt
    -rw-r--r--  1 myuser  staff   0 Oct  1 17:26 error_wo_context.txt
    
    

    我的解决方法——有没有更好的方法 contextlib 单元

    我发现实现这一点的唯一方法是创建一个公共存根函数,在需要时打开文件,然后用文件句柄调用实际的实函数。

    但正如你所看到的,我已经重复了4次函数签名。当然 *args 和 **kwargs 这可能会有所帮助,但也会使代码更难理解。

    def _write_it(name, fo=None):
        #assume this is a lot of complex code
        data = "some very complicated calculations taking many lines"    
        fo.write(data)
    
    
    def write_it(name, fo=None):
        if not fo:
           with open(name,"w") as fo:
                _write_it(name, fo)
        else:
            _write_it(name, fo)
    
    
    
    name = "works2_w_context.txt"
    with open(name, "w") as fo:
        write_it(name, fo)
        fo.write("\n and it still should be open")
    
    #this fails, as expected
    name = "works2_wo_context.txt"
    write_it(name)
    
    

    是的,它是有效的:

    (venv38) myuser@test_182_context$ py test_182_context_2.py
    (venv38) myuser@test_182_context$ dir *.txt
    -rw-r--r--  1 myuser  staff  52 Oct  1 17:29 works2_w_context.txt
    -rw-r--r--  1 myuser  staff  52 Oct  1 17:29 works2_wo_context.txt
    

    今天早些时候有人问了一个问题,并提到了一条评论 contextlib.nullcontext

    返回一个上下文管理器,该管理器从 进来 ,但除此之外什么都不做。它打算用作可选上下文管理器的替代品。

    有点 看 它似乎与我所追求的相关,但同时它似乎无法解决嵌套的核心问题。

    0 回复  |  直到 6 年前
        1
  •  2
  •   chepner    6 年前

    可以使用出口堆栈为自己打开的文件有条件地添加新的上下文管理器。

    from contextlib import ExitStack
    
    def write_it(name, fo=None):
        with ExitStack() as es:
            if fo is None:
                fo = es.enter_context(open(name, "w"))
        
            data = ...
            fo.write()
    

    如果出口堆栈为空,则当 with 声明退出。只有新打开的文件才会添加到堆栈中,当 具有 语句完成。


    如果需要,还可以使用空上下文管理器 fo 不是 None :

    from contextlib import nullcontext
    
    def write_it(name, fo=None):
        if fo is None:
            # With no file received, open a new one and use it
            # as the context manager
            cm = open(name, "w")
        else:
            # Create a do-nothing context manager that won't close
            # the received file.
            cm = nullcontext(fo)
    
        # Get an open file handle from the defined context manager
        with cm as f:
            data = ...
            f.write(data)
    

    空上下文管理器的 __enter__ 方法返回包装的对象,但其 __exit__ 方法不起任何作用。