为什么我不能抓住这个python异常?

时间:2015-11-04 07:36:37

标签: python exception-handling

我正在为SaltStack编写一些etcd模块并遇到了这个奇怪的问题,它以某种方式阻止我捕获异常,我对它是如何做到这一点感兴趣。它似乎特别以urllib3为中心。

一个小脚本(不是盐):

import etcd
c = etcd.Client('127.0.0.1', 4001)
print c.read('/test1', wait=True, timeout=2)

当我们运行它时:

[root@alpha utils]# /tmp/etcd_watch.py
Traceback (most recent call last):
  File "/tmp/etcd_watch.py", line 5, in <module>
    print c.read('/test1', wait=True, timeout=2)
  File "/usr/lib/python2.6/site-packages/etcd/client.py", line 481, in read
    timeout=timeout)
  File "/usr/lib/python2.6/site-packages/etcd/client.py", line 788, in api_execute
    cause=e
etcd.EtcdConnectionFailed: Connection to etcd failed due to ReadTimeoutError("HTTPConnectionPool(host='127.0.0.1', port=4001): Read timed out.",)

好的,让我们抓住那个bugger:

#!/usr/bin/python

import etcd
c = etcd.Client('127.0.0.1', 4001)

try:
  print c.read('/test1', wait=True, timeout=2)
except etcd.EtcdConnectionFailed:
  print 'connect failed'

运行它:

[root@alpha _modules]# /tmp/etcd_watch.py
connect failed

看起来不错 - 这都是有效的python。那么问题是什么?我在salt etcd模块中有这个:

[root@alpha _modules]# cat sjmh.py
import etcd

def test():
  c = etcd.Client('127.0.0.1', 4001)
  try:
    return c.read('/test1', wait=True, timeout=2)
  except etcd.EtcdConnectionFailed:
    return False

当我们运行时:

[root@alpha _modules]# salt 'alpha' sjmh.test
alpha:
    The minion function caused an exception: Traceback (most recent call last):
      File "/usr/lib/python2.6/site-packages/salt/minion.py", line 1173, in _thread_return
        return_data = func(*args, **kwargs)
      File "/var/cache/salt/minion/extmods/modules/sjmh.py", line 5, in test
        c.read('/test1', wait=True, timeout=2)
      File "/usr/lib/python2.6/site-packages/etcd/client.py", line 481, in read
        timeout=timeout)
      File "/usr/lib/python2.6/site-packages/etcd/client.py", line 769, in api_execute
        _ = response.data
      File "/usr/lib/python2.6/site-packages/urllib3/response.py", line 150, in data
        return self.read(cache_content=True)
      File "/usr/lib/python2.6/site-packages/urllib3/response.py", line 218, in read
        raise ReadTimeoutError(self._pool, None, 'Read timed out.')
    ReadTimeoutError: HTTPConnectionPool(host='127.0.0.1', port=4001): Read timed out.

嗯,这很奇怪。 etcd的read应该返回etcd.EtcdConnectionFailed。那么,让我们进一步研究它。我们的模块现在是这样的:

import etcd

def test():
  c = etcd.Client('127.0.0.1', 4001)
  try:
    return c.read('/test1', wait=True, timeout=2)
  except Exception as e:
    return str(type(e))

我们得到:

[root@alpha _modules]# salt 'alpha' sjmh.test
alpha:
    <class 'urllib3.exceptions.ReadTimeoutError'>

好的,我们知道我们可以抓住这件事。我们现在知道它抛出了一个ReadTimeoutError,所以让我们抓住它。我们模块的最新版本:

import etcd
import urllib3.exceptions

def test():
  c = etcd.Client('127.0.0.1', 4001)
  try:
    c.read('/test1', wait=True, timeout=2)
  except urllib3.exceptions.ReadTimeoutError as e:
    return 'caught ya!'
  except Exception as e:
    return str(type(e))

我们的测试..

[root@alpha _modules]# salt 'alpha' sjmh.test
alpha:
    <class 'urllib3.exceptions.ReadTimeoutError'>
呃,等等,什么?我们为什么不赶上那个?例外工作,对吧..?

