Python中的“with”语句是用来干什么的?

512

我今天第一次了解到 Python 的 with 语句。虽然我已经轻松使用 Python 几个月了,但甚至都不知道它的存在!鉴于其有点模糊的地位,我认为值得问一下:

  1. with 语句设计用于什么?
  2. 你用它来做什么?
  3. 有任何需要注意的问题或常见反模式与其使用相关的问题吗?在哪些情况下最好使用 try..finally 而不是 with
  4. 为什么它没有被更广泛地使用?
  5. 哪些标准库类与其兼容?

19
仅供参考,这是 Python 3 文档中关于 with 的部分。 - Alexey
1
作为一个Java背景的人,我会把它记作Java中相应的“try with resources”,即使这可能并不完全正确。 - vefthym
1
只是为了记录,这里是 PEP-0343:https://www.python.org/dev/peps/pep-0343/。 - Priyanka
11个回答

450
  1. 我相信其他用户之前已经回答过这个问题,因此我只是为了完整性而添加这个回答:使用with语句可以通过封装通用的准备和清理任务在所谓的上下文管理器中简化异常处理。更多细节可以在PEP 343中找到。例如,open语句本身就是一个上下文管理器,它允许您打开一个文件,在使用with语句的上下文中保持该文件处于打开状态,并在您离开上下文时立即关闭它,无论您是由于异常还是在正常控制流期间离开的。因此,with语句可以以类似于C++中的RAII模式的方式使用:一些资源被with语句获取并在您离开with上下文时被释放。

  2. 一些例子包括使用with open(filename) as fp:来打开文件,使用with lock:(其中lockthreading.Lock的实例)来获取锁。您还可以使用contextlib中的contextmanager装饰器构建自己的上下文管理器。例如,当我需要临时更改当前目录并返回原来的目录时,我经常使用它:

    from contextlib import contextmanager
    import os
    
    @contextmanager
    def working_directory(path):
        current_dir = os.getcwd()
        os.chdir(path)
        try:
            yield
        finally:
            os.chdir(current_dir)
    
    with working_directory("data/stuff"):
        # do something within data/stuff
    # here I am back again in the original working directory
    

    下面是另一个示例,它临时重定向了sys.stdinsys.stdoutsys.stderr到其他文件句柄,稍后将它们恢复:

    from contextlib import contextmanager
    import sys
    
    @contextmanager
    def redirected(**kwds):
        stream_names = ["stdin", "stdout", "stderr"]
        old_streams = {}
        try:
            for sname in stream_names:
                stream = kwds.get(sname, None)
                if stream is not None and stream != getattr(sys, sname):
                    old_streams[sname] = getattr(sys, sname)
                    setattr(sys, sname, stream)
            yield
        finally:
            for sname, stream in old_streams.iteritems():
                setattr(sys, sname, stream)
    
    with redirected(stdout=open("/tmp/log.txt", "w")):
         # these print statements will go to /tmp/log.txt
         print "Test entry 1"
         print "Test entry 2"
    # back to the normal stdout
    print "Back to normal stdout again"
    

    最后,又有一个例子创建了一个临时文件夹,并在离开上下文时清理它:

    from tempfile import mkdtemp
    from shutil import rmtree
    
    @contextmanager
    def temporary_dir(*args, **kwds):
        name = mkdtemp(*args, **kwds)
        try:
            yield name
        finally:
            shutil.rmtree(name)
    
    with temporary_dir() as dirname:
        # do whatever you want
    

32
感谢您加入对RAII的比较说明。作为一名C++程序员,这告诉了我所需要知道的一切。 - Fred Thomsen
好的,让我澄清一下。你是说with语句的设计是将变量填充数据,直到其下方的指令完成,然后释放该变量? - Musixauce3000
因为我使用它来打开一个py脚本。with open('myScript.py', 'r') as f: pass。我期望能够调用变量f来查看文档的文本内容,因为如果文档通过常规的open语句分配给f,那么这就是所显示的内容:f = open('myScript.py').read()。但是我得到了以下结果:<_io.TextIOWrapper name='myScript.py' mode='r' encoding='cp1252'>。这是什么意思? - Musixauce3000
5
使用with并不能代替操作文件的必要性。with只是调用open函数,它不知道你需要对文件进行何种操作——例如,你可能需要执行搜索操作。 - Tony Suffolk 66
@Musixauce3000 with语句可以将变量填充数据或对环境进行其他更改,直到其下方的指令完成,然后执行任何所需的清理。可以进行的清理类型包括关闭打开的文件,或者像@Tamas在此示例中那样将目录更改回之前的位置等。由于Python具有垃圾收集功能,释放变量不是重要的用例。with通常用于其他类型的清理。 - Bob Steinke

102

我建议两个有趣的讲座:

  • PEP 343The "with"语句
  • Effbot深入了解Python的"with"语句

