Python : Le mécanisme de passed by assignment comparé au passage par référence et par valeur dans .NET

La documentation Python contient cette précision importante :

Souviens-toi que les arguments sont passés par assignment en Python. Puisqu'une assignation crée simplement des références vers des objets, il n'y a pas d'alias entre le nom d'un argument chez l'appelant et l'appelé, et donc pas vraiment de passage par référence en tant que tel.

Comment faut-il interpréter ce mécanisme de passed by assignment dans Python, alors que nous distinguons généralement le passage par valeur du passage par référence ?

argument vs parameter

Sur Stack Overflow, la distinction entre paramètre et argument est clairement établie :

Un paramètre est une variable dans la définition d'une méthode. Lorsqu'une méthode est appelée, les arguments sont les données que l'on transmet aux paramètres de la méthode.

  • PARAMÈTRE → PLACEHOLDER (un marqueur qui appartient à la fonction et est utilisé dans son corps)
  • ARGUMENT → ACTUELLEMENT VALEUR (une valeur effective transmise par l'appelant)

.NET : Passage par valeur et par référence

Dans .NET, les types sont divisés en types valeurs et types références. Par défaut, le passage s'effectue par valeur. Pour utiliser le passage par référence, il faut employer explicitement les mots-clés ref, in ou out.

En C#, les arguments peuvent être transmis aux paramètres soit par valeur, soit par référence. Le passage par référence permet aux membres de fonction, méthodes, propriétés, indexeurs, opérateurs et constructeurs de modifier la valeur des paramètres et de conserver cette modification dans l'environnement appelant. Pour passer un paramètre par référence dans l'intention de modifier la valeur, on utilise les mots-clés ref ou out. Pour passer par référence dans le but d'éviter la copie mais sans modifier la valeur, on utilise le modificateur in.

Passage des paramètres de type référence

Lorsqu'on passe un paramètre de type référence par valeur, il est possible de modifier les données de l'objet référencé, comme la valeur d'un membre de classe. Cependant, il n'est pas possible de modifier la valeur de la référence elle-même : par exemple, on ne peut pas utiliser la même référence pour allouer de la mémoire pour un nouvel objet et le faire persister hors de la méthode. Pour cela, il faut passer le paramètre avec les mots-clés ref ou out. Pour simplifier, les exemples suivants utilisent ref.

Passage des types références par valeur

class TransfertRefParValeur 
{
    static void Modifier(int[] tableau)
    {
        tableau[0] = 888;  // Cette modification affecte l'élément original.
        tableau = new int[5] {-3, -1, -2, -3, -4};   // Cette modification est locale.
        System.Console.WriteLine("Dans la méthode, le premier élément vaut : {0}", tableau[0]);
    }

    static void Main() 
    {
        int[] donnees = {1, 4, 5};
        System.Console.WriteLine("Dans Main, avant l'appel, le premier élément vaut : {0}", donnees[0]);

        Modifier(donnees);
        System.Console.WriteLine("Dans Main, après l'appel, le premier élément vaut : {0}", donnees[0]);
    }
}
/* Résultat :
    Dans Main, avant l'appel, le premier élément vaut : 1
    Dans la méthode, le premier élément vaut : -3
    Dans Main, après l'appel, le premier élément vaut : 888
*/

Dans l'exemple précédent, le tableau donnees, qui est un type référence, est passé à la méthode sans le paramètre ref. Dans ce cas, une copie de la référence pointant vers donnees est transmise à la méthode. Le résultat montre qu'il est possible pour la méthode de modifier le contenu d'un élément du tableau, ici de 1 à 888. Cependant, l'allocation d'un nouveau bloc mémoire avec l'opérateur new à l'intérieur de la méthode Modifier fait que la variable tableau référence un nouveau tableau. Ainsi, toute modification ultérieure n'affectera pas le tableau original donnees créé danss Main. En fait, deux tableaux sont créés dans cet exemple : un dans Main et un dans la méthode Modifier.

Passage des types références par référence

L'exemple suivant est identique au précédent, sauf que le mot-clé ref est ajouté dans l'en-tête de la méthode et dans l'appel. Toute modification effectuée dans la méthode affecte la varible originale du programme appelant.

class TransfertRefParRef 
{
    static void Modifier(ref int[] donnees)
    {
        // Les deux modifications suivantes affectent les variables originales :
        donnees[0] = 888;
        donnees = new int[5] {-3, -1, -2, -3, -4};
        System.Console.WriteLine("Dans la méthode, le premier élément vaut : {0}", donnees[0]);
    }
        
    static void Main() 
    {
        int[] sauvegarde = {1, 4, 5};
        System.Console.WriteLine("Dans Main, avant l'appel, le premier élément vaut : {0}", sauvegarde[0]);

        Modifier(ref sauvegarde);
        System.Console.WriteLine("Dans Main, après l'appel, le premier élément vaut : {0}", sauvegarde[0]);
    }
}
/* Résultat :
    Dans Main, avant l'appel, le premier élément vaut : 1
    Dans la méthode, le premier élément vaut : -3
    Dans Main, après l'appel, le premier élément vaut : -3
*/

Toutes les modifications effectuées dans la méthode affectent le tableau original de Main. En fait, le tableau original est réalloué avec l'opérateur new. Ainsi, après l'appel de la méthode Modifier, toute référence à sauvegarde pointe vers le tableau de cinq éléments créé dans la méthode Modifier.

⚠️ Il ne faut pas confondre le concept de passage par référence avec le concept de types références. Ces deux concepts sont différents. Un paramètre de méthode peut être modifié avec ref qu'il s'agisse d'un type valeur ou d'un type référence. Il n'y a pas de boxing d'un type valeur lorsqu'il est passé par référence.

Passage des paramètres de type valeur

Passer une variable de type valeur à une méthode par valeur signifie transmettre une copie de la variable à la méthode. Toute modification du paramètre effectuée à l'intérieur de la méthode n'affecte pas les données originales stockées dans la variable argument. Si l'on veut que la méthode appelée modifie la valeur de l'argument, il faut le passer par référence avec les mots-clés ref ou out. On peut également utiliser le mot-clé in pour passer un paramètre de valeur par référence en évitant la copie tout en garantissant que la valeur ne sera pas modifiée. Pour simplifier, les exemples suivants utilisent ref.

Passage des types valeurs par valeur

class TransfertValeurParValeur
{
    static void EleverAuCarre(int nombre)
    // Le paramètre nombre est passé par valeur.
    // Les modifications sur nombre n'affecteront pas la valeur originale de nombre.
    {
        nombre *= nombre;
        System.Console.WriteLine("La valeur dans la méthode : {0}", nombre);
    }
    static void Main()
    {
        int valeur = 5;
        System.Console.WriteLine("La valeur avant l'appel : {0}", valeur);

        EleverAuCarre(valeur);  // Passage de la variable par valeur.
        System.Console.WriteLine("La valeur après l'appel : {0}", valeur);

        // Garder la fenêtre console ouverte en mode débogage.
        System.Console.WriteLine("Appuyez sur une touche pour quitter.");
        System.Console.ReadKey();
    }
}
/* Résultat :
    La valeur avant l'appel : 5
    La valeur dans la méthode : 25
    La valeur après l'appel : 5
*/

La variable valeur est un type valeur. Elle contient ses données, la valeur 5. Lorsque EleverAuCarre est invoqué, le contenu de valeur est copié dans le paramètre nombre, qui est élevé au carré dans la méthode. Dans Main, cependant, la valeur de valeur reste la même après l'appel de EleverAuCarre. La modification effectuée dans la méthode n'affecte que la variable locale nombre.

Passage des types valeurs par référence

L'exemple suivant est identique au précédent, sauf que l'argument est passé comme paramètre ref. La valeur de l'argument sous-jacent, valeur, est modifiée lorsque nombre est changé dans la méthode.

class TransfertValeurParRef
{
    static void EleverAuCarre(ref int nombre)
    // Le paramètre nombre est passé par référence.
    // Les modifications sur nombre affecteront la valeur originale de nombre.
    {
        nombre *= nombre;
        System.Console.WriteLine("La valeur dans la méthode : {0}", nombre);
    }
    static void Main()
    {
        int valeur = 5;
        System.Console.WriteLine("La valeur avant l'appel : {0}", valeur);

        EleverAuCarre(ref valeur);  // Passage de la variable par référence.
        System.Console.WriteLine("La valeur après l'appel : {0}", valeur);

        // Garder la fenêtre console ouverte en mode débogage.
        System.Console.WriteLine("Appuyez sur une touche pour quitter.");
        System.Console.WriteLine(" ");
        System.Console.ReadKey();
    }
}
/* Résultat :
    La valeur avant l'appel : 5
    La valeur dans la méthode : 25
    La valeur après l'appel : 25
*/

Dans cet exemple, ce n'est pas la valeur de valeur qui est passée ; c'est plutôt une référence vers valeur qui est passée. Le paramètre nombre n'est pas un int ; c'est une référence vers un int</strong>, en l'occurrence une référence vers valeur. Par conséquent, lorsque nombre est élevé au carré dans la méthode, ce qui est effectivement mis au carré est ce à quoi nombre fait référence, c'est-à-dire valeur.

Illustration du passage par référence

J'ai déjà soumis un problème sur Github avec quelques illustrations décrivant le passage par référence.

Python : passed by assignment

Après ces explications, comment comprendre le mécanisme de passed by assignment en Python ? Python ne distingue pas les types en types valeurs et types références comme le fait .NET. En Python, toutes les valeurs sont des objets,分类és en deux catégories : les objets mutables et les objets immuables :

  • Types immuablesfloat, int, str, tuple, bool, etc.
  • Types mutableslist, dict, set, etc.

En Python, tous les types de données sont des objets, et lors du passage de paramètres, ce sont les références vers les objets qui sont transmises. Cela ressemble au passage par valeur des types références dans .NET.

Pour les types immuables :

nombre = 9

def modifierValeur(valeur: int):
    # Le type int est immuable : changer la valeur de valeur fait pointer vers le nouvel objet 8
    # Cela ne modifie pas la variable nombre originale
    valeur = 8

print(nombre)
modifierValeur(nombre)
print(nombre)

"""
Résultat :
9
9
"""

Pour les types mutables :

elements = [9, 8, 7]

def creerNouvelleListe(liste: list):
    # liste + [0] crée un nouvel objet list, cela change seulement la référence de la variable locale
    # Attention : en Python, liste = liste + [0] n'est pas équivalent à liste += [0]
    # L'opérateur += étend la liste originale, comme la méthode extend
    liste = liste + [0]

def modifierListe(liste: list):
    # Ajout d'un élément à la fin de la liste
    # Comme liste et elements pointent vers le même objet, elements sera aussi modifié
    liste.append(0)

print(elements)
creerNouvelleListe(elements)
print(elements)
modifierListe(elements)
print(elements)

"""
Résultat :
[9, 8, 7]
[9, 8, 7]
[9, 8, 7, 0]
"""

Synthèse

Le passage par valeur implique la création d'une copie de la valeur de la variable. Toutes les opérations sont effectuées sur cette copie, sans affecter les données originales.

⚠️ Pour les types valeurs dans .NET, les données originales sont la valeur même de la variable. Pour les types références dans .NET, les données originales sont aussi la valeur de la varible, mais cette valeur représente une adresse mémoire.

Le diagramme ci-dessous illustre le passage par valeur d'un type référence : les variables pointeur2 et objet ont des adresses mémoire différentes, mais stockent la même valeur :

Le passage par référence équivaut à créer un alias pour la variable. Toutes les opérations s'effectuent directement sur la variable originale. Comme le montre le diagramme suivant, lors du passage par référence d'un type référence, alias et objet partagent la même adresse mémoire et le même contenu :Le même principe s'applique au passage par référence des types valeurs et au passage par valeur des types valeurs :Pour aller plus loin

Passing Parameters (C# Programming Guide)

Method Parameters (C# Reference)

The address-of operator

Publié le 30 septembre à 13h43