Maison > développement back-end > Tutoriel Python > Comment j'ai ajouté la prise en charge des fonctions imbriquées dans le bytecode Python

Comment j'ai ajouté la prise en charge des fonctions imbriquées dans le bytecode Python

Susan Sarandon
Libérer: 2024-12-31 18:58:18
original
897 Les gens l'ont consulté

How I added support for nested functions in Python bytecode

Je voulais partager des trucs plutôt sympas J'ai découvert le bytecode Python avec vous, notamment comment j'ai ajouté la prise en charge des fonctions, mais mon gars de l'imprimerie a dit que je devais le limiter à 500 mots.

C'est une semaine de vacances, il haussa les épaules. Qu'attendez-vous de moi ?

Hors extraits de code, j'ai négocié.

Bien, a-t-il cédé.

Savez-vous pourquoi nous utilisons le bytecode en premier lieu ?

Je fais juste fonctionner la presse à imprimer, mais je te fais confiance.

Assez juste. Commençons.

Pourquoi nous utilisons le bytecode en premier lieu

Memphis, mon interpréteur Python écrit en Rust, dispose de deux moteurs d'exécution. Aucun des deux ne peut exécuter tout le code, mais les deux peuvent exécuter du code.

Mon interprète de promenade dans les arbres est ce que vous construireriez si vous ne saviez pas ce que vous faisiez. ?‍♂️ Vous tokenisez le code Python d'entrée, générez un arbre de syntaxe abstraite (AST), puis parcourez l'arbre et évaluez chaque nœud. Les expressions renvoient des valeurs et les instructions modifient la table des symboles, qui est implémentée sous la forme d'une série de portées respectant les règles de portée de Python. Rappelez-vous simplement le LEGB pneumonique facile : local, englobant, global, intégré.

Ma machine virtuelle bytecode est ce que vous créeriez si vous ne saviez pas ce que vous faisiez mais vouliez agir comme vous le faisiez. Aussi ?‍♂️. Pour ce moteur, les jetons et l'AST fonctionnent de la même manière, mais plutôt que de marcher, nous décollons du sprint. Nous compilons l'AST en une représentation intermédiaire (IR) ci-après appelée bytecode. Nous créons ensuite une machine virtuelle (VM) basée sur une pile, qui agit conceptuellement comme un processeur, exécutant les instructions de bytecode en séquence, mais elle est entièrement implémentée dans le logiciel.

(Pour un guide complet des deux approches sans divagations, Crafting Interpreters est excellent.)

Pourquoi faisons-nous cela en premier lieu ? N'oubliez pas les deux P : portabilité et performances. Rappelez-vous qu'au début des années 2000, personne ne se taisait sur la portabilité du bytecode Java ? Tout ce dont vous avez besoin est une JVM et vous pouvez exécuter un programme Java compilé sur n'importe quelle machine ! Python a choisi de ne pas suivre cette approche pour des raisons à la fois techniques et marketing, mais en théorie, les mêmes principes s'appliquent. (En pratique, les étapes de compilation sont différentes et je regrette d'ouvrir cette boîte de Pandore.)

La performance est cependant le plus important. Plutôt que de parcourir un AST plusieurs fois au cours de la durée de vie d'un programme, l'IR compilé est une représentation plus efficace. Nous constatons une amélioration des performances en évitant la surcharge liée au parcours répété d'un AST, et sa structure plate se traduit souvent par une meilleure prédiction de branchement et une meilleure localisation du cache au moment de l'exécution.

(Je ne vous en veux pas de ne pas penser à la mise en cache si vous n'avez pas d'expérience en architecture informatique. Bon sang, j'ai commencé ma carrière dans cette industrie et je pense beaucoup moins à la mise en cache qu'à la façon d'éviter écrire deux fois la même ligne de code. Alors faites-moi confiance sur la performance. C'est mon style de leadership : une confiance aveugle.)

Hé mon pote, ça fait 500 mots. Nous devons charger le cadre et le laisser déchirer.

Déjà ?! Vous avez exclu des extraits de code ?

Il n'y a pas d'extraits de code, mon homme.

D'accord, d'accord. Juste 500 de plus. Je le promets.

Le contexte est important pour les variables Python

J'ai fait un peu de chemin avant de déposer mon implémentation de bytecode VM il y a environ un an : je pouvais définir des fonctions et des classes Python, appeler ces fonctions et instancier ces classes. J'ai réprimé ce comportement avec quelques tests. Mais je savais que ma mise en œuvre était compliquée et que je devrais revoir les fondamentaux avant d’ajouter des éléments plus amusants. C'est maintenant la semaine de Noël et je veux ajouter des trucs amusants.

Considérez cet extrait pour appeler une fonction, en gardant un œil sur le TODO.

fn compile_function_call(
    &mut self,
    name: &str,
    args: &ParsedArguments)
) -> Result<Bytecode, CompileError> {
    let mut opcodes = vec![];

    // We push the args onto the stack in reverse call order so that we will pop
    // them off in call order.
    for arg in args.args.iter().rev() {
        opcodes.extend(self.compile_expr(arg)?);
    }

    let (_, index) = self.get_local_index(name);

    // TODO how does this know if it is a global or local index? this may not be the right
    // approach for calling a function
    opcodes.push(Opcode::Call(index));

    Ok(opcodes)
}
Copier après la connexion
Copier après la connexion

