Plus sur les fonctions

Intermediate

This tutorial is from open-source community. Access the source code

Introduction

Bien que les fonctions aient été introduites plus tôt, très peu de détails ont été fournis sur la manière dont elles fonctionnent réellement au niveau plus profond. Cette section vise à combler certains écarts et à discuter de questions telles que les conventions d'appel, les règles de portée et plus encore.

Ceci est un Guided Lab, qui fournit des instructions étape par étape pour vous aider à apprendre et à pratiquer. Suivez attentivement les instructions pour compléter chaque étape et acquérir une expérience pratique. Les données historiques montrent que c'est un laboratoire de niveau intermédiaire avec un taux de réussite de 75%. Il a reçu un taux d'avis positifs de 100% de la part des apprenants.

Appeler une fonction

Considérez cette fonction :

def read_prices(filename, debug):
  ...

Vous pouvez appeler la fonction avec des arguments positionnels :

prices = read_prices('prices.csv', True)

Ou vous pouvez appeler la fonction avec des arguments nommés :

prices = read_prices(filename='prices.csv', debug=True)

Arguments par défaut

Parfois, vous voulez qu'un argument soit facultatif. Dans ce cas, affectez-lui une valeur par défaut dans la définition de la fonction.

def read_prices(filename, debug=False):
 ...

Si une valeur par défaut est assignée, l'argument est facultatif dans les appels de fonction.

d = read_prices('prices.csv')
e = read_prices('prices.dat', True)

Nota : Les arguments avec des valeurs par défaut doivent apparaître à la fin de la liste d'arguments (tous les arguments non facultatifs vont en premier).

Préférez les arguments nommés pour les arguments facultatifs

Comparez et contrastez ces deux styles d'appel différents :

parse_data(data, False, True) #?????

parse_data(data, ignore_errors=True)
parse_data(data, debug=True)
parse_data(data, debug=True, ignore_errors=True)

Dans la plupart des cas, les arguments nommés améliorent la clarté du code - en particulier pour les arguments qui servent de drapeaux ou qui sont liés à des fonctionnalités facultatives.

Meilleures pratiques de conception

Donnez toujours des noms courts, mais significatifs aux arguments des fonctions.

Quelqu'un utilisant une fonction peut vouloir utiliser le style d'appel avec arguments nommés.

d = read_prices('prices.csv', debug=True)

Les outils de développement Python afficheront les noms dans les fonctionnalités d'aide et la documentation.

Retourner des valeurs

L'instruction return renvoie une valeur

def square(x):
    return x * x

Si aucune valeur de retour n'est donnée ou si return est manquant, None est renvoyé.

def bar(x):
    statements
    return

a = bar(4)      ## a = None

## OU
def foo(x):
    statements  ## Pas de `return`

b = foo(4)      ## b = None

Plusieurs valeurs de retour

Les fonctions ne peuvent renvoyer qu'une seule valeur. Cependant, une fonction peut renvoyer plusieurs valeurs en les renvoyant dans un tuple.

def divide(a,b):
    q = a // b      ## Quotient
    r = a % b       ## Reste
    return q, r     ## Renvoie un tuple

Exemple d'utilisation :

x, y = divide(37,5) ## x = 7, y = 2

x = divide(37, 5)   ## x = (7, 2)

Portée des variables

Les programmes attribuent des valeurs à des variables.

x = value ## Variable globale

def foo():
    y = value ## Variable locale

Les affectations de variables se produisent en dehors et à l'intérieur des définitions de fonctions. Les variables définies en dehors sont "globales". Les variables à l'intérieur d'une fonction sont "locales".

Variables locales

Les variables assignées à l'intérieur des fonctions sont privées.

def read_portfolio(filename):
    portfolio = []
    for line in open(filename):
        fields = line.split(',')
        s = (fields[0], int(fields[1]), float(fields[2]))
        portfolio.append(s)
    return portfolio

Dans cet exemple, filename, portfolio, line, fields et s sont des variables locales. Ces variables ne sont pas conservées ni accessibles après l'appel de la fonction.

>>> stocks = read_portfolio('portfolio.csv')
>>> fields
Traceback (most recent call last):
File "<stdin>", line 1, in?
NameError: name 'fields' is not defined
>>>

Les variables locales ne peuvent également pas entrer en conflit avec les variables trouvées ailleurs.

Variables globales

Les fonctions peuvent librement accéder aux valeurs des variables globales définies dans le même fichier.

name = 'Dave'

def greeting():
    print('Hello', name)  ## Utilisation de la variable globale `name`

Cependant, les fonctions ne peuvent pas modifier les variables globales :

name = 'Dave'

def spam():
  name = 'Guido'

spam()
print(name) ## Affiche 'Dave'

Rappel : Toutes les affectations dans les fonctions sont locales.

Modifier les variables globales

Si vous devez modifier une variable globale, vous devez la déclarer comme telle.

name = 'Dave'

def spam():
    global name
    name = 'Guido' ## Modifie la variable globale name ci-dessus

La déclaration global doit apparaître avant son utilisation et la variable correspondante doit exister dans le même fichier que la fonction. Ayant vu cela, sachez que cela est considéré comme une mauvaise pratique. En fait, essayez d'éviter complètement global si vous le pouvez. Si vous avez besoin qu'une fonction modifie un certain type d'état en dehors de la fonction, il est préférable d'utiliser une classe à la place (nous en reparlerons plus tard).

Passage d'arguments

Lorsque vous appelez une fonction, les variables d'arguments sont des noms qui se réfèrent aux valeurs passées. Ces valeurs ne sont PAS des copies. Si des types de données mutables sont passés (par exemple, des listes, des dictionnaires), ils peuvent être modifiés in-place.

def foo(items):
    items.append(42)    ## Modifie l'objet d'entrée

a = [1, 2, 3]
foo(a)
print(a)                ## [1, 2, 3, 42]

Point clé : Les fonctions ne reçoivent pas une copie des arguments d'entrée.

Reaffectation vs Modification

Assurez-vous de comprendre la subtile différence entre modifier une valeur et réaffecter un nom de variable.

def foo(items):
    items.append(42)    ## Modifie l'objet d'entrée

a = [1, 2, 3]
foo(a)
print(a)                ## [1, 2, 3, 42]

## CONTRARES
def bar(items):
    items = [4,5,6]    ## Change la variable locale `items` pour qu'elle pointe vers un autre objet

b = [1, 2, 3]
bar(b)
print(b)                ## [1, 2, 3]

Rappel : L'affectation de variable n'écrase jamais la mémoire. Le nom est simplement lié à une nouvelle valeur.

Cet ensemble d'exercices vous demande de mettre en œuvre ce qui est peut-être la partie la plus puissante et la plus difficile du cours. Il y a beaucoup d'étapes et de nombreux concepts des exercices précédents sont combinés tous à la fois. La solution finale n'est que d'environ 25 lignes de code, mais prenez votre temps et assurez-vous de comprendre chaque partie.

Une partie centrale de votre programme report.py porte sur la lecture de fichiers CSV. Par exemple, la fonction read_portfolio() lit un fichier contenant des lignes de données de portefeuille et la fonction read_prices() lit un fichier contenant des lignes de données de prix. Dans les deux fonctions, il y a beaucoup de parties "fastidieuses" de bas niveau et des fonctionnalités similaires. Par exemple, elles ouvrent toutes deux un fichier et l'enveloppent avec le module csv et elles convertissent tous deux divers champs en nouveaux types.

Si vous deviez faire beaucoup d'analyse de fichiers dans la réalité, vous voudriez probablement nettoyer certaines de ces parties et la rendre plus générique. C'est notre objectif.

Commencez cet exercice en ouvrant le fichier appelé fileparse.py. C'est là que nous allons travailler.

Exercice 3.3 : Lecture de fichiers CSV

Pour commencer, concentrons-nous seulement sur le problème de la lecture d'un fichier CSV dans une liste de dictionnaires. Dans le fichier fileparse_3.3.py, définissez une fonction qui ressemble à ceci :

## fileparse_3.3.py
import csv

def parse_csv(filename):
    '''
    Analyse un fichier CSV en une liste d'enregistrements
    '''
    with open(filename) as f:
        rows = csv.reader(f)

        ## Lit les en-têtes du fichier
        headers = next(rows)
        records = []
        for row in rows:
            if not row:    ## Ignore les lignes sans données
                continue
            record = dict(zip(headers, row))
            records.append(record)

    return records

Cette fonction lit un fichier CSV dans une liste de dictionnaires tout en cachant les détails de l'ouverture du fichier, de son enveloppement avec le module csv, de l'ignorance des lignes vides, etc.

Essayez-le :

Indice : python3 -i fileparse_3.3.py.

>>> portfolio = parse_csv('/home/labex/project/portfolio.csv')
>>> portfolio
[{'name': 'AA','shares': '100', 'price': '32.20'}, {'name': 'IBM','shares': '50', 'price': '91.10'}, {'name': 'CAT','shares': '150', 'price': '83.44'}, {'name': 'MSFT','shares': '200', 'price': '51.23'}, {'name': 'GE','shares': '95', 'price': '40.37'}, {'name': 'MSFT','shares': '50', 'price': '65.10'}, {'name': 'IBM','shares': '100', 'price': '70.44'}]
>>>

