Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python Compilation/Interpretation Process

I'm trying to understand the python compiler/interpreter process more clearly. Unfortunately, I have not taken a class in interpreters nor have I read much about them.

Basically, what I understand right now is that Python code from .py files is first compiled into python bytecode (which I assume are the .pyc files I see occasionally?). Next, the bytecode is compiled into machine code, a language the processor actually understands. Pretty much, I've read this thread Why python compile the source to bytecode before interpreting?

Could somebody give me a good explanation of the whole process keeping in mind that my knowledge of compilers/interpreters is almost non-existent? Or, if that's not possible, maybe give me some resources that give quick overviews of compilers/interpreters?

Thanks

like image 430
NickHalden Avatar asked Jul 21 '10 13:07

NickHalden


People also ask

How is Python compiled and interpreted?

Python is an interpreted language, which means the source code of a Python program is converted into bytecode that is then executed by the Python virtual machine. Python is different from major compiled languages, such as C and C + +, as Python code is not required to be built and linked like code for these languages.

How is interpretation done in Python?

An interpreter is a kind of program that executes other programs. When you write Python programs , it converts source code written by the developer into intermediate language which is again translated into the native language / machine language that is executed.

What interpreter does Python use?

Interpreter python is widely used throughout the computer programming and source coding industries. Python interpreter takes an interactive command and executes it. All lines of source code are completed (translated) one line at a time. Compilation of the source codes occur through the translation process.


Video Answer


1 Answers

The bytecode is not actually interpreted to machine code, unless you are using some exotic implementation such as pypy.

Other than that, you have the description correct. The bytecode is loaded into the Python runtime and interpreted by a virtual machine, which is a piece of code that reads each instruction in the bytecode and executes whatever operation is indicated. You can see this bytecode with the dis module, as follows:

>>> def fib(n): return n if n < 2 else fib(n - 2) + fib(n - 1) ...  >>> fib(10) 55 >>> import dis >>> dis.dis(fib)   1           0 LOAD_FAST                0 (n)               3 LOAD_CONST               1 (2)               6 COMPARE_OP               0 (<)               9 JUMP_IF_FALSE            5 (to 17)              12 POP_TOP                           13 LOAD_FAST                0 (n)              16 RETURN_VALUE                 >>   17 POP_TOP                           18 LOAD_GLOBAL              0 (fib)              21 LOAD_FAST                0 (n)              24 LOAD_CONST               1 (2)              27 BINARY_SUBTRACT                   28 CALL_FUNCTION            1              31 LOAD_GLOBAL              0 (fib)              34 LOAD_FAST                0 (n)              37 LOAD_CONST               2 (1)              40 BINARY_SUBTRACT                   41 CALL_FUNCTION            1              44 BINARY_ADD                        45 RETURN_VALUE         >>>  

Detailed explanation

It is quite important to understand that the above code is never executed by your CPU; nor is it ever converted into something that is (at least, not on the official C implementation of Python). The CPU executes the virtual machine code, which performs the work indicated by the bytecode instructions. When the interpreter wants to execute the fib function, it reads the instructions one at a time, and does what they tell it to do. It looks at the first instruction, LOAD_FAST 0, and thus grabs parameter 0 (the n passed to fib) from wherever parameters are held and pushes it onto the interpreter's stack (Python's interpreter is a stack machine). On reading the next instruction, LOAD_CONST 1, it grabs constant number 1 from a collection of constants owned by the function, which happens to be the number 2 in this case, and pushes that onto the stack. You can actually see these constants:

>>> fib.func_code.co_consts (None, 2, 1) 

The next instruction, COMPARE_OP 0, tells the interpreter to pop the two topmost stack elements and perform an inequality comparison between them, pushing the Boolean result back onto the stack. The fourth instruction determines, based on the Boolean value, whether to jump forward five instructions or continue on with the next instruction. All that verbiage explains the if n < 2 part of the conditional expression in fib. It will be a highly instructive exercise for you to tease out the meaning and behaviour of the rest of the fib bytecode. The only one, I'm not sure about is POP_TOP; I'm guessing JUMP_IF_FALSE is defined to leave its Boolean argument on the stack rather than popping it, so it has to be popped explicitly.

Even more instructive is to inspect the raw bytecode for fib thus:

>>> code = fib.func_code.co_code >>> code '|\x00\x00d\x01\x00j\x00\x00o\x05\x00\x01|\x00\x00S\x01t\x00\x00|\x00\x00d\x01\x00\x18\x83\x01\x00t\x00\x00|\x00\x00d\x02\x00\x18\x83\x01\x00\x17S' >>> import opcode >>> op = code[0] >>> op '|' >>> op = ord(op) >>> op 124 >>> opcode.opname[op] 'LOAD_FAST' >>>  

Thus you can see that the first byte of the bytecode is the LOAD_FAST instruction. The next pair of bytes, '\x00\x00' (the number 0 in 16 bits) is the argument to LOAD_FAST, and tells the bytecode interpreter to load parameter 0 onto the stack.

like image 55
Marcelo Cantos Avatar answered Sep 29 '22 19:09

Marcelo Cantos