Avez-vous fini de réfléchir ? Nous chargeons les arguments de la fonction sur la pile et « appelons la fonction ». Dans le bytecode, tous les noms sont convertis en index (car l'accès aux index est plus rapide pendant l'exécution de la VM), mais nous n'avons pas vraiment de moyen de savoir si nous avons affaire ici à un index local ou à un index global.

Considérez maintenant la version améliorée.

fn compile_function_call(
    &mut self,
    name: &str,
    args: &ParsedArguments)
) -> Result<Bytecode, CompileError> {
    let mut opcodes = vec![self.compile_load(name)];

    // We push the args onto the stack in reverse call order so that we will pop
    // them off in call order.
    for arg in args.args.iter().rev() {
        opcodes.extend(self.compile_expr(arg)?);
    }

    let argc = opcodes.len() - 1;
    opcodes.push(Opcode::Call(argc));

    Ok(opcodes)
}
Copier après la connexion

Merci d'avoir pris en compte ce code.

Nous prenons désormais en charge les appels de fonctions imbriqués ! Qu'est-ce qui a changé ?

  1. L'opcode Call prend désormais un certain nombre d'arguments de position, plutôt qu'un index de la fonction. Cela indique à la VM combien d'arguments doivent être retirés de la pile avant d'appeler la fonction.
  2. Après avoir retiré les arguments de la pile, la fonction elle-même sera laissée sur la pile et compile_load a déjà géré la portée locale par rapport à la portée globale pour nous.

LOAD_GLOBAL contre LOAD_FAST

Jetons un coup d'œil à ce que fait compile_load.

fn compile_load(&mut self, name: &str) -> Opcode {
    match self.ensure_context() {
        Context::Global => Opcode::LoadGlobal(self.get_or_set_nonlocal_index(name)),
        Context::Local => {
            // Check locals first
            if let Some(index) = self.get_local_index(name) {
                return Opcode::LoadFast(index);
            }

            // If not found locally, fall back to globals
            Opcode::LoadGlobal(self.get_or_set_nonlocal_index(name))
        }
    }
}
Copier après la connexion

