Programmation,  Python

Python: Les fonctions ordinaires versus les générateurs pour le web scraping

J’ai regardé des programmes pour faire du web scraping avec le module Scrapy de python. Mon but était de créer un robot d’indexation pour certaines sites web. J’ai remarqué que les programmes de Scrapy pour faire ce travail se servaient du mot «yield» au lieu de «return» lorsqu’il y avait un objet a retourné. Je ne savais pas sa utilité. Puis j’ai appris que «yield» est utilisé pour créer un générateur.

Les générateurs m’ont fait faire un grand détour dans mon projet. À tel point que j’ai décidé d’écrire un article sur le sujet avant d’en écrire une sur le robot d’indexation que j’ai créé. Pourquoi? Parce qu’il faut comprendre le code qu’on lit et les générateurs font partie du code. En plus, ils jouent un rôle important.

D’abord, je n’avais jamais utilisé un générateur. Il m’a fallait apprendre. Il y a plusieurs excellents blogs sur le web et livres de programmation qui abordent le sujet. Plusieurs d’entre eux le font très bien. Il est donc inutile de simplement répéter ce que les autres font très bien. Je parle plutôt de ma compréhension, comment j’organise les renseignements et j’mets leurs idées dans mes propres mots. Un défi pour moi était de d’apprendre pourquoi on les utilise dans les programmes de Scrapy pour faire du web scraping. C’est-à-dire, je parle de l’application dans un contexte spécifique.

Autre défi, c’était de savoir quand je savais assez sur le sujet, car il y a beaucoup d’encres versées sur le sujet. Surtout lorsqu’on tient compte du fait que les générateurs se situent dans le contexte des itérateurs, les itérables et le protocole d’itération.

La réponse à ce défi de savoir assez se situe dans le fait que l’apprentissage consiste à faire des liens entre ce qu’on sait déjà et la nouvelle matière. En conséquence, on doit maîtriser les bases avant d’aller plus loin. Tant et aussi longtemps qu’on n’a pas de bases solides, on va avoir de la difficulté d’aller plus loin. En conséquence, il me fallait tenir compte de ce que je savais à propos de l’itération en programmation. Je parle surtout des boucles for et while. En plus, je suis habitué d’utiliser les «return» dans les fonctions pour retourner une valeur, objet, liste, etc.

Les générateurs s’inscrivent dans ce contexte. On les utilise pour itérer et les générateurs retournent les valeurs avec «yield» au lieu d’utiliser «return». Mais ce n’est pas tout. L’article de Dataquest[efn-note]«Python Generators» https://www.dataquest.io/blog/python-generators-tutorial/[/efn_note] fait un très bon travail à expliquer les bases et il va plus loin. Mais rendu à la section «Generators feeding generators» j’ai commencé à être perdu. Pourquoi? Parce que je n’avais pas assez maîtrise des préalables pour aller plus loin.

Pour savoir pourquoi les générateurs sont utilisés dans le contexte de web scraping et quels sont des avantages à les utiliser. Il faut comprendre certaines caractéristiques des générateurs.

Itérables et itérateurs

Un blog que j’ai lu (je ne me rappelle plus lequel) a écrit qu’il faut comprendre les itérables et les itérateurs avant de pouvoir comprendre les générateurs. J’ai appris que les générateurs s’inscrivent dans le contexte des itérables et les itérateurs. Une fois que j’ai compris ces derniers, j’ai établi les préalables nécessaires pour une bonne compréhension des générateurs. Commençons avec les itérables.

Un itérable est n’importe quoi qui peut être exécuter dans une boucle for. Par exemple, les listes, les dictionnaires, les tuples sont les itérables. Un chiffre n’est pas un itérable, car si l’on essaie de l’exécuter dans une boucle for, l’interpréteur va afficher une erreur. Par exemple, si l’on essaie d’exécuter le chiffre 12345 dans une boucle for, l’erreur «int n’est pas un itérable» va s’afficher.

La question suivante est celle-ci: Qu’est ce-qui fait en sorte qu’un objet devient itérable? La réponse: tous les itérables possèdent la méthode iter(). Par exemple, supposons que je crée une liste quelconque et je veux savoir toutes les méthodes dans cette liste. J’exécute print(dir(nom_de_liste)). La méthode iter()fait en sorte qu’un objet devient itérable.1 Nous pouvons donc conclure que n’importe quoi qui peut être transmis à la méthode iter() est un itérable.2

Je vais avoir autres éléments à ajouter aux itérables, mais il faut comprendre les itérateurs avant.

Les itérateurs possèdent toutes les caractéristiques des itérables. C’est-à-dire, ils contiennent la méthode iter(). Mais ils en ont d’autres caractéristiques. Les itérateurs sont des objets qui sont capables de souvenir où ils sont pendant l’itération. Ils savent aussi comment obtenir leur prochaine valeur. Par exemple, dans une boucle for l’index «i» sert ces deux fonctions. La méthode qui donne aux itérateurs ces caractéristiques s’intitule next().

