Contexte et bonnes pratiques – Real Python

By | août 10, 2020

Formation gratuite Python

Après vous être familiarisé avec Python, vous remarquerez peut-être des cas dans lesquels vos fonctions ne modifient pas les arguments en place comme vous vous en doutez, surtout si vous êtes familier avec d’autres langages de programmation. Certains langages traitent les arguments de fonction comme références aux variables existantes, appelées passer par référence. D'autres langues les traitent comme valeurs indépendantes, une approche connue sous le nom de passer par valeur.

Si vous êtes un programmeur Python intermédiaire qui souhaite comprendre la manière particulière de Python de gérer les arguments de fonction, alors ce didacticiel est pour vous. Vous implémenterez des cas d'utilisation réels de constructions passe-par-référence en Python et apprendrez plusieurs bonnes pratiques pour éviter les pièges avec vos arguments de fonction.

Dans ce didacticiel, vous apprendrez:

  • Ce que cela signifie passer par référence et pourquoi vous souhaitez le faire
  • Comment le passage par référence diffère des deux passant par valeur et L'approche unique de Python
  • Comment arguments de fonction se comporter en Python
  • Comment vous pouvez utiliser certains types mutables passer par référence en Python
  • Qu'est-ce que les meilleures pratiques sont pour la réplication passe par référence en Python

Définition du passage par référence

Avant de vous plonger dans les détails techniques du passage par référence, il est utile d'examiner de plus près le terme lui-même en le décomposant en composants:

  • Passer signifie fournir un argument à une fonction.
  • Par référence signifie que l'argument que vous passez à la fonction est un référence à une variable qui existe déjà en mémoire plutôt qu'à une copie indépendante de cette variable.

Étant donné que vous attribuez à la fonction une référence à une variable existante, toutes les opérations effectuées sur cette référence affecteront directement la variable à laquelle elle se réfère. Voyons quelques exemples de la manière dont cela fonctionne dans la pratique.

Ci-dessous, vous verrez comment passer des variables par référence en C #. Notez l'utilisation du réf mot-clé dans les lignes en surbrillance:

en utilisant Système;

// La source:
// https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/passing-parameters
classe Programme

    statique néant Principale(chaîne[] args)
    
        int arg;

        // Passage par référence.
        // La valeur de arg dans Main est modifiée.
        arg = 4;
        squareRef(réf arg);
        Console.WriteLine(arg);
        // Sortie: 16
    

    statique néant squareRef(réf int refParamètre)
    
        refParamètre * = refParamètre;
    

Comme vous pouvez le voir, le refParamètre de squareRef () doit être déclaré avec le réf mot-clé, et vous devez également utiliser le mot-clé lors de l'appel de la fonction. Ensuite, l'argument sera passé par référence et pourra être modifié sur place.

Python n'a pas réf mot-clé ou quelque chose d'équivalent. Si vous essayez de reproduire l'exemple ci-dessus aussi fidèlement que possible en Python, vous obtiendrez des résultats différents:

>>>

>>> def principale():
...     arg = 4
...     carré(arg)
...     impression(arg)
...
>>> def carré(n):
...     n * = n
...
>>> principale()
4

Dans ce cas, le arg la variable est ne pas modifié en place. Il semble que Python traite votre argument fourni comme une valeur autonome plutôt que comme une référence à une variable existante. Cela signifie-t-il que Python passe les arguments par valeur plutôt que par référence?

Pas assez. Python ne passe d'arguments ni par référence ni par valeur, mais par cession. Ci-dessous, vous explorerez rapidement les détails du passage par valeur et du passage par référence avant d'examiner de plus près l'approche de Python. Ensuite, vous passerez en revue quelques bonnes pratiques pour obtenir l'équivalent du passage par référence en Python.

Contraste passe par référence et passe par valeur

Lorsque vous passez des arguments de fonction par référence, ces arguments ne sont que des références à des valeurs existantes. En revanche, lorsque vous passez des arguments par valeur, ces arguments deviennent des copies indépendantes des valeurs d'origine.

Revenons à l'exemple C #, cette fois sans utiliser le réf mot-clé. Cela amènera le programme à utiliser le comportement par défaut de passer par valeur:

en utilisant Système;

// La source:
// https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/passing-parameters
classe Programme

    statique néant Principale(chaîne[] args)
    
        int arg;

        // Passage par valeur.
        // La valeur de arg dans Main n'est pas modifiée.
        arg = 4;
        squareVal(arg);
        Console.WriteLine(arg);
        // Sortie: 4
    

    statique néant squareVal(int valParamètre)
    
        valParamètre * = valParamètre;
    

