Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Which line is chosen to be reported in exception

Suppose I have a multiline in Python which raises an Exception.

How does Python decide which line to raise the exception against?

Examples: (Note: I could have used backslashes \ after each line)

(1
 +0/0
 +3)

Thows an exception on line 3 (a ZeroDivisionError exception, at +3)).

(1
 +
 0/0
 )

Throws an exception on line 3.

(0/0
 +
 1)

Throws an exception on line 2.

This question was inspired by this example, and @Godman pointed out that exceptions don't just occur on the last line (as I had previously thought).

like image 229
Andy Hayden Avatar asked Aug 16 '12 16:08

Andy Hayden


People also ask

What is an exception in Python?

An Exception is an error that happens during the execution of a program. Whenever there is an error, Python generates an exception that could be handled. It basically prevents the program from getting crashed.


2 Answers

Fundamentally, I dont think we all are thinking on the right lines. There is no such thing as the last line here. The exception is raised by the interpreter when it receives an expression fully. According to the Python grammar: http://docs.python.org/reference/grammar.html, the expression is not fully complete until you hit a closing brace ')'. A brief explanation for the same was given by Joran Beasley in the comments against the question itself.

You can do 3 things do judge the correctness of this, without delving much deep into the grammar:-

  1. Write this code in the python interpreter:

    a=(1+2+0/0+4+5)

This also raises ZeroDivionError.

  1. Write this code in the python interpreter:

    a=(1+2+0/0+4+5 # And, press enter

This gives you invalid syntax since the expression is not complete and can not be parsed by the interpreter PS: This is the same as the code mentioned in the question

  1. Write this code in the python interpreter:

a = (1
+2
+0/0
+4
+5)

Eventually, the expression does not complete until you hit the closing brace. Hence, you can continue to add on more sub-expressions inside it without getting any exception. Thus, fundamentally, the interpreter does not see this all as line numbers; it waits until the all the expressions (including the sub-expressions) have completed. And, it is a proper programming control flow for the interpeter.

PS: Forgive my formatting of the answer.

New edit:-

@ Hayden: I thought it would be easy to explain the subtleties by not delving too deep into the grammar. But, for your reference, I am just copying the code from the URL: http://nedbatchelder.com/blog/200804/the_structure_of_pyc_files.html

Steps to run:- 1. Write your code asked in the question in a temp.py file and save it, then, import temp in another file or in the interpreter. This will create temp.pyc 2. Now, copy and paste the full code in the above mentioned URL in byteCodeDetails.py and run the file in command prompt as: python byteCodeDetails.py temp.pyc. The function show_file will be called here and will give the following output:-

magic 03f30d0a
moddate 458c2e50 (Fri Aug 17 23:54:05 2012) code
argcount 0
nlocals 0 stacksize 3 flags 0040 code
640600640200640200151764030017640400175a000064050053 5
0
LOAD_CONST 6 (3)
3 LOAD_CONST 2 (0)
6 LOAD_CONST 2 (0)
9 BINARY_DIVIDE
10 BINARY_ADD
11 LOAD_CONST 3 (4)
14 BINARY_ADD
15 LOAD_CONST 4 (5)
18 BINARY_ADD
19 STORE_NAME 0 (a)
22 LOAD_CONST 5 (None)
25 RETURN_VALUE
consts
1
2
0
4
5
None
3
names ('a',)
varnames ()
freevars ()
cellvars ()
filename 'C:\Users\Python\temp1.py'

name ''
firstlineno 5
lnotab


So, as you can notice that:-

  1. Quoting from the link mentioned above: In the disassembled output, the left-most numbers (1, 2, 3) are the line numbers in the original source file and the next numbers (0, 3, 6, 9, ...) are the byte offsets of the instruction. Similarly, for your code, the left-most number is only 5 which is the line number, and the columns to the right represent the mnemonics (to be read by the interpreter) translated by the compiler for your code., thus indicating how expressions are formed and their formation is overtaken by the value of the line numbers in the compiled code.
  2. firstlineno points to 5.

Now, just make a slight change in your initial code in the temp.py file:-

a = (1
+2
+0/0
+4+
5)

Now, Run through the above 2 steps once again. The following is the output:-

magic 03f30d0a
moddate 0f8e2e50 (Sat Aug 18 00:01:43 2012)
code
argcount 0
nlocals 0
stacksize 3

flags 0040
code 640600640200640200151764030017640400175a000064050053
4
0 LOAD_CONST 6 (3)
3 LOAD_CONST 2 (0)
6 LOAD_CONST 2 (0)
9 BINARY_DIVIDE
10 BINARY_ADD
11 LOAD_CONST 3 (4)
14 BINARY_ADD

5 15 LOAD_CONST 4 (5)
18 BINARY_ADD
19 STORE_NAME 0 (a)
22 LOAD_CONST 5 (None)
25 RETURN_VALUE
consts
1
2
0
4
5
None
3
names ('a',)
varnames ()
freevars ()
cellvars ()
filename 'C:\Users\Python\temp1.py'
name ''
firstlineno 4

lnotab 0f01

Well, now you can clearly see 2 things:-

  1. The byte code is composed of 2 lines shown in the next line to 'code 640600640200640200151764030017640400175a000064050053', prefixed by '4' and '5'. This shows that the compiler has parsed the .py file and converted the code in temp.py into 2 lines of code which will be run by the interpreter. Note that here the contents of line 4 will be executed by the interpreter no matter the expression is complete or not
  2. The value of firstlineno changes to 4 instead of 5

The whole point of this lengthy discussion is that wherever the byte code indicates to the interpreter that this is where a line starts and the corresponding statements which should be executed for this line, then, the interpreter just runs that line and the corresponding statements written next to it.

The code in your question shows firstlineno as 5, that is why you receive an error in line 5. Hopefully, this helps now.

like image 101
GodMan Avatar answered Sep 17 '22 21:09

GodMan


The exception will point to the line* containing either:

  1. The last operator (if the previous literals/operators caused the exception).

  2. The last literal (otherwise i.e. the last literal/operator caused the exception).

.

However, if this is not the behaviour that you see, it may be caused by discrepancies in one of your py (source) files and either its corresponding (compiled) pyc file, or the running code (in memory). The following is an illustrative example.

  • Suppose E.py contains:

    def z():
        0/0
    
  • From the python command line, import E (this will compile E.py into byte-code: E.pyc, and puts into memory).

  • Call E.z(), which will produce an exception, on line 2 in z, displaying the line 0/0 - no surprise here.

  • Go back to the E.py source file, insert two lines at the top, and on the second insert the string "oh dear, oh dear".

  • Go back in the python command line, and call E.z() a second time.

  • The exception (on line 2, in z) now displays "oh dear, oh dear".

*Update: I don't have a reference for this, please comment one if you come across one. I had previously thought that it was the simply the last line!

like image 27
Andy Hayden Avatar answered Sep 20 '22 21:09

Andy Hayden