Les fonctions de Python (que ce soit les fonctions intégrées built-in ou les fonctions personnalisées que nous écrivons nous-mêmes) sont des outils essentiels pour travailler avec des données. Mais ce qu’ils font avec nos données peut être un peu déroutant surtout si on ignore ce qu’il se passe à l’intérieur. Cela pourrait entraîner de graves erreurs dans notre analyse notamment avec la distinction de données mutables et immuables en Python.

Dans cet article, nous allons examiner de près la façon dont Python traite différents types de données lorsqu’ils sont manipulés dans des fonctions et nous allons apprendre à faire en sorte que nos données ne soient modifiées que lorsque nous souhaitons les modifier.

Isolement de la mémoire dans les fonctions

Pour comprendre comment Python gère les variables globales dans les fonctions, faisons une petite expérience. Nous allons créer deux variables globales, number_1 et number_2 et les affecter aux nombres entiers 5 et 10. Nous utiliserons ensuite ces variables globales comme arguments d’une fonction qui exécute des calculs simples. Nous utiliserons également les noms de variables comme noms de paramètres de la fonction. Ensuite, nous verrons si toute l’utilisation des variables dans notre fonction a affecté la valeur globale de ces variables.

number_1 = 5
number_2 = 10

def multiply_and_add(number_1, number_2):
    number_1 = number_1 * 10
    number_2 = number_2 * 10
    return number_1 + number_2

a_sum = multiply_and_add(number_1, number_2)
print(a_sum)
print(number_1)
print(number_2)
150
5
10

Comme nous pouvons le voir ci-dessus, la fonction a fonctionné correctement et les valeurs des variables globales number_1 et number_2 n’ont pas changé, même si nous les avons utilisées comme arguments et noms de paramètres dans notre fonction. En effet, Python stocke les variables d’une fonction dans un emplacement de mémoire différent de celui des variables globales. Ils sont isolés. Ainsi, la variable number_1 peut avoir une valeur (5) globale et une valeur différente (50) dans la fonction, où elle est isolée.

Si vous ne comprenez pas la différence entre les paramètres et les arguments, regardez la documentation de Python sur le sujet qui est très utile.

Quid des listes et dictionnaires?

Listes

Nous avons vu que ce que nous faisons avec une variable telle que number_1 ci-dessus dans une fonction n’affecte pas sa valeur globale. Mais number_1 est un entier (type de données assez basique). Que se passe-t-il si nous essayons la même expérience avec un type de données différent, comme une liste? Ci-dessous, nous allons créer une fonction appelée duplicate_last() qui dupliquera l’entrée finale de toute liste que nous lui transmettons en tant qu’argument.

initial_list = [1, 2, 3]

def duplicate_last(a_list):
    last_element = a_list[-1]
    a_list.append(last_element)
    return a_list

new_list = duplicate_last(a_list = initial_list)
print(new_list)
print(initial_list)
[1, 2, 3, 3]
[1, 2, 3, 3]

Comme on peut le voir, la valeur globale de initial_list a été mise à jour alors que sa valeur n’a changé que dans la fonction!

Dictionnaires

Maintenant, écrivons une fonction qui prend un dictionnaire comme argument pour voir si une variable du dictionnaire global sera modifiée quand elle sera également manipulée dans une fonction.

Pour rendre cela un peu plus réaliste, nous utiliserons les données du dataset AppleStore.csv disponible ici.

Dans l’extrait de code ci-dessous, nous commençons avec un dictionnaire contenant le nombre d’applications avec chaque classe d’âge dans l’ensemble de données (il y a donc 4 433 applications classées «4+», 987 classées «9+», etc.). . Imaginons que nous voulions calculer un pourcentage pour chaque classe d’âge afin d’avoir une idée des classes d’âge les plus courantes parmi les applications de l’App Store.

Pour ce faire, nous allons écrire une fonction appelée make_percentages() qui prend un dictionnaire en argument et convertit les décomptes en pourcentages. Nous devrons commencer un compteur à zéro, puis parcourir chaque valeur du dictionnaire, en les ajoutant au nombre afin d’obtenir le nombre total d’évaluations. Ensuite, nous allons parcourir à nouveau le dictionnaire et faire un calcul pour chaque valeur afin de calculer le pourcentage.

content_ratings = {'4+': 4433, '9+': 987, '12+': 1155, '17+': 622}

def make_percentages(a_dictionary):
    total = 0
    for key in a_dictionary:
        count = a_dictionary[key]
        total += count

    for key in a_dictionary:
        a_dictionary[key] = (a_dictionary[key] / total) * 100

    return a_dictionary

Avant d’examiner le résultat, regardons rapidement ce qui se passe ci-dessus. Après avoir affecté notre dictionnaire des classements par âge de l’application à la variable content_ratings, nous créons une nouvelle fonction appelée make_percentages() qui prend un seul argument: a_dictionary.