如果我们尝试从urllib3捕获基类怎么样..

[root@alpha _modules]# cat sjmh.py
import etcd
import urllib3.exceptions

def test():
  c = etcd.Client('127.0.0.1', 4001)
  try:
    c.read('/test1', wait=True, timeout=2)
  except urllib3.exceptions.HTTPError:
    return 'got you this time!'

希望并祈祷......

[root@alpha _modules]# salt 'alpha' sjmh.test
alpha:
    The minion function caused an exception: Traceback (most recent call last):
      File "/usr/lib/python2.6/site-packages/salt/minion.py", line 1173, in _thread_return
        return_data = func(*args, **kwargs)
      File "/var/cache/salt/minion/extmods/modules/sjmh.py", line 7, in test
        c.read('/test1', wait=True, timeout=2)
      File "/usr/lib/python2.6/site-packages/etcd/client.py", line 481, in read
        timeout=timeout)
      File "/usr/lib/python2.6/site-packages/etcd/client.py", line 769, in api_execute
        _ = response.data
      File "/usr/lib/python2.6/site-packages/urllib3/response.py", line 150, in data
        return self.read(cache_content=True)
      File "/usr/lib/python2.6/site-packages/urllib3/response.py", line 218, in read
        raise ReadTimeoutError(self._pool, None, 'Read timed out.')
    ReadTimeoutError: HTTPConnectionPool(host='127.0.0.1', port=4001): Read timed out.

BLAST YE!好吧,让我们尝试一种不同的方法,返回不同的etcd Exception。我们的模块现在看起来像这样:

import etcd

def test():
  c = etcd.Client('127.0.0.1', 4001)
  try:
    c.delete('/')
  except etcd.EtcdRootReadOnly:
    return 'got you this time!'

我们的跑步:

[root@alpha _modules]# salt 'alpha' sjmh.test
alpha:
    got you this time!

作为最后的测试,我制作了这个模块,我可以从直接python或作为盐模块运行..

import etcd
import urllib3

def test():
  c = etcd.Client('127.0.0.1', 4001)
  try:
    c.read('/test1', wait=True, timeout=2)
  except urllib3.exceptions.ReadTimeoutError:
    return 'got you this time!'
  except etcd.EtcdConnectionFailed:
    return 'cant get away from me!'
  except etcd.EtcdException:
    return 'oh no you dont'
  except urllib3.exceptions.HTTPError:
    return 'get back here!'
  except Exception as e:
    return 'HOW DID YOU GET HERE? {0}'.format(type(e))

if __name__ == "__main__":
  print test()

通过python:

[root@alpha _modules]# python ./sjmh.py
cant get away from me!

通过盐:

[root@alpha _modules]# salt 'alpha' sjmh.test
alpha:
    HOW DID YOU GET HERE? <class 'urllib3.exceptions.ReadTimeoutError'>

因此,我们可以从它抛出的etcd中捕获异常。但是,虽然当我们通过它的寂寞运行python-etcd时,我们通常能够捕获urllib3 ReadTimeoutError,但是当我通过salt运行它时,似乎没有任何东西能够捕获该urllib3异常,除了一个毯子'异常'子句。

我可以做到这一点,但我真的很好奇,正在做的是什么,这使得异常无法捕捉。我在使用python之前从未见过这个,所以我很好奇它是如何发生的以及我如何解决它。

编辑:

所以我终于能够抓住它。

import etcd
import urllib3.exceptions
from urllib3.exceptions import ReadTimeoutError

def test():
  c = etcd.Client('127.0.0.1', 4001)
  try:
    c.read('/test1', wait=True, timeout=2)
  except urllib3.exceptions.ReadTimeoutError:
    return 'caught 1'
  except urllib3.exceptions.HTTPError:
    return 'caught 2'
  except ReadTimeoutError:
    return 'caught 3'
  except etcd.EtcdConnectionFailed as ex:
    return 'cant get away from me!'
  except Exception as ex:
    return 'HOW DID YOU GET HERE? {0}'.format(type(ex))

if __name__ == "__main__":
  print test()

跑步时:

[root@alpha _modules]# salt 'alpha' sjmh.test
alpha:
    caught 3

