Question Processus de compilation / interprétation de Python


J'essaie de comprendre le processus du compilateur / interprète de python plus clairement. Malheureusement, je n'ai pas pris de cours avec des interprètes et je n'ai pas beaucoup lu à leur sujet.

Fondamentalement, ce que je comprends tout de suite, c’est que le code Python des fichiers .py est d’abord compilé en bytecode python (ce que je suppose être les fichiers .pyc que je vois de temps en temps?). Ensuite, le bytecode est compilé en code machine, un langage que le processeur comprend réellement. A peu près, j'ai lu ce fil Pourquoi python compile-t-il la source au bytecode avant d'interpréter?

Quelqu'un pourrait-il me donner une bonne explication de l'ensemble du processus en gardant à l'esprit que ma connaissance des compilateurs / interprètes est quasiment inexistante? Ou, si ce n'est pas possible, donnez-moi peut-être des ressources qui donnent un aperçu rapide des compilateurs / interprètes?

Merci


35
2017-07-21 13:21


origine


Réponses:


Le bytecode n'est pas réellement interprété en code machine, sauf si vous utilisez une implémentation exotique telle que pypy.

En dehors de cela, vous avez la description correcte. Le bytecode est chargé dans l'exécution Python et interprété par une machine virtuelle, qui est un morceau de code qui lit chaque instruction du bytecode et exécute toute opération indiquée. Vous pouvez voir ce bytecode avec le dis module, comme suit:

>>> 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        
>>> 

Explication détaillée

Il est très important de comprendre que le code ci-dessus n'est jamais exécuté par votre CPU; elle n'est jamais non plus convertie en quelque chose qui soit (du moins, pas sur l'implémentation C officielle de Python). Le processeur exécute le code de la machine virtuelle, qui effectue le travail indiqué par les instructions du bytecode. Lorsque l'interprète veut exécuter le fib fonction, il lit les instructions une par une et fait ce qu’elles lui disent de faire. Il regarde la première instruction, LOAD_FAST 0, et saisit donc le paramètre 0 (le n transmis à fib) à partir de là où les paramètres sont conservés et la place dans la pile de l’interpréteur (l’interpréteur de Python est une machine à pile). En lisant l'instruction suivante, LOAD_CONST 1, il saisit la constante numéro 1 dans une collection de constantes appartenant à la fonction, qui se trouve être le numéro 2 dans ce cas, et la transmet à la pile. Vous pouvez réellement voir ces constantes:

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

La prochaine instruction, COMPARE_OP 0, indique à l'interpréteur de faire apparaître les deux éléments de pile les plus hauts et d'effectuer une comparaison d'inégalité entre eux, en renvoyant le résultat booléen sur la pile. La quatrième instruction détermine, en fonction de la valeur booléenne, s'il faut avancer de cinq instructions ou continuer avec l'instruction suivante. Tout ce verbiage explique la if n < 2 partie de l'expression conditionnelle dans fib. Ce sera un exercice très instructif pour vous de dégager le sens et le comportement du reste de la fib bytecode. Le seul, je ne suis pas sûr de est POP_TOP; je devine JUMP_IF_FALSE est défini pour laisser son argument booléen sur la pile plutôt que de le sauter, il doit donc être explicitement affiché.

Encore plus instructif est d'inspecter le bytecode brut pour fib Ainsi:

>>> 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'
>>> 

Ainsi, vous pouvez voir que le premier octet du bytecode est le LOAD_FAST instruction. La prochaine paire d'octets, '\x00\x00' (le nombre 0 dans 16 bits) est l'argument de LOAD_FAST, et indique à l'interpréteur de bytecode de charger le paramètre 0 sur la pile.


44
2017-07-21 13:28



Pour compléter le grand La réponse de Marcelo Cantos, voici juste un petit résumé colonne par colonne pour expliquer la sortie du bytecode désassemblé.

Par exemple, étant donné cette fonction:

def f(num):
    if num == 42:
        return True
    return False

Cela peut être démonté dans (Python 3.6):

(1)|(2)|(3)|(4)|          (5)         |(6)|  (7)
---|---|---|---|----------------------|---|-------
  2|   |   |  0|LOAD_FAST             |  0|(num)
   |-->|   |  2|LOAD_CONST            |  1|(42)
   |   |   |  4|COMPARE_OP            |  2|(==)
   |   |   |  6|POP_JUMP_IF_FALSE     | 12|
   |   |   |   |                      |   |
  3|   |   |  8|LOAD_CONST            |  2|(True)
   |   |   | 10|RETURN_VALUE          |   |
   |   |   |   |                      |   |
  4|   |>> | 12|LOAD_CONST            |  3|(False)
   |   |   | 14|RETURN_VALUE          |   |

Chaque colonne a un but spécifique:

  1. Le correspondant numéro de ligne dans le code source
  2. Indique éventuellement le instruction actuelle exécuté (lorsque le bytecode provient d'un objet cadre par exemple)
  3. Une étiquette qui dénote un possible JUMP d'une instruction antérieure à celui-ci
  4. le adresse dans le bytecode qui correspond à l'index d'octet (ce sont des multiples de 2 car Python 3.6 utilise 2 octets pour chaque instruction, alors qu'il pourrait varier dans les versions précédentes)
  5. Le nom de l'instruction (également appelé opname), chacun est brièvement expliqué dans la dis module et leur mise en œuvre peut être trouvée dans ceval.c (la boucle principale de CPython)
  6. le argument (le cas échéant) de l'instruction utilisée en interne par Python pour récupérer des constantes ou des variables, gérer la pile, accéder à une instruction spécifique, etc.
  7. le interprétation conviviale de l'argument d'instruction

1
2017-11-28 10:19