在 Python 中使用 context class 和 contextmananger 方法装饰器时的不同行为

Different behaviour when using context class and contextmananger method decorator in Python

在尝试使用 Python (3.5) 的上下文管理器时,我在以下两种情况下得到不同的行为。我正在尝试通过结合上下文管理器使用适当的关闭过程来优雅地处理我的线程程序的 KeyboardInterrupt 异常,但在第二种情况下我似乎无法让它工作而且我可以'不明白为什么。

这两种情况的共同点是使用线程的通用 "job" 任务:

import threading
class Job(threading.Thread):
    def run(self):
        self.active = True

        while self.active:
            continue

    def stop(self):
        self.active = False

一旦使用 start 启动(threading.Thread 父 class 提供的方法,内部调用 run),可以通过调用 [=23] 停止=].

我尝试这样做的第一种方法是使用内置的 __enter____exit__ 方法,以便利用 Python 的 with语句支持:

class Context(object):
    def __init__(self):
        self.active = False

    def __enter__(self):
        print("Entering context")
        self.job = Job()
        self.job.start()
        return self.job

    def __exit__(self, exc_type, exc_value, traceback):
        print("Exiting context")
        self.job.stop()
        self.job.join()
        print("Job stopped")

我运行它使用下面的代码:

with Context():
    while input() != "stop":
        continue

这会一直等到用户键入 "stop" 并按回车键。如果在此循环期间用户改为按 Ctrl+C 来创建 KeyboardInterrupt,则仍会调用 __exit__ 方法:

Entering context
^CExiting context
Job stopped
Traceback (most recent call last):
  File "tmp2.py", line 48, in <module>
    while input() != "stop":
KeyboardInterrupt

我尝试这样做的第二种方法是使用 @contextmanager 装饰器创建一个函数:

from contextlib import contextmanager

@contextmanager
def job_context():
    print("Entering context")
    job = Job()
    job.start()
    yield job
    print("Exiting context")
    job.stop()
    job.join()
    print("Job stopped")

我再次运行它使用with语句:

with job_context():
    while input() != "stop":
        continue

但是当我 运行 它并按 Ctrl+C 时,yield 之后的代码 - 相当于第一个示例中的 __exit__ 方法,不是执行。相反,Python 脚本在无限循环中继续 运行。要停止程序,我必须再次按 Ctrl+C,此时 yield 之后的代码不会执行:

Entering context
^CTraceback (most recent call last):
  File "tmp2.py", line 42, in <module>
    while input() != "stop":
KeyboardInterrupt
^CException ignored in: <module 'threading' from '/usr/lib/python3.5/threading.py'>
Traceback (most recent call last):
  File "/usr/lib/python3.5/threading.py", line 1288, in _shutdown
    t.join()
  File "/usr/lib/python3.5/threading.py", line 1054, in join
    self._wait_for_tstate_lock()
  File "/usr/lib/python3.5/threading.py", line 1070, in _wait_for_tstate_lock
    elif lock.acquire(block, timeout):
KeyboardInterrupt

您可以在我按下 Ctrl+C 以创建中断的位置看到 ^C 符号。第二种情况有什么不同,它在第一种情况下不执行相当于 __exit__ 的关闭代码?

根据 the documentation:

If an unhandled exception occurs in the block, it is reraised inside the generator at the point where the yield occurred. Thus, you can use a try...except...finally statement to trap the error (if any), or ensure that some cleanup takes place.

在你的情况下,这看起来像:

@contextmanager
def job_context():
    print("Entering context")
    job = Job()
    job.start()
    try:
        yield job
    finally:
        print("Exiting context")
        job.stop()
        job.join()
        print("Job stopped")