Les itérateurs ne peuvent qu’avancer. Ils ne peuvent pas reculer. Ils ne sont pas obligés de terminer, mais lorsqu’ils le font ils affichent un «StopIteration exception». Il faut souligner que les itérateurs ne vont chercher qu’une valeur à la fois. Pourquoi est-ce que c’est important de le souligner? Parce que cela rend les itérateurs sont efficaces dans la gestion de mémoire.

Tous les itérables peuvent devenir les itérables. Il s’agit d’exécuter la méthode iter() sur l’itérable en question. Par exemple, regardons le code suivant:

 nums = [1, 2, 3]
 i_nums = iter(nums)
 print(i_nums)

La méthode iter() fait en sorte que i_nums devient un itérable.3

Terminons cette section avec des clarifications. Un itérable peut être passé à la méthode iter() pour créer un itérator. Un itérator peut être passé à la méthode next() qui va donner la prochaine valeur ou soulever un StopIteration exception. Ils retournent eux-mêmes lorsqu’ils sont passés à la méthode iter().4

Nous avons maintenant les bases nécessaires pour bien comprendre les générateurs, car les générateurs sont des itérateurs dans lesquels les méthods iter() et les next() sont automatiquement créées.5 Les générateurs ont donc toutes les caractéristiques des itérables et des itérateurs. Comment invoque-t-on un générateur? Avec le mot «yield».

Return vs yield

Au début de cet article, j’ai écrit que les générateurs se servent du mot «yield» au lieu de «return» comme les autres fonctions. Il est le temps d’analyser la différence entre les deux.

Commençons avec une fonction qui utilise «return». Voici quelques caractéristiques de «return»: 1) lorsqu’on fait appel a une fonction, le code va chercher la fonction en question. Puis exécuter le bloc de code en question et retourner le résultat. Une fois la valeur retournée, la fonction a terminé tout ce qu’elle avait à faire. La prochaine fois qu’on l’appelle, elle recommence à zéro comme elle n’avait jamais été utilisée avant. C’est-à-dire, les fonctions n’ont pas de mémoire une fois la valeur retournée.

Par contre, pour retourner le résultat avec un générateur (qui utilise «yield» au lieu de «return») il faut insérer la valeur dans la fonction next(). Voici un exemple:

function_a()
    >>> "a"
generator_a()
>>> <generator objet a at 0x000001565469DA98>

Pour savoir le prochain objet
>>> next(a())

En fait «yield» est la clé des générateurs. Pensons de la déclaration «yield» comme «return». Il sort de la fonction et il retourne un objet. À la différence «return», lorsque la fonction est appelée à nouveau (via next()), il commence là où il a terminé–à la ligne après la déclaration «yield»–au lieu du début de la fonction.7 illustre très bien la différence entre les fonctions ordinaires et les générateurs. La vidéo commence avec une liste. Puis elle utilise une boucle «for» pour traverser la liste et imprimer les résultats. La voici:

def square_numbers(nums):
     result = []
     for i in nums:
         result.append(i*i)
     return result

my_nums = square_numbers([1,2,3,4,5])
print my_nums # [1, 4, 9, 16, 25

«Return» dans ce cas retourne une liste.

Comment convertissons-nous ce code dans un générateur. Le voici:

my_nums = square_numbers([1,2,3,4,5])
def square_numbers(nums):
    for i in nums:
        yield (i*i)

my_nums = square_numbers([1,2,3,4,5])
print my_nums # <generator object square_numbers at 0x1004dc500>

Le résultat est un objet de générateur. La raison est parce que le générateur ne garde pas le résultat entier en mémoire. À la place, il produit ses résultats une à la fois. Si je veux savoir la valeur retournée, je dois utiliser la fonction next():
print next(my_nums) –> 1

Supposons que j’imprime next(my_nums) plusieurs fois:

 print next(my_nums) --> 1
 print next(my_nums) --> 4
 print next(my_nums) --> 9
 print next(my_nums) --> 16
 print next(my_nums) --> 25
 print next(my_nums) --> Error de StopIteration

StopIteration indique tout simplement que le générateur n’a plus de valeurs à passer.

Point important à noter, dans l’exemple ci-haut le générateur «square_numbers» est attribué à la variable «my_nums» qui instancie l’objet. Cela fait en sorte qu’on puisse itérer à travers «my_nums».

Voici le cas où square_numbers() n’est pas attribué à une variable et on essaie d’itérer à travers:

 next(square_numbers())
     >>> "1"
 next(square_numbers())
     >>> "1"

On continue d’avoir le résultat de la première déclaration de «yield». Pourquoi? Parce ce que le Python tient pour acquis que c’est une nouvelle instance de «square_numbers» et il commence à la première déclaration de «yield». En assignant le générateur à une variable, le Python sait qu’on agit sur le même objet lorsqu’on applique next().

Cela nous amène à une autre caractéristique des générateurs. Une fois qu’on a itéré à travers un générateur, on ne peut plus l’utiliser. Il faut créer une autre instance de square_numbers() pour pouvoir appliquer next() à nouveau.8

Boucle for, itérables et itérateurs