Cela est bon, sauf que vous ne pouvez pas faire de calcul utile avec les données car tout est représenté comme une chaîne de caractères. Nous allons corriger cela bientôt, mais continuons à construire dessus.

Exercice 3.4 : Construction d'un sélecteur de colonnes

Dans de nombreux cas, vous n'êtes intéressé que par certaines colonnes d'un fichier CSV, pas par toutes les données. Modifiez la fonction parse_csv() de sorte qu'elle permette facultativement de sélectionner des colonnes spécifiées par l'utilisateur comme suit :

>>> ## Lire toutes les données
>>> portfolio = parse_csv('/home/labex/project/portfolio.csv')
>>> portfolio
[{'name': 'AA','shares': '100', 'price': '32.20'}, {'name': 'IBM','shares': '50', 'price': '91.10'}, {'name': 'CAT','shares': '150', 'price': '83.44'}, {'name': 'MSFT','shares': '200', 'price': '51.23'}, {'name': 'GE','shares': '95', 'price': '40.37'}, {'name': 'MSFT','shares': '50', 'price': '65.10'}, {'name': 'IBM','shares': '100', 'price': '70.44'}]

>>> ## Lire seulement certaines données
>>> shares_held = parse_csv('/home/labex/project/portfolio.csv', select=['name','shares'])
>>> shares_held
[{'name': 'AA','shares': '100'}, {'name': 'IBM','shares': '50'}, {'name': 'CAT','shares': '150'}, {'name': 'MSFT','shares': '200'}, {'name': 'GE','shares': '95'}, {'name': 'MSFT','shares': '50'}, {'name': 'IBM','shares': '100'}]
>>>

Un exemple de sélecteur de colonnes a été donné dans l'exercice 2.23. Cependant, voici une manière de le faire :

## fileparse_3.4.py
import csv

def parse_csv(filename, select=None):
    '''
    Analyse un fichier CSV en une liste d'enregistrements
    '''
    with open(filename) as f:
        rows = csv.reader(f)

        ## Lit les en-têtes du fichier
        headers = next(rows)

        ## Si un sélecteur de colonnes a été donné, trouver les indices des colonnes spécifiées.
        ## Rétroalimenter également l'ensemble d'en-têtes utilisés pour les dictionnaires résultants
        if select:
            indices = [headers.index(colname) for colname in select]
            headers = select
        else:
            indices = []

        records = []
        for row in rows:
            if not row:    ## Ignore les lignes sans données
                continue
            ## Filtrer la ligne si des colonnes spécifiques ont été sélectionnées
            if indices:
                row = [ row[index] for index in indices ]

            ## Créer un dictionnaire
            record = dict(zip(headers, row))
            records.append(record)

    return records

Il y a un certain nombre de points délicats dans cette partie. Probablement le plus important est la correspondance des sélections de colonnes aux indices de ligne. Par exemple, supposons que le fichier d'entrée ait les en-têtes suivants :

>>> headers = ['name', 'date', 'time','shares', 'price']
>>>

Maintenant, supposons que les colonnes sélectionnées soient les suivantes :

>>> select = ['name','shares']
>>>

Pour effectuer la sélection appropriée, vous devez mapper les noms de colonnes sélectionnés aux indices de colonne dans le fichier. C'est ce que fait cette étape :

>>> indices = [headers.index(colname) for colname in select ]
>>> indices
[0, 3]
>>>

En d'autres termes, "name" est la colonne 0 et "shares" est la colonne 3. Lorsque vous lisez une ligne de données à partir du fichier, les indices sont utilisés pour la filtrer :

>>> row = ['AA', '6/11/2007', '9:50am', '100', '32.20' ]
>>> row = [ row[index] for index in indices ]
>>> row
['AA', '100']
>>>

Exercice 3.5 : Effectuer des conversions de type

Modifiez la fonction parse_csv() dans le répertoire /home/labex/project/fileparse_3.5.py de sorte qu'elle permette facultativement d'appliquer des conversions de type aux données renvoyées. Par exemple :

>>> portfolio = parse_csv('/home/labex/project/portfolio.csv', types=[str, int, float])
>>> portfolio
[{'name': 'AA','shares': 100, 'price': 32.2}, {'name': 'IBM','shares': 50, 'price': 91.1}, {'name': 'CAT','shares': 150, 'price': 83.44}, {'name': 'MSFT','shares': 200, 'price': 51.23}, {'name': 'GE','shares': 95, 'price': 40.37}, {'name': 'MSFT','shares': 50, 'price': 65.1}, {'name': 'IBM','shares': 100, 'price': 70.44}]

