Почему я не могу поймать это исключение python?
я писал некоторые модули etcd для SaltStack и столкнулся с этой странной проблемой, где это каким-то образом мешает мне поймать исключение, и мне интересно, как это делается. Кажется, конкретно вокруг 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.",)
Ок, давайте поймаем этого мерзавца:
#!/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
выглядит хорошо - все работает питон. Так в чем проблема? У меня есть это в модуле соли 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.
Hrm, это странно. чтение etcd должно было вернуть 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.
ВЗРЫВ ВЫ! Хорошо, давайте попробуем другой метод, который возвращает другое исключение etcd. Наш модуль теперь выглядит так:
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, которые он бросает. Но, хотя мы обычно можем поймать urllib3 ReadTimeoutError, когда мы запускаем python-etcd в одиночку, когда я запускаю его через соль, ничто, похоже, не может поймать это исключение urllib3, за исключением предложения "исключение".
я могу это сделать, но мне действительно любопытно, что, черт возьми, делает соль, которая делает это так, что исключение неуловимо. Я никогда не видел этого раньше при работе с python, поэтому мне было бы интересно, как это происходит и как я могу обойти это.
Edit:
так что я, наконец, смог поймать его.
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 не работает, поэтому они не могут быть одинаковыми.
я добавил следующее к сценарию, прямо перед тем, как вызвать С. читать().
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.исключения.ReadTimeoutError, потому что etcd сейчас больше не ловит исключение.
таким образом, кажется, что при импорте запросов он перезаписывает исключения urllib3, и любой другой модуль, который пытается поймать их, терпит неудачу. Кроме того, похоже, что более новые версии запросов не имеют этой проблемы.
1 ответов
мой ответ ниже немного спекуляции, потому что я не могу доказать это на практике с этими точными библиотеками (для начала я не могу воспроизвести вашу ошибку, так как это также зависит от версий библиотек и как они установлены), но тем не менее показывает один из возможных способов этого:
самый последний пример дает хороший ключ: дело в том, что в разные моменты времени выполнения программы name 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
- ясно, тогда все вернется на свои места как и должно быть.
это очень упрощенный пример, но достаточно иллюстративный, чтобы показать, что когда класс определен в каком-либо модуле, вновь созданный тип (объект, представляющий сам новый класс) "добавляется" под его объявленное имя класса в пространстве имен модуля. Как и любая другая переменная-ее значение может быть технически изменено. То же самое происходит с функциями.