1. with语句用于使用上下文管理器定义的方法包装块的执行。这允许常见的try...except...finally使用模式被封装起来,以便方便地重复使用。

2. 你可以像这样做:

with open("foo.txt") as foo_file:
    data = foo_file.read()

from contextlib import nested
with nested(A(), B(), C()) as (X, Y, Z):
   do_something()

或者 (Python 3.1)

with open('data') as input_file, open('result', 'w') as output_file:
   for line in input_file:
     output_file.write(parse(line))

或者

lock = threading.Lock()
with lock:
    # Critical section of code

3. 我没有看到任何反模式。
引用Python深入浅出的话:

try..finally是好的,但with更好。

4. 我猜这可能与程序员习惯于从其他语言使用try..catch..finally语句有关。


4
当你处理线程同步对象时,它真正发挥作用。在Python中相对较少见,但当你需要它们时,你确实需要使用with - detly
1
diveintopython.org已经无法访问(永久性的?)。可以在http://www.diveintopython.net/上找到镜像。 - snuggles
打开文件是一个很好的例子,展示了打开、输入、关闭文件操作的幕后情况,并且使用自定义引用名称清晰地隐藏了这些操作。 - Angry 84
在处理文件对象时,使用with关键字的另一个小例子可以在文档中找到:https://docs.python.org/3/tutorial/inputoutput.html#reading-and-writing-files - Priyanka

46

Python中的with语句是内置的语言支持,用于实现C++中常用的资源获取即初始化习惯用法。它旨在允许安全地获取和释放操作系统资源。

with语句在一个作用域/块中创建资源。您可以在该块中使用这些资源编写代码。当块退出时,无论块中的代码正常退出还是因为异常而退出,都会干净地释放资源。

Python库中有许多遵循with语句所需协议的资源,因此可以直接与之一起使用。但是,任何人都可以通过实现完全记录的协议PEP 0343来制作可以在with语句中使用的资源。

在应用程序中明确释放必须释放的资源(例如文件、网络连接、锁等)时,请始终使用它。


29

为了完整起见,我将添加我最有用的使用情况来说明with语句。

我经常进行科学计算,对于某些活动,我需要使用Decimal库进行任意精度计算。我的代码中有一部分需要高精度,而大多数其他部分则需要较低精度。

我将默认精度设置为较低的数字,然后使用with为某些部分获取更高精度的答案:

from decimal import localcontext

with localcontext() as ctx:
    ctx.prec = 42   # Perform a high precision calculation
    s = calculate_something()
s = +s  # Round the final result back to the default precision

我经常在超几何测试中使用这个方法,需要计算阶乘产生的大数的除法。当你进行基因组规模的计算时,必须小心四舍五入和溢出错误。


28

一个反面模式的例子可能是在循环内使用with,而将with放在循环外会更有效率。

例如

for row in lines:
    with open("outfile","a") as f:
        f.write(row)

对比

with open("outfile","a") as f:
    for row in lines:
        f.write(row)

第一种方法是对每个row打开和关闭文件,这可能会导致性能问题,相比之下第二种方法只需打开和关闭文件一次。


11

请查看PEP 343——'with'语句,其中在末尾有一个示例部分。

...引入了新语句“with”到Python语言中,以便将try/finally语句的标准使用因素分离出来。


5

已经比较详细地涵盖了1、2、3点:

4:这是一个比较新的技术,只能在Python2.6+(或者使用from __future__ import with_statement在Python2.5上实现)。


4

3

通常在Python中,“with”语句用于打开文件、处理文件中的数据,而且不需要调用close()方法即可关闭文件。“with”语句通过提供清理活动使异常处理更简单。

“with”的一般形式:

with open(“file name”, “mode”) as file_var:
    processing statements

注意: 在调用 file_var.close() 后不需要再调用 close() 方法来关闭文件。


3
另一个实例展示了开箱即用的支持,对于习惯于内置 open() 行为方式的人来说,这可能有点令人困惑,就是流行的数据库模块中的 connection 对象,例如: connection 对象是上下文管理器,因此可以在 with-statement 中直接使用。但是,在使用上述内容时,请注意以下事项:

with-block 完成时(无论是否有异常),连接不会关闭。如果 with-block 以异常结束,则事务将被回滚;否则,该事务将被提交。

这意味着程序员必须自己负责关闭连接,但允许获取连接,并在多个 with-statements 中使用它,如 psycopg2 文档 所示:
conn = psycopg2.connect(DSN)

with conn:
    with conn.cursor() as curs:
        curs.execute(SQL1)

with conn:
    with conn.cursor() as curs:
        curs.execute(SQL2)

conn.close()

在上面的示例中,您会注意到 psycopg2cursor 对象也是上下文管理器。从相关行为文档中可以看出:

cursor 退出 with-block 时,它会关闭并释放任何与其关联的资源。交易状态不受影响。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接