En réalité, il est préférable à utiliser une boucle for pour itérer à travers un générateur plutôt d’utiliser les fonctions next(). Cela explique pourquoi on trouve les générateurs dans les boucles for. Notons aussi que les boucles for exécutent automatiquement la fonction next(). Voici comment se servir d’une boucle for dans l’exemple ci-haut:

for num in my_nums:
     print num

Dans ce cas, on n’aura pas d’erreur de StopIteration, car la boucle sait quand elle doit s’arrêter.

Voici un exemple qui illustre très bien comment les boucles for fonctionnent avec les itérables et les itérateurs:

a = ['foo', 'bar', 'baz']
       for i in a:
           print(i)
       …
       foo
       bar
       baz
Boucle for itérable et itérator

Note que la boucle for applique tous les concepts mentionnés ci-haut.9

Il est important à noter que le générateur ne produit qu’un résultat à la fois jusqu’à ce qu’il n’y ait plus de résultats à produire. Une fois qu’un nouveau résultat est produit. L’ancien est effacé de la mémoire. En contrepartie, la liste produit tous les résultats au complet. En plus, le générateur ne produit la valeur que quand on lui demande de le faire. En conséquence, les générateurs consomment beaucoup moins de mémoire.

Le fait que les générateurs se souviennent de ce qu’ils viennent de faire les distinguent des fonctions habituels! Une fois une fonction utilisé, c’est fini une fois que la valeur est retournée par la fonction. Par contre, un générateur va continuer à donner les valeurs jusqu’à ce qu’il n’y en a plus.

Notons aussi que le générateur a gardé en mémoire la dernière valeur retournée pour pouvoir recommencer après cette valeur une fois que le générateur est appelé à nouveau.

Enfin, le code pour la liste peut être écrit comme une compréhension de liste. Même chose pour la générateur. Mais je n’embarquerai pas dans le sujet dans cet article. Pourquoi? Parce que d’autres l’expliquent très bien et les compréhensions ne sont qu’une façon plus courte d’écrire le code.

Débogueurs et logging

J’ai trouvé que les débogueurs, les logging sont des excellentes façons de voire l’exécution des générateurs. Une belle découverte que j’ai trouvée s’appelle PythonTutor de Philip Guo qui illustre très bien l’exécution du code. En plus, celui-ci se trouve en ligne. Il n’y a donc rien à installer. Pas de mises à jour à faire. Accessible partout où on a une connexion internet, etc.

Cependant, PythonTutor ne marche pas avec Scrapy, car il faut l’installer le module Scrapy et PythonTutor n’installe pas des modules. En conséquence, j’ai dû recourir au débogueur PDB et le logging pour voir l’exécution du code de Scrapy. Voir l’article sur Scrapy pour en savoir davantage.

Conclusion

Dans le contexte de big data et de web scraping, la quantité de données peuvent devenir assez grande. Les garder dans les listes, tuples ou dictionnaires, etc. consommerait beaucoup trop de mémoire. Voilà une des raisons importantes d’utiliser les générateurs avec Scrapy.

Comme Schafer explique, le générateur est beaucoup plus facile à lire que la boucle avec la liste.

Est-ce que j’ai fait le tour des générateurs? J’ai couvert des bases, mais il reste encore des éléments importants à apprendre. Mais avant d’aller plus loin dans ma connaissance du sujet, je dois maîtriser ce que j’ai appris et surtout le mettre en pratique. C’est le sujet d’un autre article dans lequel je vais faire du web scraping avec Scrapy.

  1. Corey Schafer explique très bien les itérables et les itérateurs dans son vidéo «Python Tutorial: Iterators and Iterables – What Are They and How Do They Work?». Cela dit, j’ai dû lire plusieurs blogues et consulter plusieurs livres avant de comprendre qu’il l’explique bien. Mais cela est une autre histoire.
  2. Source: «The Iterator Protocol: How « For Loops » Work in Python» de Trey HUNNER à https://treyhunner.com/2016/12/python-iterator-protocol-how-for-loops-work/
  3. Corey Schafer l’explique très bien dans sa vidéo «Python Tutorial: Iterators and Iterables – What Are They and How Do They Work?» sur YouTube. Je n’ai donc pas besoin d’aller dans les détails.
  4. «The Iterator Protocol: How « For Loops » Work in Python» de Trey HUNNER
  5. Voire la vidéo de Corey Schafer mentionnée dans cet article.
  6. On peut avoir plusieurs yields dans un générateur. Ils vont continuer à retourner leurs valeurs jusqu’à ce qu’il n’y a plus de valeurs à retourner. La vidéo sur youtube de Cory Schafer sur les générateurs6«Python Tutorial: Generators – How to use them and the benefits you receive»

  7. Source: «Python Generators» de Dataquest à https://www.dataquest.io/blog/python-generators-tutorial/
  8. «Python « for » Loops (Definite Iteration)» de John STURTZ à https://realpython.com/python-for-loop/#the-guts-of-the-python-for-loop

Programmation, science de données et marketing

5 commentaires

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *