Comprendre un code avec les fonctions

Écrire un programme, c’est exprimer des instructions, les instructions que l’on donne à l’ordinateur pour qu’il fasse quelque chose. Il est donc indispensable à la lecture d’un programme de comprendre au moins l’intention de ces instructions. La première étape est de prendre soin des noms utilisés.

Mais ce n’est pas suffisant. Pour certaines fonctionnalités, vous allez devoir écrire un enchainement complexe d’instructions dont le sens n’apparait qu’à la lecture et la compréhension de l’ensemble. À la lecture d’un code, vous pouvez perdre beaucoup de temps à essayer de comprendre cette partie qui n’est pas d’intérêt pour vous.

Comment expliquer ce que fait un ensemble d’instructions ? Le plus évident est d’ajouter des commentaires, mais c’est aussi la plus mauvaise idée et si vous vous demandez pourquoi… Ce sera le sujet d’un article.

C »est là qu’entre en jeu les fonctions qui ont un élément très important : un nom.

Rappel sur les fonctions

Une fonction, c’est une structure qui définit un bloc de code. Elle a un nom, peut recevoir des paramètres et retourner un résultat. En Python, nous pouvons la déclarer de la manière suivante :

def add(a, b):
    return a + b

Le nom de cette fonction est add. Elle prend deux paramètres. Elle n’a certes qu’une seule instruction et elle retourne le résultat de l’addition des deux paramètres.

Le nom d’une fonction devient une instruction. Il est donc possible d’exécuter cette fonction et récupérer le résultat.

résultat = add(5, 10)

De manière classique, les fonctions sont présentées comme un moyen d’éviter la redondance. Une fonction est alors appelée par plusieurs parties du programme.

Mais il y a une autre caractéristique : son nom.

Un code peu lisible.

Je vous ai proposé un article sur comment renommer vos photos avec Python. Le code que je vous ai proposé dans l’article et qui est disponible sur mon Github, contient ce code :

for image_file in image_files:

    with open(image_file, 'rb') as my_picture:
        tags = exifread.process_file(my_picture)
        try:
            picture_date = datetime.strptime(str(
                           tags.get('EXIF DateTimeOriginal')),
                           '%Y:%m:%d %H:%M:%S')
        except ValueError:
            picture_date = None
            print("No value for ", image_file)

    if picture_date:
        renamed_file_name = picture_date.strftime('%y%m%d-%H%M%S') + image_path.suffix
        rename_path = image_path.with_name(renamed_file_name)

        if not rename_path.exists():
            image_path.rename(rename_path)
        else:
            print(f"Fichier {rename_path} existe pour le fichier {image_path}")

Nous avons ici une boucle qui, pour chaque ficher, extrait la métadonnée date si elle existe, en crée une chaine de caractère et renomme le fichier en conséquence. Si la métadonnée n’existe pas, le fichier n’est pas renommé.

En soi, il n’est pas trop compliqué mais les différentes étapes (extraction de l’information, transformation, renommage) nécessitent plusieurs instructions et il est difficile d’identifier ce que l’on fait.

Je vais m’intéresser au bloc entre la ligne 3 et 11. C’est le bloc qui, pour chaque fichier, extrait un objet datetime. Ce bloc travaille sur une données unique, un chemin vers un fichier, et génère une donnée qui est un objet de type datetime ou None.

Ce bloc peut être extrait et déporté dans une fonction qui prends en paramètre un chemin et retourne un objet de type datetime ou None :

def get_creation_date(image_file_path):
    with open(image_file_path, 'rb') as my_picture:
        tags = exifread.process_file(my_picture)
        try:
            return datetime.strptime(str(
                tags.get('EXIF DateTimeOriginal')),
                '%Y:%m:%d %H:%M:%S')
        except ValueError:
            print("No value for ", image_file)
            return None

Nous pouvons maintenant faire appel à cette fonction et le code précédent devient :

for image_file in image_files:

    picture_date = get_creation_date(image_file)

    if picture_date:
        renamed_file_name = picture_date.strftime('%y%m%d-%H%M%S') + image_path.suffix
        rename_path = image_path.with_name(renamed_file_name)

        if not rename_path.exists():
            image_path.rename(rename_path)
        else:
            print(f"Fichier {rename_path} existe pour le fichier {image_path}")

Les 8 lignes du code précédent sont devenues la ligne 5.

Bien entendu, on peut appliquer ce principe à la seconde partie du code que l’on peut aussi déporter dans une fonction.

def rename_image(image_file_path, picture_date):
    if picture_date:
        renamed_file_name = picture_date.strftime('%y%m%d-%H%M%S') + image_file_path.suffix
        rename_path = image_file_path.with_name(renamed_file_name)

        if not rename_path.exists():
            image_file_path.rename(rename_path)
        else:
            print(f"Fichier {rename_path} existe pour le fichier {image_file_path}")

Et ainsi, notre bloc principal devient :

for image_file in image_files:

    picture_date = get_creation_date(image_file)

    rename_image(image_file, picture_date)

Ce qui, admettons le, simplifie grandement la lecture de la boucle.

Lorsqu’on apprends les fonctions, on apprends aussi que leur utilité est d’éviter la redondance et donc de contenir du code qui sera appelé à plusieurs endroits. Ainsi, dans du code comme le précédent, vous n’avez en général pas le réflexe de transformer ce code en une fonction. Et pourtant, le faire apporte beaucoup…

Apport des fonctions

Il ne faut pas oublier qu’une fonction est identifiée par un nom. Un nom bien choisi doit permettre de comprendre ce que fait le bloc d’instructions. Ainsi, en remplaçant un code complexe par un appel de fonction décrivant ce que fait le code, on améliore la lisibilité et la maintenance de ce code.

La lisibilité pour commencer. Vous avez donné un nom à cet ensemble d’action expliquant ce qu’elles font. Le bloc qui y fait appel devient plus lisible comme vous pouvez le voir avec la boucle finale.

Mais il y a également des bénéfices au niveau de la maintenance. Si nous voulons faire une évolution, nous pouvons facilement identifier où faire la modification car les codes sont isolés. Il faut modifier comment une image est renommée ? Vous n’avez pas à chercher, c’est dans la fonction rename_image().

Autre point important sur la question de la maintenance : une fonction peut être testée de manière isolée. Est-ce que vous arrivez à extraire correctement la date ? Vous pouvez le tester en appelant directement la fonction get_creation_date(). Comparez avec le code précédent.

En conclusion

Les fonctions ne servent donc pas uniquement à éviter la redondance de code mais également à rendre un code plus lisible et plus maintenance. Si vous avez une suite d’instructions complexe qui fait une chose identifiable, pensez à convertir ce code en fonction.

Si vous avez aimé ce post, n’hésitez pas à laisser un commentaire ci-dessous ou sur la page Facebook 😉

À propos de... Darko Stankovski

iT guy, photographe et papa 3.0, je vous fais partager mon expérience et découvertes dans ces domaines. Vous pouvez me suivre sur les liens ci-dessous.

Vous aimerez aussi...

Laisser un commentaire

En naviguant sur Dad 3.0, vous acceptez l’utilisation de cookies pour une navigation optimale et nous permettre de réaliser des statistiques de visites. Plus d'informations

Le blog Dad 3.0 utilise les cookies pour vous permettre une navigation optimale et nous permettre de réaliser des statistiques de visite. Dad 3.0 affichant des publicités, celles-si utilisent également des cookies pour un ciblage publicitaire. En continuant la navigation sur Dad 3.0, vous acceptez le dépôt et la lecture de cookies.

Fermer