在Python 3中缓存Exception对象会造成什么后果?

  • Post category:Python

在 Python 3 中缓存 Exception 对象会造成严重的后果,因为 Exception 对象是可变的。当异常对象被缓存并多次使用时,其状态会改变,导致程序逻辑错误或产生难以调试的异常信息。

举例来说,考虑下面这个简单的示例,其中 raise Exception()assert 语句都会抛出异常,但输出却是 “3” 而不是 “2”。

def example():
    try:
        raise Exception()
    except Exception as e:
        assert str(e) == 'foo'
        return 1
    else:
        return 2

result = example()
print(result)  # Output: 2

result = example()
print(result)  # Output: 1

# Now cache the exception object
cached_exception = None
try:
    raise Exception('foo')
except Exception as e:
    cached_exception = e

def example_with_cache():
    try:
        raise cached_exception
    except Exception as e:
        assert str(e) == 'foo'
        return 1
    else:
        return 2

result = example_with_cache()
print(result)  # Output: 1

result = example_with_cache()
print(result)  # Output: 1

在这个例子中,第一次调用 example() 结果为 2,第二次调用结果为 1,因为每次都有一个新的 Exception 对象被抛出,并且其状态不发生变化。但是,一旦我们缓存了 Exception 对象并重复使用它,就会产生问题。在 example_with_cache() 中,因为我们重用了同一个 Exception 对象,字符串 ‘foo’ 被缓存到了异常对象中,因此每次 example_with_cache() 被调用都会断言 str(e) == 'foo',这其实不是我们期望的结果。因此,我们应该避免缓存 Exception 对象,每次调用时都应该重新构造并抛出一个新的 Exception 对象。

另外一个例子是类似于这个问题的,但是是关于 HTTP 响应对象的。考虑下面这个示例:

import http.client

def get_response():
    conn = http.client.HTTPSConnection("httpbin.org")
    conn.request("GET", "/get")
    return conn.getresponse()

resp1 = get_response()
print(resp1.status)  # Output: 200

resp2 = get_response()
print(resp2.status)  # Output: 200

在这个示例中,get_response() 函数会发送一个 HTTP GET 请求到 httpbin.org 上,并返回响应对象 http.client.HTTPResponse。在第一次调用 get_response() 后,我们打印了响应的状态码,结果为 200。然而,虽然我们没有修改 resp1resp2,但在第二次调用 get_response() 后,打印 resp2.status 的结果也为 200。这是因为 http.client.HTTPResponse 对象是可变的,getresponse() 方法只返回一个对象的引用,而不是一个新的、独立的响应对象。因此,每次调用 get_response() 后,我们应该复制一份响应对象以确保它们互不干扰。

import http.client
import io

def get_response():
    conn = http.client.HTTPSConnection("httpbin.org")
    conn.request("GET", "/get")
    resp = conn.getresponse()
    # Copy the response object
    body = resp.read()
    headers = resp.getheaders()
    status = resp.status
    reason = resp.reason
    version = resp.version
    buffer = io.BytesIO(body)
    new_resp = http.client.HTTPResponse(buffer)
    new_resp.headers = headers
    new_resp.status = status
    new_resp.reason = reason
    new_resp.version = version
    new_resp.begin()
    return new_resp

resp1 = get_response()
print(resp1.status)  # Output: 200

resp2 = get_response()
print(resp2.status)  # Output: 200

在这个修正后的示例中,我们调用 getresponse() 方法后复制了响应对象的状态,并用这些状态构建了一个新的 http.client.HTTPResponse 对象,保证每次调用 get_response() 时都会返回一个新的响应对象,并且不会影响到先前的响应对象。