>>> shares_held = parse_csv('/home/labex/project/portfolio.csv', select=['name','shares'], types=[str, int])
>>> shares_held
[{'name': 'AA','shares': 100}, {'name': 'IBM','shares': 50}, {'name': 'CAT','shares': 150}, {'name': 'MSFT','shares': 200}, {'name': 'GE','shares': 95}, {'name': 'MSFT','shares': 50}, {'name': 'IBM','shares': 100}]
>>>

Vous avez déjà exploré ceci dans l'exercice 2.24. Vous devrez insérer le fragment de code suivant dans votre solution :

...
if types:
    row = [func(val) for func, val in zip(types, row) ]
...

Exercice 3.6 : Travailler sans en-têtes

Certains fichiers CSV ne contiennent pas d'informations d'en-tête. Par exemple, le fichier prices.csv ressemble à ceci :

"AA",9.22
"AXP",24.85
"BA",44.85
"BAC",11.27
...

Modifiez la fonction parse_csv() dans /home/labex/project/fileparse_3.6.py de sorte qu'elle puisse fonctionner avec de tels fichiers en créant une liste de tuples à la place. Par exemple :

>>> prices = parse_csv('/home/labex/project/prices.csv', types=[str,float], has_headers=False)
>>> prices
[('AA', 9.22), ('AXP', 24.85), ('BA', 44.85), ('BAC', 11.27), ('C', 3.72), ('CAT', 35.46), ('CVX', 66.67), ('DD', 28.47), ('DIS', 24.22), ('GE', 13.48), ('GM', 0.75), ('HD', 23.16), ('HPQ', 34.35), ('IBM', 106.28), ('INTC', 15.72), ('JNJ', 55.16), ('JPM', 36.9), ('KFT', 26.11), ('KO', 49.16), ('MCD', 58.99), ('MMM', 57.1), ('MRK', 27.58), ('MSFT', 20.89), ('PFE', 15.19), ('PG', 51.94), ('T', 24.79), ('UTX', 52.61), ('VZ', 29.26), ('WMT', 49.74), ('XOM', 69.35)]
>>>

Pour apporter ces modifications, vous devrez modifier le code de sorte que la première ligne de données ne soit pas interprétée comme une ligne d'en-tête. De plus, vous devrez vous assurer de ne pas créer de dictionnaires car il n'y a plus de noms de colonnes pour servir de clés.

Exercice 3.7 : Sélectionner un délimiteur de colonne différent

Bien que les fichiers CSV soient assez courants, il est également possible que vous rencontriez un fichier utilisant un séparateur de colonne différent, tel qu'un tabulation ou un espace. Par exemple, le fichier portfolio.dat ressemble à ceci :

name shares price
"AA" 100 32.20
"IBM" 50 91.10
"CAT" 150 83.44
"MSFT" 200 51.23
"GE" 95 40.37
"MSFT" 50 65.10
"IBM" 100 70.44

La fonction csv.reader() permet de spécifier un délimiteur de colonne différent comme suit :

rows = csv.reader(f, delimiter=' ')

Modifiez votre fonction parse_csv() dans /home/labex/project/fileparse_3.7.py de sorte qu'elle permette également de changer le délimiteur.

Par exemple :

>>> portfolio = parse_csv('/home/labex/project/portfolio.dat', types=[str, int, float], delimiter=' ')
>>> portfolio
[{'name': 'AA','shares': 100, 'price': 32.2}, {'name': 'IBM','shares': 50, 'price': 91.1}, {'name': 'CAT','shares': 150, 'price': 83.44}, {'name': 'MSFT','shares': 200, 'price': 51.23}, {'name': 'GE','shares': 95, 'price': 40.37}, {'name': 'MSFT','shares': 50, 'price': 65.1}, {'name': 'IBM','shares': 100, 'price': 70.44}]
>>>

Commentaire

Si vous êtes arrivé jusqu'ici, vous avez créé une fonction de bibliothèque agréable et vraiment utile. Vous pouvez l'utiliser pour analyser des fichiers CSV arbitraires, sélectionner les colonnes qui vous intéressent, effectuer des conversions de type, sans avoir à vous soucier trop des fonctionnements internes des fichiers ou du module csv.

Sommaire

Félicitations ! Vous avez terminé le laboratoire Plus sur les fonctions. Vous pouvez pratiquer d'autres laboratoires sur LabEx pour améliorer vos compétences.