Pour déterminer le pourcentage d’applications classées dans chaque classe d’âge, nous devons connaître le nombre total d’applications. Nous avons donc défini une nouvelle variable appelée total fixée à 0, puis parcouru chaque clé de a_dictionary, en l’ajoutant à total.

Une fois terminé, tout ce que nous avons à faire est de parcourir a_dictionary à nouveau, en divisant chaque entrée par le total, puis en multipliant le résultat par 100. Cela nous donnera un dictionnaire avec des pourcentages.

Mais que se passe-t-il lorsque nous utilisons notre variable globale content_ratings comme argument pour cette nouvelle fonction?

c_ratings_percentages = make_percentages(content_ratings)
print(c_ratings_percentages)
print(content_ratings)
{'4+': 61.595109073224954, '9+': 13.714047519799916, '12+': 16.04835348061692, '17+': 8.642489926358204}
{'4+': 61.595109073224954, '9+': 13.714047519799916, '12+': 16.04835348061692, '17+': 8.642489926358204}

Comme nous l’avons vu avec les listes, notre variable globale content_ratings a été modifiée, même si elle n’a été modifiée qu’à l’intérieur de la fonction make_percentages() que nous avons créée.

Alors que se passe-t-il réellement ici? Nous nous sommes heurtés à la différence entre les types de données mutable (sujet au changement) et immuable (demeure inchangé).

Types de données mutables et immuables

En Python, les types de données peuvent être mutables (modifiables) ou immuables (non modifiables). Et bien que la plupart des types de données avec lesquels nous avons travaillé dans l’introduction à Python soient immuables (y compris les entiers, les flottants, les chaînes, les booléens et les n-uplets), les listes et les dictionnaires sont modifiables. Cela signifie qu’une liste globale ou un dictionnaire peut être modifié même s’il est utilisé dans une fonction, comme nous l’avons vu dans les exemples ci-dessus.

Pour comprendre la différence entre mutable (modifiable) et immuable (immuable), il est utile de regarder comment Python traite réellement ces variables.

Commençons par considérer une affectation de variable simple: a = 5

Le nom de variable a agit comme un pointeur vers 5 et nous aide à récupérer 5 quand nous le voulons.

5 est un entier et les entiers sont des types de données immuables. Si un type de données est immuable, cela signifie qu’il ne peut pas être mis à jour une fois créé. Si nous faisons a += 1, nous ne mettons pas à jour 5 à 6.

  • a pointe initialement vers 5.
  • a += 1 est exécuté, et cela déplace le pointeur de 5 à 6, cela ne change pas le nombre 5.

Les types de données modifiables tels que les listes et les dictionnaires se comportent différemment. Ils peuvent être mis à jour. Donc, par exemple, faisons une liste très simple:

list_1 = [1, 2]

Si nous ajoutons un 3 à la fin de cette liste, nous ne faisons pas que pointer list_1 vers une liste différente, nous mettons directement à jour la liste existante:

Même si nous créons plusieurs variables de liste, dans la mesure où elles pointent vers la même liste, elles seront toutes mises à jour lorsque cette liste sera modifiée, comme le montre le code ci-dessous:

list_1 = [1, 2]
list_2 = list_1
list_1.append(3)
print(list_1)
print(list_2)
[1, 2, 3]
[1, 2, 3]

Cela explique pourquoi nos variables globales ont été modifiées lorsque nous avons expérimenté des listes et des dictionnaires plus tôt. Comme les listes et les dictionnaires sont mutables, leur modification (même à l’intérieur d’une fonction) modifie la liste ou le dictionnaire lui-même, ce qui n’est pas le cas pour les types de données immuables.

Conserver les types de données mutables inchangés

De manière générale, nous ne voulons pas que nos fonctions changent les variables globales, même si elles contiennent des types de données modifiables comme des listes ou des dictionnaires. C’est parce que dans des analyses et des programmes plus complexes, nous pourrions utiliser fréquemment de nombreuses fonctions différentes. Si tous modifient les listes et les dictionnaires sur lesquels ils travaillent, il peut devenir assez difficile de garder trace de ce qui change.

Heureusement, il existe un moyen simple de contourner ce problème: nous pouvons faire une copie de la liste ou du dictionnaire en utilisant une méthode built-in de Python appelée .copy().

Si vous ne connaissez pas les méthodes, ne vous inquiétez pas. Elles sont couvertEs dans mon cours sur les bases de Python, mais pour ce tutoriel, tout ce que vous devez savoir, c’est que .copy() fonctionne comme .append():

list.append() # ajoute un élément à une liste
list.copy() # fais une copie de la liste

Examinons de nouveau la fonction que nous avons écrite pour les listes et mettez-la à jour afin que ce qui se passe dans notre fonction ne change pas initial_list. Tout ce que nous avons à faire est de changer l’argument que nous passons à notre fonction de initial_list à initial_list.copy().

initial_list = [1, 2, 3]

