在 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。然而,虽然我们没有修改 resp1
或 resp2
,但在第二次调用 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()
时都会返回一个新的响应对象,并且不会影响到先前的响应对象。