Ici, vous pouvez voir que squareVal () ne modifie pas la variable d'origine. Plutôt, valParamètre est une copie indépendante de la variable d'origine arg. Bien que cela corresponde au comportement que vous verriez en Python, rappelez-vous que Python ne passe pas exactement par valeur. Prouvons-le.

Intégré à Python id () renvoie un entier représentant l'adresse mémoire de l'objet souhaité. En utilisant id (), vous pouvez vérifier les assertions suivantes:

  1. Les arguments de fonction font initialement référence à la même adresse que leurs variables d'origine.
  2. La réattribution de l'argument dans la fonction lui donne une nouvelle adresse tandis que la variable d'origine reste inchangée.

Dans l'exemple ci-dessous, notez que l'adresse de X correspond initialement à celui de n mais change après la réaffectation, tandis que l'adresse de n ne change jamais:

>>>

>>> def principale():
...     n = 9001
...     impression(F"Adresse initiale de n: id(n)")
...     incrément(n)
...     impression(F"Adresse finale de n: id(n)")
...
>>> def incrément(X):
...     impression(F"Adresse initiale de x: id(X)")
...     X + = 1
...     impression(F"Adresse finale de x: id(X)")
...
>>> principale()
Adresse initiale du n: 140562586057840
Adresse initiale de x: 140562586057840
        Adresse finale de x: 140562586057968
        Adresse finale du n: 140562586057840

Le fait que les adresses initiales de n et X sont les mêmes lorsque vous invoquez incrément() prouve que le X l'argument n'est pas passé par valeur. Autrement, n et X aurait des adresses mémoire distinctes.

Avant d'apprendre les détails sur la manière dont Python gère les arguments, examinons quelques cas d'utilisation pratiques du passage par référence.

Utilisation de constructions passe par référence

Le passage de variables par référence est l'une des nombreuses stratégies que vous pouvez utiliser pour implémenter certains modèles de programmation. Bien que cela soit rarement nécessaire, le passage par référence peut être un outil utile.

Dans cette section, vous examinerez trois des modèles les plus courants pour lesquels le passage par référence est une approche pratique. Vous verrez ensuite comment mettre en œuvre chacun de ces modèles avec Python.

Éviter les objets en double

Comme vous l’avez vu, la transmission d’une variable par valeur entraînera la création et le stockage d’une copie de cette valeur en mémoire. Dans les langages qui passent par défaut par valeur, vous pouvez trouver des avantages en termes de performances à passer la variable par référence à la place, en particulier lorsque la variable contient beaucoup de données. Cela sera plus apparent lorsque votre code s'exécute sur des machines à ressources limitées.

En Python, cependant, ce n'est jamais un problème. Vous verrez pourquoi dans la section suivante.

Renvoyer plusieurs valeurs

L'une des applications les plus courantes du passage par référence est de créer une fonction qui modifie la valeur des paramètres de référence tout en renvoyant une valeur distincte. Vous pouvez modifier votre exemple C # pass-by-reference pour illustrer cette technique:

en utilisant Système;

classe Programme

    statique néant Principale(chaîne[] args)
    
        int compteur = 0;

        // Passage par référence.
        // La valeur du compteur dans Main est modifiée.
        Console.WriteLine(saluer(«Alice», réf compteur));
        Console.WriteLine("Le compteur est 0", compteur);
        Console.WriteLine(saluer("Bob", réf compteur));
        Console.WriteLine("Le compteur est 0", compteur);
        // Production:
        // Salut, Alice!
        // Le compteur est 1
        // Salut Bob!
        // Le compteur est 2
    

    statique chaîne saluer(chaîne Nom, réf int compteur)
    
        chaîne salutation = "Salut, " + Nom + "!";
        compteur++;
        revenir salutation;
    

Dans l'exemple ci-dessus, saluer() renvoie une chaîne de salutation et modifie également la valeur de compteur. Maintenant, essayez de reproduire cela aussi fidèlement que possible en Python:

>>>

>>> def principale():
...     compteur = 0
...     impression(saluer(«Alice», compteur))
...     impression(F"Le compteur est compteur")
...     impression(saluer("Bob", compteur))
...     impression(F"Le compteur est compteur")
...
>>> def saluer(Nom, compteur):
...     compteur + = 1
...     revenir F"Salut, Nom! "
...
>>> principale()
Salut Alice!
Le compteur est 0
Salut Bob!
Le compteur est 0