def duplicate_last(a_list):
    last_element = a_list[-1]
    a_list.append(last_element)
    return a_list

new_list = duplicate_last(a_list = initial_list.copy()) # copie de la liste
print(new_list)
print(initial_list)
[1, 2, 3, 3]
[1, 2, 3]

Comme nous pouvons le constater, cela a résolu notre problème. Voici pourquoi: utiliser .copy() crée une copie distincte de la liste. Ainsi, au lieu de pointer sur initial_list elle-même, a_list pointe sur une nouvelle liste qui commence par une copie de initial_list. Toutes les modifications apportées à a_list après ce point sont apportées à cette liste séparée, et non à initial_list elle-même. Par conséquent, la valeur globale de initial_list est inchangée.

Comme illustré ci-dessous, à gauche sans utiliser .copy() et à droite en l’utilisant.

différence méthodes

Cette solution n’est toujours pas parfaite, car nous devons nous rappeler d’ajouter .copy() à chaque fois que nous passons un argument à notre fonction, sans quoi nous risquerions de modifier accidentellement la valeur globale de initial_list. Si nous ne voulons pas avoir à nous en préoccuper, nous pouvons créer cette copie de liste dans la fonction elle-même:

initial_list = [1, 2, 3]

def duplicate_last(a_list):
    copy_list = a_list.copy() # copie de la liste directement dans la fonction
    last_element = copy_list[-1]
    copy_list.append(last_element)
    return copy_list

new_list = duplicate_last(a_list = initial_list)
print(new_list)
print(initial_list)
[1, 2, 3, 3]
[1, 2, 3]

Avec cette approche, nous pouvons transmettre en toute sécurité une variable globale mutable, telle que initial_list à notre fonction. La valeur globale ne sera pas modifiée car la fonction elle-même effectue une copie, puis effectue ses opérations sur cette copie.

La méthode .copy() fonctionne également pour les dictionnaires. Comme pour les listes, nous pouvons simplement ajouter .copy() à l’argument selon lequel nous passons notre fonction pour créer une copie qui sera utilisée pour la fonction sans changer la variable d’origine:

content_ratings = {'4+': 4433, '9+': 987, '12+': 1155, '17+': 622}

def make_percentages(a_dictionary):
    total = 0
    for key in a_dictionary:
        count = a_dictionary[key]
        total += count

    for key in a_dictionary:
        a_dictionary[key] = (a_dictionary[key] / total) * 100

    return a_dictionary


c_ratings_percentages = make_percentages(content_ratings.copy()) # copie du dictionnaire
print(c_ratings_percentages)
print(content_ratings)
{'4+': 61.595109073224954, '9+': 13.714047519799916, '12+': 16.04835348061692, '17+': 8.642489926358204}
{'4+': 4433, '9+': 987, '12+': 1155, '17+': 622}

Mais encore une fois, utiliser cette méthode signifie que nous devons nous rappeler d’ajouter .copy() à chaque fois que nous passons un dictionnaire dans notre fonction make_percentages(). Si nous allons utiliser cette fonction fréquemment, il serait peut-être préférable d’implémenter la copie dans la fonction elle-même, afin que nous n’ayons pas à nous en souvenir.

Ci-dessous, nous utiliserons .copy() dans la fonction elle-même. Cela garantira que nous pourrons l’utiliser sans changer les variables globales que nous lui transmettons en tant qu’arguments, et nous n’avons pas besoin de nous rappeler d’ajouter .copy() à chaque argument passé.

content_ratings = {'4+': 4433, '9+': 987, '12+': 1155, '17+': 622}

def make_percentages(a_dictionary):
    copy_dict = a_dictionary.copy() # crée une copie du dictionnaire
    total = 0
    for key in a_dictionary:
        count = a_dictionary[key]
        total += count

    for key in copy_dict: # utilise la copie, de sorte que l'original reste inchangée
        copy_dict[key] = (copy_dict[key] / total) * 100

    return copy_dict

c_ratings_percentages = make_percentages(content_ratings)
print(c_ratings_percentages)
print(content_ratings)
{'4+': 61.595109073224954, '9+': 13.714047519799916, '12+': 16.04835348061692, '17+': 8.642489926358204}
{'4+': 4433, '9+': 987, '12+': 1155, '17+': 622}

Comme nous pouvons le constater, modifier notre fonction pour créer une copie de notre dictionnaire, puis changer les comptes en pourcentages uniquement dans cette copie nous a permis d’effectuer l’opération que nous voulions sans modifier réellement content_ratings.

Conclusion

Dans cet article, nous avons examiné la différence entre les types de données mutables (qui peuvent changer) et les types de données immuables (qui ne le peuvent pas). Nous avons appris comment utiliser la méthode .copy() pour créer des copies de types de données mutables, tels que des listes et des dictionnaires, afin de pouvoir les utiliser dans des fonctions sans modifier leurs valeurs globales.