但它仍然没有意义。根据我所知的例外情况,回报应该是“被抓住1”。为什么我必须直接导入异常的名称,而不是仅仅使用完整的类名?

更多编辑!

因此,在两个类之间添加比较会产生'False' - 这很明显,因为except子句不起作用,所以它们可能不一样。

在我调用c.read()之前,我在脚本中添加了以下内容。

log.debug(urllib3.exceptions.ReadTimeoutError.__module__)
log.debug(ReadTimeoutError.__module__)

现在我在日志中得到这个:

[DEBUG   ] requests.packages.urllib3.exceptions
[DEBUG   ] urllib3.exceptions

所以,这似乎是被抓住的原因。只需下载etcd并请求库并执行以下操作即可重现:

#!/usr/bin/python

#import requests
import etcd

c = etcd.Client('127.0.0.1', 4001)
c.read("/blah", wait=True, timeout=2)

你最终会引发'正确'异常 - etcd.EtcdConnectionFailed。但是,取消注释“请求”,你最终会得到urllib3.exceptions.ReadTimeoutError,因为etcd现在不再捕获异常。

因此,当导入请求时,它会重写urllib3异常,并且尝试捕获这些异常的任何其他模块都会失败。此外,似乎较新版本的请求没有此问题。

1 个答案:

答案 0 :(得分:3)

我的回答有点推测,因为我不能用这些精确的库来证明它(开头我不能重现你的错误,因为它还取决于库的版本以及它们的安装方式),但是仍然显示了一个发生这种情况的可能方式:

最后一个例子提供了一个很好的线索:关键在于,在程序执行时的不同时刻,名称urllib3.exceptions.ReadTimeoutError可能引用不同的类。 ReadTimeoutError就像在Python中的每个其他模块一样,只是urllib3.exceptions命名空间中的名称,并且可以重新分配(但这并不意味着它是一个这样做的好主意。)

当通过其完全限定的“路径”引用此名称时 - 我们保证在我们引用它时引用它的实际状态。但是,当我们第一次像from urllib3.exceptions import ReadTimeoutError一样导入它时 - 它会将名称ReadTimeoutError带入执行导入的命名空间,并且此名称在时间上绑定到urllib3.exceptions.ReadTimeoutError的值这个导入。现在,如果某些其他代码稍后重新分配urllib3.exceptions.ReadTimeoutError的值 - 这两个(它的“当前”/“最新”值和之前导入的值)可能实际上是不同的 - 所以从技术上讲,你最终可能会有两个不同的类。现在,将实际引发哪个异常类 - 这取决于引发错误的代码如何使用它:如果它们先前将ReadTimeoutError导入其命名空间 - 那么将引发这个(“原始”)。

要验证是否是这种情况,您可以将以下内容添加到except ReadTimeoutError块:

print(urllib3.exceptions.ReadTimeoutError == ReadTimeoutError)

如果这打印False - 它证明了在引发异常时,两个“引用”确实引用了不同的类。

可以产生类似结果的不良实现的简化示例:

文件api.py(设计合理并且自己幸福地存在):

class MyApiException(Exception):
    pass

def foo():
    raise MyApiException('BOOM!')

文件apibreaker.py(应该责备的人):

import api

class MyVeryOwnException(Exception):
    # note, this doesn't extend MyApiException,
    # but creates a new "branch" in the hierarhcy
    pass

# DON'T DO THIS AT HOME!
api.MyApiException = MyVeryOwnException

档案apiuser.py

import api
from api import MyApiException, foo
import apibreaker

if __name__ == '__main__':
    try:
        foo()
    except MyApiException:
        print("Caught exception of an original class")
    except api.MyApiException:
        print("Caught exception of a reassigned class")

执行时:

$ python apiuser.py
Caught exception of a reassigned class

如果您删除行import apibreaker - 显然所有内容都会回到原来的位置。

这是一个非常简化的示例,但足以表明当在某个模块中定义类时 - 新创建的类型(表示新类本身的对象)在其声明的类名下“添加”到模块的名称空间。与任何其他变量一样 - 其值可以在技术上进行修改。功能也是如此。