compteur n'est pas incrémenté dans l'exemple ci-dessus car, comme vous l'avez appris précédemment, Python n'a aucun moyen de transmettre des valeurs par référence. Alors, comment pouvez-vous obtenir le même résultat qu'avec C #?

Essentiellement, les paramètres de référence en C # permettent à la fonction non seulement de renvoyer une valeur, mais également d'opérer sur des paramètres supplémentaires. Cela équivaut à renvoyer plusieurs valeurs!

Heureusement, Python prend déjà en charge le renvoi de plusieurs valeurs. À proprement parler, une fonction Python qui renvoie plusieurs valeurs renvoie en fait un tuple contenant chaque valeur:

>>>

>>> def retour_ multiple():
...     revenir 1, 2
...
>>> t = retour_ multiple()
>>> t  # Un tuple
(1, 2)

>>> # Vous pouvez décompresser le tuple en deux variables:
>>> X, y = retour_ multiple()
>>> X
1
>>> y
2

Comme vous pouvez le voir, pour renvoyer plusieurs valeurs, vous pouvez simplement utiliser le revenir mot-clé suivi de valeurs ou de variables séparées par des virgules.

Armé de cette technique, vous pouvez modifier la déclaration de retour dans saluer() à partir de votre code Python précédent pour renvoyer à la fois un message d'accueil et un compteur:

>>>

>>> def principale():
...     compteur = 0
...     impression(saluer(«Alice», compteur))
...     impression(F"Le compteur est compteur")
...     impression(saluer("Bob", compteur))
...     impression(F"Le compteur est compteur")
...
>>> def saluer(Nom, compteur):
...     revenir F"Salut, Nom! ", compteur + 1
...
>>> principale()
('Salut, Alice!', 1)
Le compteur est 0
('Salut, Bob!', 1)
Le compteur est 0

Cela ne semble toujours pas correct. Bien que saluer() renvoie maintenant plusieurs valeurs, elles sont imprimées en tant que tuple, ce n’est pas votre intention. De plus, l'original compteur la variable reste à 0.

Pour nettoyer votre sortie et obtenir les résultats souhaités, vous devrez réaffecter votre compteur variable à chaque appel à saluer():

>>>

>>> def principale():
...     compteur = 0
...     salutation, compteur = saluer(«Alice», compteur)
...     impression(F"salutation nLe compteur est compteur")
...     salutation, compteur = saluer("Bob", compteur)
...     impression(F"salutation nLe compteur est compteur")
...
>>> def saluer(Nom, compteur):
...     revenir F"Salut, Nom! ", compteur + 1
...
>>> principale()
Salut Alice!
Le compteur est 1
Salut Bob!
Le compteur est 2

Maintenant, après avoir réaffecté chaque variable avec un appel à saluer(), vous pouvez voir les résultats souhaités!

L'attribution de valeurs de retour à des variables est le meilleur moyen d'obtenir les mêmes résultats que le passage par référence en Python. Vous découvrirez pourquoi, ainsi que quelques méthodes supplémentaires, dans la section sur les bonnes pratiques.

Création de fonctions de retour multiple conditionnelles

Il s'agit d'un cas d'utilisation spécifique de retour de plusieurs valeurs dans lequel la fonction peut être utilisée dans une instruction conditionnelle et a des effets secondaires supplémentaires tels que la modification d'une variable externe qui a été transmise en tant qu'argument.

Considérez la fonction standard Int32.TryParse en C #, qui retourne un booléen et opère sur une référence à un argument entier en même temps:

Publique statique booléen TryParse (chaîne s, en dehors int résultat);

Cette fonction tente de convertir un chaîne en un entier signé 32 bits à l'aide de en dehors mot-clé. Il y a deux résultats possibles:

  1. Si l'analyse réussit, alors le paramètre de sortie sera défini sur l'entier résultant et la fonction retournera vrai.
  2. Si l'analyse échoue, le paramètre de sortie sera réglé sur 0, et la fonction retournera faux.

Vous pouvez le voir en pratique dans l'exemple suivant, qui tente de convertir un certain nombre de chaînes différentes:

en utilisant Système;