Il y a plusieurs principes clés en action ici :

  1. Nous jumelons en fonction du contexte actuel. En adhérant à la sémantique Python, nous pouvons considérer Context::Global comme étant au niveau supérieur de n'importe quel module (pas seulement le point d'entrée de votre script), et Context::Local se trouve à l'intérieur de n'importe quel bloc (c'est-à-dire la définition de fonction ou la définition de classe).
  2. On fait désormais la différence entre un index local et un index non local. (Parce que je devenais fou en essayant de déchiffrer à quoi faisait référence l'index 0 à différents endroits, j'ai introduit les entiers typés. LocalIndex et NonlocalIndex fournissent une sécurité de type pour les entiers non signés autrement non typés. Je pourrais écrire à ce sujet à l'avenir !)
  3. Nous pouvons savoir au moment de la compilation du bytecode si une variable locale existe avec un nom donné, et si ce n'est pas le cas, au moment de l'exécution, nous rechercherons une variable globale. Cela témoigne du dynamisme intégré à Python : tant qu'une variable est présente dans la portée globale de ce module au moment de l'exécution d'une fonction, sa valeur peut être résolue au moment de l'exécution. Cependant, cette résolution dynamique s’accompagne d’une baisse de performances. Alors que les recherches de variables locales sont optimisées pour utiliser les index de pile, les recherches globales nécessitent une recherche dans le dictionnaire d'espace de noms global, ce qui est plus lent. Ce dictionnaire est un mappage de noms avec des objets, qui eux-mêmes peuvent vivre sur le tas. Qui aurait cru que le dicton « Pensez globalement, agissez localement ». faisait-il réellement référence aux scopes Python ?

Qu'y a-t-il dans un nom de variable ?

La dernière chose que je vais vous laisser aujourd'hui est un aperçu de la manière dont ces noms de variables sont mappés. Dans l'extrait de code ci-dessous, vous remarquerez que les index locaux se trouvent dans code.varnames et que les index non locaux se trouvent dans code.names. Les deux vivent sur un CodeObject, qui contient les métadonnées d'un bloc de bytecode Python, y compris ses mappages de variables et de noms.

fn compile_function_call(
    &mut self,
    name: &str,
    args: &ParsedArguments)
) -> Result<Bytecode, CompileError> {
    let mut opcodes = vec![];

    // We push the args onto the stack in reverse call order so that we will pop
    // them off in call order.
    for arg in args.args.iter().rev() {
        opcodes.extend(self.compile_expr(arg)?);
    }

    let (_, index) = self.get_local_index(name);

    // TODO how does this know if it is a global or local index? this may not be the right
    // approach for calling a function
    opcodes.push(Opcode::Call(index));

    Ok(opcodes)
}
Copier après la connexion
Copier après la connexion

La différence entre les noms de variables et les noms m'a tourmenté pendant des semaines (CPython appelle ces noms de variables et noms de variables), mais c'est en fait assez simple. varnames contient les noms de variables pour toutes les variables locales dans une portée donnée, et les noms font de même pour toutes les variables non locales.

Une fois que nous avons correctement suivi cela, tout le reste fonctionne. Au moment de l'exécution, la VM voit un LOAD_GLOBAL ou un LOAD_FAST et sait qu'il faut chercher dans le dictionnaire d'espace de noms global ou dans la pile locale, respectivement.

Mon pote ! M. Gutenberg est au téléphone et dit que nous ne pouvons plus tenir les presses.

D'accord ! Bien! Je comprends! Envoyons-le. ?

Quelle est la prochaine étape pour Memphis ?

Chut ! L’imprimerie ne sait pas que j’écris une conclusion, donc je serai bref.

Avec une portée variable et des appels de fonctions dans un endroit solide, je tourne progressivement mon attention vers des fonctionnalités telles que les traces de pile et la prise en charge asynchrone. Si vous avez apprécié cette plongée dans le bytecode ou si vous avez des questions sur la création de votre propre interprète, j'aimerais avoir de vos nouvelles : laissez un commentaire !


Abonnez-vous et économisez [sur rien]

Si vous souhaitez recevoir plus de messages comme celui-ci directement dans votre boîte de réception, vous pouvez vous abonner ici !

Travailler avec moi

J'encadre des ingénieurs logiciels pour qu'ils relèvent les défis techniques et évoluent dans leur carrière dans un environnement favorable, parfois idiot. Si vous êtes intéressé, vous pouvez réserver une séance ici.

Ailleurs

En plus du mentorat, j'écris également sur mon expérience de travail indépendant et d'autisme diagnostiqué tardivement. Moins de code et le même nombre de blagues.

  • Café effet lac, chapitre 2 - From Scratch dot org

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

source:dev.to
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Derniers articles par auteur
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal