In the event of an IndexError, is there a way to tell which object on a line is 'out of range'?
Consider this code:
a = [1,2,3]
b = [1,2,3]
x, y = get_values_from_somewhere()
try:
a[x] = b[y]
except IndexError as e:
....
In the event that x or y is too large and IndexError gets caught, I would like to know which of a or b is out of range (so I can perform different actions in the except block).
Clearly I could compare x and y to len(a) and len(b) respectively, but I am curious if there is another way of doing it using IndexError.
There is a way, but I wouldn't consider it as very robust. There is a subtle difference in the error messages:
a = [1,2,3]
b = [1,2,3]
a[2] = b[3]
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
<ipython-input-69-8e0d280b609d> in <module>()
2 b = [1,2,3]
3
----> 4 a[2] = b[3]
IndexError: list index out of range
But if the error is on the left hand side:
a[3] = b[2]
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
<ipython-input-68-9d66e07bc70d> in <module>()
2 b = [1,2,3]
3
----> 4 a[3] = b[2]
IndexError: list assignment index out of range
Note the 'assignment' in the message.
So, you could do something like:
a = [1,2,3]
b = [1,2,3]
try:
a[3] = b[2]
except IndexError as e:
message = e.args[0]
if 'assignment' in message:
print("Error on left hand side")
else:
print("Error on right hand side")
Output:
# Error on left hand side
Again, I wouldn't trust it too much, it would fail if the message changes in another version of Python.
I had a look at these parts of the source code, these different messages are really the only difference between the two errors.
The IndexError exception does not store information about what raised the exception. Its only data is an error message. You have to craft your code to do so.
a = [1,2,3]
b = [1,2,3]
x, y = get_values_from_somewhere()
try:
value = b[y]
except IndexError as e:
...
try:
a[x] = value
except IndexError as e:
...
I want to add I tried to recover the culprit through inspect.frame and was unable to do so. Thus I suspect there really is no robust way.
Finally, note that this is specific to IndexError as other exceptions may contain the needed information to infer what caused them. By example a KeyError contains the key that raised it.
A more robust approach would be to tap into the traceback object returned by sys.exc_info(), extract the code indicated by the file name and line number from the frame, use ast.parse to parse the line, subclass ast.NodeVisitor to find all the Subscript nodes, unparse (with astunparse) and eval the nodes with the frame's global and local variables to see which of the nodes causes exception, and print the offending expression:
import sys
import linecache
import ast
import astunparse
def find_index_error():
tb = sys.exc_info()[2]
frame = tb.tb_frame
lineno = tb.tb_lineno
filename = frame.f_code.co_filename
line = linecache.getline(filename, lineno, frame.f_globals)
class find_index_error_node(ast.NodeVisitor):
def visit_Subscript(self, node):
expr = astunparse.unparse(node).strip()
try:
eval(expr, frame.f_globals, frame.f_locals)
except IndexError:
print("%s causes IndexError" % expr)
find_index_error_node().visit(ast.parse(line.strip(), filename))
a = [1,2,3]
b = [1,2,3]
x, y = 1, 2
def f():
return 3
try:
a[f() - 1] = b[f() + y] + a[x + 1] # can you guess which of them causes IndexError?
except IndexError:
find_index_error()
This outputs:
b[(f() + y)] causes IndexError
Something like this perhaps?
a = [1,2,3]
b = [2,3,4]
x = 5
y = 1
try:
a[x] = b[y]
except IndexError:
try:
a[x]
print('b caused indexerror')
except IndexError:
print('a caused indexerror')
No, there is no way of deducing which of the two is out of bounds using the IndexError thrown.
Related
If the number in the line is already wroten, I must print 0 and if it's not, I must print 1.
Got ValueError and tried to catch it, but it fails.
a=int(input())
b=input()
n='1 '
c=b.split()
for i in range(a-1):
l=c[0]
c.remove(l)
d=c.index(l)
try:
a=a
except ValueError as ve:
if d > 0:
c = c.replace(l,'zero')
else:
c=c.replace(l,'one')
for amogus in range(a):
a=a.replace('one',1)
a=a.replace('zero',0)
print(a)
The error tracebacks like so:
Traceback (most recent call last):
File "program.pys3", line 8, in <module>
d=c.index(l)
ValueError: '1' is not in list
The first part of your loop is like this:
for i in range(a-1):
l = c[0]
c.remove(l)
d = c.index(l)
l is set to the first item in the list c. You then remove that item. The last line above then tries to find the index of that item again. This will work if there is another one, but if not will throw the error you report.
Did you mean to put the last line inside the try:?
for i in range(a-1):
l = c[0]
c.remove(l)
try:
d = c.index(l)
except ValueError as ve:
if d > 0:
c = c.replace(l,'zero')
else:
c = c.replace(l,'one')
Now a friendly warning: if this fixes the reported error you will encounter another one.
In a python script, if we have multiple lists in a single expression, for example:
a[1] = b[2] + c[3] + d[4]
OR
a[1] = b[c[d[1]]] # (This case added in EDIT)
Now, one of these lists throws an error IndexError: List Index Out of Range because the index is higher than the list length.
Is there a way to improve this default exception handling using try/except statements such that we can instantly figure out which list caused the problem?
Otherwise, one needs to check each list using a command line debugger. I understand that if an IDE is available, then this feature is probably inbuilt into the IDE.
The easiest way to do this is to artificially split the line over several lines. For example:
b =[1]
c = []
d = [1]
a = b[0] + \
c[0] + \
d[0]
Traceback (most recent call last):
File "/tmp/foo.py", line 7, in <module>
c[0] + \
IndexError: list index out of range
another option could be to use inspect module to find the line number that raised the error. This requires you to modify the addition + assignment to use local vars previously defined like bv = b[2] though, or else split it over individual lines as shown.
a = [1, 2, 3]
b = [1] * 3
c = [2] * 3
d = [3] * 5
try:
a[1] = b[2] + \
c[3] + \
d[4]
except IndexError:
from inspect import trace
var = trace()[0].code_context[0].split('=', 1)[-1].split('[', 1)[0].lstrip(' +-*')
print(f'The variable that raised the IndexError was: {var}')
Out:
The variable that raised the IndexError was: c
Interesting fact: the following syntax, which I actually consider good coding practice, actually does not behave how you want for an informative stack trace. I have no idea why, unfortunately.
a[1] = b[2] \
+ c[3] \
+ d[4]
This exact issue has been addressed and will be available in the python-3.11 interpreter . Reproduced below from the release notes:
"When printing tracebacks, the interpreter will now point to the exact expression that caused the error instead of just the line. For example:"
Traceback (most recent call last):
File "distance.py", line 11, in <module>
print(manhattan_distance(p1, p2))
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "distance.py", line 6, in manhattan_distance
return abs(point_1.x - point_2.x) + abs(point_1.y - point_2.y)
^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'x'
References:
Release notes
and
PEP657
I have a function which creates a list of integers. Under some circumstances it can happen (and will) that I can't determine a value to add to the list, so until now I added a float('nan'):
min_list = []
for obj in objects:
try:
min_list.append(obj.foo())
except Exception as e:
min_list.append(float('nan')) # I have to add something here,
# to get the correct index later
Then I want to find the index of the min value in the list:
index = min_list.index(min(min_list))
So, as you could imagine, appending float('nan') does not really work and I have no clue, how to solve this problem. Especially I also need to find the case, when every call of foo() raises a Exception(every value is a float('nan')):
nan_list = [e for e in min_list if isinstance(e, float)]
if len(nan_list) == len(min_list):
return None
else:
return index
How can I achieve that the None value is returned, if I have the same count of exceptions and objects? (I could also raise a exception or return a negative value). And how can I satisfy the problem to find the correct index, even with a raised exception, when I can't add a correct value to the list?
You can do:
def get_min(min_list):
try:
return min_list.index(min([np.inf if val is None else val for val in min_list]))
except ValueError:
return None
list_a = [None, 3, 5, 7, None, 5]
list_b = [None, None, None]
print(get_min(list_a)) #OUTPUT 1
print(get_min(list_b)) #OUTPUT None
The benefit of this method is that you don't really have to use numpy for it, and just replace np.inf with a very high value, that you know there is no way it will be in the original list (like 99999999999)
Numpy-based solution using nanargmin
import numpy as np
min_list = []
for obj in objects:
try:
min_list.append(obj.foo())
except Exception as e:
min_list.append(np.nan)
# ....
try:
# Case where there is at least one valid element
min_i = np.nanargmin(min_list)
except ValueError as e:
# Case where min_list is full of nan
I have a statement like this.
I just want to know which of the two assert statement has thrown the exception.
try:
assert re.search("xyz", statement)
assert re.search("abc", statement)
except AssertionError:
print "AssertionError : Expected Error message not found"
Thanks for the answer.
As mentioned in The assert statement docs, you can give an expression after the assertion test expression; that second expression will be passed in the AssertionError. Here's a simple demo:
for n in (-5, 10, 20):
try:
assert 0 <= n, '%d is too low' % n
assert n <= 10, '%d is too high' % n
print('%d is ok' % n)
except AssertionError as err:
print "AssertionError:", err
output
AssertionError: -5 is too low
10 is ok
AssertionError: 20 is too high
That second expression doesn't have to be a string, it can be anything. Since assertions should only be used to verify program logic, not to validate user data, I generally don't bother passing a nicely-formatted string, I just pass a tuple containing the relevant values, and maybe an identifying string. Eg,
assert (a * b > c), ('Bad product', a, b, c)
You could use functions from the traceback module. For example, extract_tb returns a list of tuples (named tuples in Python 3.5 and newer) representing the stack trace entries. Each tuple contains a line number as well as the source text line (if available).
import traceback
try:
assert 1
assert None
except AssertionError as e:
for x in traceback.extract_tb(e.__traceback__, limit=-1):
print(x.lineno, repr(x.line)) # Prints 5 'assert None'
You are able to print the last raised exception with traceback.print_exc(). An example:
>>> import traceback
>>> try:
... a = 1 / 0
... except:
... traceback.print_exc()
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
ZeroDivisionError: integer division or modulo by zero
There is also traceback.format_exc() in case you don't want to print.
Maybe you want to differ the exceptions?Then Getting exception details in Python can help you.
Copying the answer here:
import sys
try:
assert re.search("xyz", statement)
assert re.search("abc", statement)
except AssertionError:
type, value, traceback = sys.exc_info()
Then you can print out the info.
I would like to find a way to get the variables I assigned before an exception is made. E.G if the code is
try:
a=b
c=d
e=f
except:
bla bla
And an exception is generated at "e=f," I still want a=b and c=d
Is it possible? I realize I could make this multiple try statements but is there something I can do in one step?
Yes, it is perfectly possible. Below is a demonstration:
>>> try:
... a = 1
... b = 2
... c = 1/0 # This will raise a ZeroDivisionError
... except ZeroDivisionError:
... print 'an error occurred'
...
an error occurred
>>> a # a still exists
1
>>> b # so does b
2
>>> c # only c is undefined
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'c' is not defined
>>>
try/except is designed to execute the code in the try-block normally until an exception is raised. When that happens, the try-block is immediately exited. This means that only the code after the line that raised the exception is ignored.
The best you can do is to limit the number of expressions inside your try block.
If you need to know where the exception is raised, you are probably better off using multiple try...except's like you mentioned in your queestion, since there is no (practical) way to know where the exception was raised.
If the expressions are of the same type, you may want to put them in lists though, and loop over them, like:
vars = [a, b, c]
values = [1, 2, 0]
for i, (var, value) in enumerate(zip(vars, values)):
try:
var /= value
except ZeroDivisionError:
print 'The exception was raised on the {}. iteration'.format(i)
continue
try:
a = "foo"
c = "bar"
e = unknown_function()
except:
pass
print a, c # prints "foo bar"
Both a and c are set, you can simply use their values after the exception is handled. e is not set to anything as an exception was raised when that line was executed.
If I understand you correctly, this will happen by default. Raising an exception does not magically undo everything that has already happened inside the try block. In your example, if an error occurs on the assignment of e, a and c will still have the values they were assigned.
try:
a = b
c = d
e = f # oh noes! error! Try block skips to the end!
except: pass
print (a) #but a and c are still there
print (c)
This is a straight forward method without much of a hassle . This is the best way out when you cant predict what type of error can occur, or multiple errors can occur .
try :
a=10
except :
print "bla A"
try :
b=20
except:
print "bla B"
try:
c=d
except :
print "bla C"
Is it something like this that you want ?
import sys
code = """
b = 100
d = 200
a=b
c=d
e=10/0
g=e
h = 100
"""
for line in filter(None,code.splitlines()):
print line
try:
exec line
except:
sys.excepthook(sys.exc_info()[0],sys.exc_info()[1],None)
result
b = 100
d = 200
a=b
c=d
e=10/0
ZeroDivisionError: integer division or modulo by zero
g=e
NameError: name 'e' is not defined
h = 100