// La source:
// https://docs.microsoft.com/en-us/dotnet/api/system.int32.tryparse?view=netcore-3.1#System_Int32_TryParse_System_String_System_Int32__
Publique classe Exemple {
    Publique statique néant Principale() 
        Chaîne[] valeurs =  nul, «160519», «9432,0», «16,667»,
                            "-322", "+4302", "(100);", "01FA" ;
        pour chaque (var valeur dans valeurs) 
            int nombre;

            si (Int32.TryParse(valeur, en dehors nombre)) 
                Console.WriteLine("Converti '0' en 1.", valeur, nombre);
            
            autre 
                Console.WriteLine("La tentative de conversion de" 0 "a échoué.",
                                   valeur ?? "");
            
        
    
}

Le code ci-dessus, qui tente de convertir des chaînes formatées différemment en entiers via TryParse (), génère les éléments suivants:

Tentative de conversion de '' échoué.
Converti «160519» en 160519.
La tentative de conversion de «9432.0» a échoué.
La tentative de conversion de «16 667» a échoué.
Converti '-322' en -322.
Converti '+4302' en 4302.
Tentative de conversion de '(100);' échoué.
La tentative de conversion de «01FA» a échoué.

Pour implémenter une fonction similaire en Python, vous pouvez utiliser plusieurs valeurs de retour comme vous l'avez vu précédemment:

def tryparse(chaîne, base=dix):
    essayer:
        revenir Vrai, int(chaîne, base=base)
    sauf ValueError:
        revenir Faux, Aucun

Ce tryparse () renvoie deux valeurs. La première valeur indique si la conversion a réussi et la seconde contient le résultat (ou Aucun, en cas de défaillance).

Cependant, l'utilisation de cette fonction est un peu maladroite car vous devez décompresser les valeurs de retour à chaque appel. Cela signifie que vous ne pouvez pas utiliser la fonction dans un si déclaration:

>>>

>>> Succès, résultat = tryparse("123")
>>> Succès
Vrai
>>> résultat
123

>>> # Nous pouvons faire fonctionner le chèque
>>> # en accédant au premier élément du tuple retourné,
>>> # mais il n'y a aucun moyen de réaffecter le deuxième élément à `result`:
>>> si tryparse(«456»)[[[[0]:
...     impression(résultat)
...
123

Même si cela fonctionne généralement en renvoyant plusieurs valeurs, tryparse () ne peut pas être utilisé dans un contrôle de condition. Cela signifie que vous avez encore du travail à faire.

Vous pouvez tirer parti de la flexibilité de Python et simplifier la fonction pour renvoyer une valeur unique de différents types selon que la conversion réussit:

def tryparse(chaîne, base=dix):
    essayer:
        revenir int(chaîne, base=base)
    sauf ValueError:
        revenir Aucun

Avec la possibilité pour les fonctions Python de renvoyer différents types de données, vous pouvez désormais utiliser cette fonction dans une instruction conditionnelle. Mais comment? N'auriez-vous pas besoin d'appeler la fonction d'abord, d'attribuer sa valeur de retour, puis de vérifier la valeur elle-même?

En tirant parti de la flexibilité de Python dans les types d’objet, ainsi que des nouvelles expressions d’affectation dans Python 3.8, vous pouvez appeler cette fonction simplifiée dans un conditionnel si déclaration et obtenir la valeur de retour si la vérification réussit:

>>>

>>> si (n : = tryparse("123")) est ne pas Aucun:
...     impression(n)
...
123
>>> si (n : = tryparse("abc")) est Aucun:
...     impression(n)
...
Aucun

>>> # Vous pouvez même faire de l'arithmétique!
>>> dix * tryparse("dix")
100

>>> # Toutes les fonctionnalités de int () sont disponibles:
>>> dix * tryparse("0a", base=16)
100

>>> # Vous pouvez également intégrer la vérification dans l'expression arithmétique!
>>> dix * (n si (n : = tryparse("123")) est ne pas Aucun autre 1)
1230
>>> dix * (n si (n : = tryparse("abc")) est ne pas Aucun autre 1)
dix

Hou la la! Cette version Python de tryparse () est encore plus puissant que la version C #, vous permettant de l'utiliser dans des instructions conditionnelles et dans des expressions arithmétiques.

Avec un peu d’ingéniosité, vous avez répliqué un modèle de passage par référence spécifique et utile sans passer d’arguments par référence. En fait, vous êtes encore affectation de valeurs de retour lors de l'utilisation de l'opérateur d'expression d'affectation (: =) et en utilisant la valeur de retour directement dans les expressions Python.

Jusqu'à présent, vous avez appris ce que signifie passer par référence, en quoi il diffère du passage par valeur et en quoi l'approche de Python diffère des deux. Vous êtes maintenant prêt à examiner de plus près comment Python gère les arguments de fonction!

Passer des arguments en Python

Python passe les arguments par affectation. Autrement dit, lorsque vous appelez une fonction Python, chaque argument de fonction devient une variable à laquelle la valeur transmise est affectée.

Par conséquent, vous pouvez apprendre des détails importants sur la façon dont Python gère les arguments de fonction en comprenant comment le mécanisme d'affectation lui-même fonctionne, même en dehors des fonctions.

Comprendre l'attribution en Python

La référence du langage Python pour les instructions d'affectation fournit les détails suivants:

  • Si la cible d'affectation est un identificateur ou un nom de variable, ce nom est lié à l'objet. Par exemple, dans x = 2, X est le nom et 2 est l'objet.
  • Si le nom est déjà lié à un objet distinct, il est alors lié au nouvel objet. Par exemple, si X est déjà 2 et vous émettez x = 3, puis le nom de la variable X est lié à 3.

Tous les objets Python sont implémentés dans une structure particulière. L'une des propriétés de cette structure est un compteur qui enregistre le nombre de noms liés à cet objet.

Tenons-nous-en à la x = 2 exemple et examinez ce qui se passe lorsque vous attribuez une valeur à une nouvelle variable:

  1. Si un objet représentant la valeur 2 existe déjà, puis il est récupéré. Sinon, il est créé.
  2. Le compteur de référence de cet objet est incrémenté.
  3. Une entrée est ajoutée dans l'espace de noms courant pour lier l'identifiant X à l'objet représentant 2.
    Cette entrée est en fait une paire clé-valeur stockée dans un dictionnaire! Une représentation de ce dictionnaire est renvoyée par des locaux() ou globaux ().

Maintenant, voici ce qui se passe si vous réaffectez X à une valeur différente:

  1. Le compteur de référence de l'objet représentant 2 est décrémenté.
  2. Le compteur de référence de l'objet qui représente la nouvelle valeur est incrémenté.
  3. Le dictionnaire de l'espace de noms actuel est mis à jour pour relier X à l'objet représentant la nouvelle valeur.

Python vous permet d'obtenir les nombres de références pour des valeurs arbitraires avec la fonction sys.getrefcount (). Vous pouvez l'utiliser pour illustrer comment l'affectation augmente et diminue ces compteurs de référence. Notez que l'interpréteur interactif utilise un comportement qui donnera des résultats différents, vous devez donc exécuter le code suivant à partir d'un fichier:

de sys importer getrefcount

impression("--- Avant l'affectation ---")
impression(F"Références à value_1: getrefcount("valeur_1")")
impression(F"Références à value_2: getrefcount("valeur_2")")
X = "value_1"
impression("--- Après affectation ---")
impression(F"Références à value_1: getrefcount("valeur_1")")
impression(F"Références à value_2: getrefcount("valeur_2")")
X = "valeur_2"
impression("--- Après la réaffectation ---")
impression(F"Références à value_1: getrefcount("valeur_1")")
impression(F"Références à value_2: getrefcount("valeur_2")")

Ce script affichera le nombre de références pour chaque valeur avant l'affectation, après l'affectation et après la réaffectation:

--- Avant l'affectation ---
Références à valeur_1: 3
Références à value_2: 3
--- Après affectation ---
Références à valeur_1: 4
Références à value_2: 3
--- Après la réaffectation ---
Références à valeur_1: 3
Références à value_2: 4

Ces résultats illustrent la relation entre les identifiants (noms de variables) et les objets Python qui représentent des valeurs distinctes. Lorsque vous affectez plusieurs variables à la même valeur, Python incrémente le compteur de référence pour l'objet existant et met à jour l'espace de noms actuel plutôt que de créer des objets en double en mémoire.

Dans la section suivante, vous partirez de votre compréhension actuelle des opérations d'affectation en explorant la manière dont Python gère les arguments de fonction.

Exploration des arguments de fonction

Les arguments de fonction en Python sont variables locales. Qu'est-ce que ça veut dire? Local est l’une des portées de Python. Ces étendues sont représentées par les dictionnaires d'espaces de noms mentionnés dans la section précédente. Vous pouvez utiliser des locaux() et globaux () pour récupérer respectivement les dictionnaires d'espaces de noms locaux et globaux.

Lors de l'exécution, chaque fonction a son propre espace de noms local:

>>>

>>> def show_locals():
...     mon_local = Vrai
...     impression(des locaux())
...
>>> show_locals()
'my_local': True

En utilisant des locaux(), vous pouvez démontrer que les arguments de fonction deviennent des variables régulières dans l’espace de noms local de la fonction. Ajoutons un argument, mon_arg, à la fonction:

>>>

>>> def show_locals(mon_arg):
...     mon_local = Vrai
...     impression(des locaux())
...
>>> show_locals("valeur_arg")
'my_arg': 'arg_value', 'my_local': True

Vous pouvez aussi utiliser sys.getrefcount () pour montrer comment les arguments de fonction incrémentent le compteur de référence pour un objet:

>>>

>>> de sys importer getrefcount

>>> def show_refcount(mon_arg):
...     revenir getrefcount(mon_arg)
...
>>> getrefcount("ma_valeur")
3
>>> show_refcount("ma_valeur")
5

Le script ci-dessus génère des nombres de références pour "ma_valeur" d'abord à l'extérieur, puis à l'intérieur show_refcount (), montrant une augmentation du nombre de références non pas d'un, mais de deux!

En effet, en plus de show_refcount () lui-même, l'appel à sys.getrefcount () à l'intérieur show_refcount () reçoit aussi mon_arg comme argument. Cet endroit mon_arg dans l'espace de noms local pour sys.getrefcount (), en ajoutant une référence supplémentaire à "ma_valeur".

En examinant les espaces de noms et le nombre de références à l'intérieur des fonctions, vous pouvez voir que les arguments de fonction fonctionnent exactement comme les affectations: Python crée des liaisons dans l'espace de noms local de la fonction entre les identificateurs et les objets Python qui représentent les valeurs des arguments. Chacune de ces liaisons incrémente le compteur de référence de l’objet.

Vous pouvez maintenant voir comment Python transmet les arguments par affectation!

Réplication passe par référence avec Python

Après avoir examiné les espaces de noms dans la section précédente, vous vous demandez peut-être pourquoi global n'a pas été mentionné comme un moyen de modifier des variables comme si elles étaient passées par référence:

>>>

>>> def carré():
...     # Non recommandé!
...     global n
...     n * = n
...
>>> n = 4
>>> carré()
>>> n
16

En utilisant le global déclaration enlève généralement à la clarté de votre code. Cela peut créer un certain nombre de problèmes, notamment les suivants:

  • Variables gratuites, apparemment sans rapport avec quoi que ce soit
  • Fonctions sans arguments explicites pour lesdites variables
  • Fonctions qui ne peuvent pas être utilisées de manière générique avec d'autres variables ou arguments car elles reposent sur une seule variable globale
  • Manque de sécurité des threads lors de l'utilisation de variables globales

Comparez l'exemple précédent avec le suivant, qui renvoie explicitement une valeur:

>>>

>>> def carré(n):
...     revenir n * n
...
>>> carré(4)
16

Bien mieux! Vous évitez tous les problèmes potentiels avec les variables globales et en exigeant un argument, vous clarifiez votre fonction.

Bien qu'il ne soit ni un langage pass-by-reference ni un langage pass-by-value, Python ne souffre d'aucun défaut à cet égard. Sa flexibilité fait plus que relever le défi.

Meilleure pratique: retour et réaffectation

Vous avez déjà abordé le renvoi des valeurs de la fonction et leur réaffectation à une variable. Pour les fonctions qui opèrent sur une seule valeur, renvoyer la valeur est beaucoup plus clair que d'utiliser une référence. De plus, étant donné que Python utilise déjà des pointeurs dans les coulisses, il n'y aurait aucun avantage supplémentaire en termes de performances, même s'il était capable de passer des arguments par référence.

Essayez d'écrire des fonctions à usage unique qui renvoient une valeur, puis (ré) affectez cette valeur à des variables, comme dans l'exemple suivant:

def carré(n):
    # Acceptez un argument, renvoyez une valeur.
    revenir n * n

X = 4
...
# Plus tard, réaffectez la valeur de retour:
X = carré(X)

Le renvoi et l'attribution de valeurs rendent également votre intention explicite et votre code plus facile à comprendre et à tester.

Pour les fonctions qui opèrent sur plusieurs valeurs, vous avez déjà vu que Python est capable de renvoyer un tuple de valeurs. Vous avez même dépassé l'élégance d'Int32.TryParse () en C # grâce à la flexibilité de Python!

Si vous avez besoin d'opérer sur plusieurs valeurs, vous pouvez écrire des fonctions à usage unique qui renvoient plusieurs valeurs, puis (ré) affecter ces valeurs à des variables. Voici un exemple:

def saluer(Nom, compteur):
    # Renvoie plusieurs valeurs
    revenir F"Salut, Nom! ", compteur + 1

compteur = 0
...
# Plus tard, réaffectez chaque valeur de retour en décompressant.
salutation, compteur = saluer(«Alice», compteur)

Lorsque vous appelez une fonction qui renvoie plusieurs valeurs, vous pouvez affecter plusieurs variables en même temps.

Meilleure pratique: utiliser des attributs d'objet

Les attributs d'objet ont leur propre place dans la stratégie d'attribution de Python. La référence du langage Python pour les instructions d’affectation indique que si la cible est un attribut d’objet prenant en charge l’affectation, l’objet sera alors invité à effectuer l’affectation sur cet attribut. Si vous passez l'objet en tant qu'argument à une fonction, ses attributs peuvent être modifiés sur place.

Écrivez des fonctions qui acceptent des objets avec des attributs, puis opèrent directement sur ces attributs, comme dans l'exemple suivant:

>>>

>>> # Pour les besoins de cet exemple, utilisons SimpleNamespace.
>>> de les types importer SimpleNamespace

>>> # SimpleNamespace nous permet de définir des attributs arbitraires.
>>> # C'est un remplacement explicite et pratique pour "class X: pass".
>>> ns = SimpleNamespace()

>>> # Définissez une fonction pour agir sur l'attribut d'un objet.
>>> def carré(exemple):
...     exemple.n * = exemple.n
...
>>> ns.n = 4
>>> carré(ns)
>>> ns.n
16

Notez que carré() doit être écrit pour opérer directement sur un attribut, qui sera modifié sans qu'il soit nécessaire de réaffecter une valeur de retour.

Il est bon de répéter que vous devez vous assurer que l'attribut prend en charge l'attribution! Voici le même exemple avec namedtuple, dont les attributs sont en lecture seule:

>>>

>>> de collections importer namedtuple
>>> NS = namedtuple("NS", "n")
>>> def carré(exemple):
...     exemple.n * = exemple.n
...
>>> ns = NS(4)
>>> ns.n
4
>>> carré(ns)
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
  
  
  
  Fichier "", ligne 2, dans carré
AttributeError: impossible de définir l'attribut

Les tentatives de modification des attributs qui n'autorisent pas la modification entraînent un AttributeError.

De plus, vous devez être attentif aux attributs de classe. Ils resteront inchangés et un attribut d'instance sera créé et modifié:

>>>

>>> classe NS:
...     n = 4
...
>>> ns = NS()
>>> def carré(exemple):
...     exemple.n * = exemple.n
...
>>> ns.n
4
>>> carré(ns)
>>> # L'attribut d'instance est modifié.
>>> ns.n
16
>>> # L'attribut de classe reste inchangé.
>>> NS.n
4

Étant donné que les attributs de classe restent inchangés lorsqu'ils sont modifiés via une instance de classe, vous devrez vous rappeler de référencer l'attribut d'instance.

Meilleure pratique: utiliser des dictionnaires et des listes

Les dictionnaires en Python sont un type d'objet différent de tous les autres types intégrés. Ils sont appelés types de mappage. La documentation de Python sur les types de mappage fournit un aperçu du terme:

Un objet de mappage mappe des valeurs hachables à des objets arbitraires. Les mappages sont des objets mutables. Il n'existe actuellement qu'un seul type de mappage standard, le dictionnaire. (La source)

Ce didacticiel ne décrit pas comment implémenter un type de mappage personnalisé, mais vous pouvez répliquer passe par référence à l'aide de l'humble dictionnaire. Voici un exemple utilisant une fonction qui agit directement sur les éléments du dictionnaire:

>>>

>>> # Les dictionnaires sont des types de mappage.
>>> mt = "n": 4
>>> # Définir une fonction à utiliser sur une touche:
>>> def carré(num_dict):
...     num_dict[[[["n"] * = num_dict[[[["n"]
...
>>> carré(mt)
>>> mt
'n': 16

Étant donné que vous réaffectez une valeur à une clé de dictionnaire, l'utilisation d'éléments de dictionnaire reste une forme d'attribution. Avec les dictionnaires, vous obtenez l'avantage supplémentaire d'accéder à la valeur modifiée via le même objet de dictionnaire.

Bien que les listes ne soient pas des types de mappage, vous pouvez les utiliser de la même manière que les dictionnaires en raison de deux caractéristiques importantes: indice et mutabilité. Ces caractéristiques méritent un peu plus d’explications, mais examinons d’abord les meilleures pratiques pour imiter le passage par référence à l’aide de listes Python.

Pour répliquer passage par référence à l'aide de listes, écrivez une fonction qui opère directement sur les éléments de liste:

>>>

>>> # Les listes sont à la fois indexables et mutables.
>>> sm = [[[[4]
>>> # Définissez une fonction pour opérer sur un index:
>>> def carré(num_list):
...     num_list[[[[0] * = num_list[[[[0]
...
>>> carré(sm)
>>> sm
[16]

Étant donné que vous réaffectez une valeur à un élément de la liste, agir sur des éléments de liste est toujours une forme d’attribution. À l'instar des dictionnaires, les listes vous permettent d'accéder à la valeur modifiée via le même objet de liste.

Explorons maintenant l'indexation. Un objet est indice lorsqu'un sous-ensemble de sa structure est accessible par des positions d'index:

>>>

>>> indice = [[[[0, 1, 2]  # Une liste
>>> indice[[[[0]
0
>>> indice = (0, 1, 2)  # Un tuple
>>> indice[[[[0]
0
>>> indice = "012"  # Un string
>>> indice[[[[0]
«0»
>>> not_subscriptable = 0, 1, 2  # Un ensemble
>>> not_subscriptable[[[[0]
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
Erreur-type: L'objet 'set' n'est pas en indice

Les listes, les tuples et les chaînes sont en indice, mais les ensembles ne le sont pas. Tenter d'accéder à un élément d'un objet qui n'est pas en indice provoquera un Erreur-type.

La mutabilité est un sujet plus large nécessitant une exploration et une documentation supplémentaires. Pour faire court, un objet est mutable si sa structure peut être modifiée sur place plutôt que d'exiger une réaffectation:

>>>

>>> mutable = [[[[0, 1, 2]  # Une liste
>>> mutable[[[[0] = "X"
>>> mutable
['x', 1, 2]

>>> not_mutable = (0, 1, 2)  # Un tuple
>>> not_mutable[[[[0] = "X"
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
Erreur-type: L'objet 'tuple' ne prend pas en charge l'attribution d'éléments

>>> not_mutable = "012"  # Un string
>>> not_mutable[[[[0] = "X"
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
Erreur-type: L'objet 'str' ne prend pas en charge l'attribution d'éléments

>>> mutable = 0, 1, 2  # Un ensemble
>>> mutable.retirer(0)
>>> mutable.ajouter("X")
>>> mutable
1, 2, 'x'

Les listes et les ensembles sont modifiables, tout comme les dictionnaires et autres types de mappage. Les chaînes et les tuples ne sont pas mutables. Tenter de modifier un élément d'un objet immuable lèvera un Erreur-type.

Conclusion

Python fonctionne différemment des langages qui prennent en charge le passage d'arguments par référence ou par valeur. Les arguments de fonction deviennent des variables locales affectées à chaque valeur transmise à la fonction. Mais cela ne vous empêche pas d’obtenir les mêmes résultats que vous attendez lorsque vous passez des arguments par référence dans d’autres langues.

Dans ce didacticiel, vous avez appris:

  • Comment Python gère affectation de valeurs à des variables
  • Comment sont les arguments de fonction passé par cession en Python
  • Pourquoi renvoyer des valeurs est une bonne pratique pour la réplication passe par référence
  • Comment utiliser les attributs, dictionnaires, et listes comme bonnes pratiques alternatives

Vous avez également appris quelques bonnes pratiques supplémentaires pour la réplication de constructions passe-par-référence en Python. Vous pouvez utiliser ces connaissances pour implémenter des modèles qui ont traditionnellement requis la prise en charge du passage par référence.

Pour poursuivre votre aventure Python, je vous encourage à approfondir certains des sujets connexes que vous avez rencontrés ici, tels que la mutabilité, les expressions d’affectation, les espaces de noms et la portée Python.

Restez curieux et à la prochaine fois!