Contexte

Ce billet s’inscrit dans une série de billets sur le traitement des séquences avec un style fonctionnel en Python.

NB : La version de Python utilisée dans les exemples de code est la version 3.

Principe

La fonction filter est native et prend en paramètre une fonction et une séquence d’éléments ; elle retourne un objet iterator.

La fonction doit prendre un paramètre dont le type correspond aux éléments de la séquence et retourne un booléen (on peut parler de fonction prédicat ou de prédicat).

L’objet iterator est une nouvelle séquence d’éléments ne comportant que les éléments de la séquence initiale pour lesquelles la fonction a retourné vrai : la séquence de départ a été filtrée des éléments faux pour le prédicat.

Par exemple, pour obtenir la liste des entiers strictement positifs inférieurs strictement à 10, avec une boucle for on pourrait procéder comme suit :

1
2
3
4
5
6
even_numbers_below_10_with_for = []
for i in range(1, 10):
    if i % 2 == 0:
        even_numbers_below_10_with_for.append(i)

print("[for] Liste des nombres pairs plus petits que 10 :", even_numbers_below_10_with_for)

Ce qui donne comme affichage :

1
[for] Liste des nombres pairs plus petits que 10 : [2, 4, 6, 8]

On pourrait pour faire la même chose écrire une compréhension comme ce qui suit :

1
2
even_numbers_below_10_with_comprehension = [n for n in range(1,10) if n % 2 == 0]
print("[compréhension] Liste des nombres pairs plus petits que 10 :", even_numbers_below_10_with_comprehension)
1
[compréhension] Liste des nombres pairs plus petits que 10 : [2, 4, 6, 8]

Avec la fonction filter, cela nous donne :

1
2
even_numbers_below_10_with_filter = filter(lambda n: n % 2 == 0, range(1, 10))
print("[filter] Liste des nombres pairs plus petits que 10 :", list(even_numbers_below_10_with_filter))
1
[filter] Liste des nombres pairs plus petits que 10 : [2, 4, 6, 8]

L’écriture de la compréhension et de la fonction filter sont plus compactes que celle de la boucle for. La fonction filter retourne un iterator, on doit d’abord le convertir en liste avant de pouvoir l’afficher sous cette forme.

La fonction filter retourne un iterator

la fonction filter retourne un objet iterator, il est assez facile de le constater. Examinons, le code ci-après :

1
2
3
4
5
6
7
8
houses = ["tyrell", "stark", "lannister", "tarly", "baratheon", "targaryen"]

houses_starting_with_t = filter(lambda s: s.startswith("t"), houses)
print("La fonction filter retourne un objet iterator :", houses_starting_with_t)
print("C'est bien un objet iterator avec les méthodes __iter__ et __next__")
print("Il possède la fonction __iter__ ('__iter__' in dir(houses_starting_with_t)) :", '__iter__' in dir(houses_starting_with_t))
print("Il possède la fonction __next__ ('__next__' in dir(houses_starting_with_t)) :", '__next__' in dir(houses_starting_with_t))
print("L'objet iterator converti en liste :", list(houses_starting_with_t))

Si on exécute ce code on aura un affichage similaire à celui ci_dessous :

1
2
3
4
5
La fonction filter retourne un objet iterator : <filter object at 0x00000228BEA5BFA0>
C'est bien un objet iterator avec les méthodes __iter__ et __next__
Il possède la fonction __iter__ ('__iter__' in dir(houses_starting_with_t)) : True
Il possède la fonction __next__ ('__next__' in dir(houses_starting_with_t)) : True
L'objet iterator converti en liste : ['tyrell', 'tarly', 'targaryen']

L’objet iterator est un objet filter. Il possède des méthodes __iter__ et __next__ nécessaires pour qu’un objet implémente le protocole iterator.

Si on souhaite être précis, la compréhension (house for house in houses if house.startswith("t")) est un véritable équivalent de filter(lambda s: s.startswith("t"), houses), mais pas [house for house in houses if house.startswith("t")] ou la boucle for ci-après :

1
2
3
4
5
6
7
houses = ["tyrell", "stark", "lannister", "tarly", "baratheon", "targaryen"]

houses_starting_with_t = []
for house in houses:
      if house.startswith("t"):
            houses_starting_with_t.append(house)
print(houses_starting_with_t)

A proprement parler ces deux derniers cas produisent des listes et non des objets iterator.

Avec une boucle for pour faire un vrai équivalent à filter, il faut faire une fonction génératrice, comme dans l’exemple ci-dessous :

1
2
3
4
def my_filter(predicate, sequence):
    for elt in sequence:
        if predicate(elt):
            yield elt

implémentation de filter avec une fonction génératrice

Cette fonction retourne un iterator et s’utilise comme tel :

1
2
3
4
5
even_numbers = my_filter(lambda x: x % 2 == 0, range(1, 11))

print(even_numbers)
print(next(even_numbers))
print(list(even_numbers))

Ce qui donne :

1
2
3
<generator object my_filter at 0x0000026C91839510>
2
[4, 6, 8, 10]

Ce qui est intéressant avec les objets iterator, c’est qu’ils sont paresseux et qu’ils peuvent représenter des séquences potentiellement infinies, les éléments n’étant générés qu’à la demande. On peut donc avoir des expressions comme ce qui suit, count produisant lui-même un iterator et une séquence infinie de nombres entiers à partir d’une valeur de départ. Par exemple pour avoir un iterator représentant les nombrs impairs :

1
odd_numbers = filter(lambda x: x % 2 != 0, itertools.count(1))

La variable odd_numbers contient un iterator que l’on peut ensuite manipuler comme tel :

1
2
3
4
5
print("Premier nombre entier impair :", next(odd_numbers))
print("Nombre impair suivant :", next(odd_numbers))
print("Les nombres impairs suivants plus petit que 100 : ", end='')
print(*itertools.takewhile(lambda x: x < 100, odd_numbers))
print("Le nombre impair suivant :", next(odd_numbers))

La fonction takewhile de itertools extrait des valeurs tant que le prédicat passé comme premier paramètre est vraie (ici la lambda lambda x: x < 100). On peut bien sûr faire quelque chose d’équivalent avec une compréhension :

1
2
3
4
5
6
7
8
import itertools


print("L'équivalent avec une compréhension :", end='')
odds_numbers_with_comprehension = (
    x for x in itertools.count(1) if x % 2 != 0)
print(*itertools.takewhile(lambda x: x < 100,
                           odds_numbers_with_comprehension))

Personnellement, je préfère utiliser filter plutôt qu’une compréhension dans un cas comme celui-ci, à moins que je ne souhaite avoir directement une liste. En effet, l’expression me paraît plus compact et traduisant mieux la sémantique. La bonne pratique dans l’écosystème Python est quand même de privilégier les compréhensions qui sont vue comme étant plus pythonique

Filtrer avec None

Il est possible de passer comme premiere paramètre à filter la valeur None plutôt qu’une fonction. Dans ce cas, c’est la fonction identité qui est utilisée comme fonction de filtre. Ainsi toute valeur pouvant s’évaluer à faux dans un contexte booléen sera filtré.

Il faut rappeler pour ceux qui viennent de Java ou de C# par exemple, qu’en Python il n’y a pas que les valeurs True et False qui s’évalue comme un booléen, du moins dans des contextes où l’on est en droit d’attendre un booléen comme la condition d’un if ou d’un while ou ici la valeur de retour d’une fonction qui est utilisée comme un prédicat pour filtrer une séquence d’éléments.

Ainsi, en Python les valeurs suivantes s’évalue à faux dans un contexte booléen : False, None, [], (), {}, set(), "", range(0), 0, 0.0, 0j.

Illsutrons tout cela avec un exemple :

1
2
3
4
5
6
7
8
9
print("Passage de None comme valeur pour le paramètre correspondant à la fonction")
falsy_values = [False, None, [], (), {}, set(), "", range(0), 0, 0.0, 0j]
print("Exemple de valeurs 'falsy' en Python : ", falsy_values)
truthy_values = [True, "Not falsy", 1, [0, 1, 2]]
print("Exemple de valeurs 'truthy' en Python : ", truthy_values)
falsy_and_truthy_values = falsy_values + truthy_values
print("Exemple d'une liste de valeurs 'falsy' et 'truthy'", falsy_and_truthy_values)
print("Liste précédente filtrée de ces valeurs 'falsy' (avec filter) :", list(filter(None,falsy_and_truthy_values)))
print("Liste précédente filtrée de ces valeurs 'falsy' (avec une compréhension) :", [item for item in falsy_and_truthy_values if item])

Cela nous donnera comme affichage dans la console après exécution de ces lignes :

1
2
3
4
5
6
Passage de None comme valeur pour le paramètre correspondant à la fonction
Exemple de valeurs 'falsy' en Python :  [False, None, [], (), {}, set(), '', range(0, 0), 0, 0.0, 0j]
Exemple de valeurs 'truthy' en Python :  [True, 'Not falsy', 1, [0, 1, 2]]
Exemple d'une liste de valeurs 'falsy' et 'truthy' [False, None, [], (), {}, set(), '', range(0, 0), 0, 0.0, 0j, True, 'Not falsy', 1, [0, 1, 2]]
Liste précédente filtrée de ces valeurs 'falsy' (avec filter) : [True, 'Not falsy', 1, [0, 1, 2]]
Liste précédente filtrée de ces valeurs 'falsy' (avec une compréhension) : [True, 'Not falsy', 1, [0, 1, 2]]

Cela peut être un truc intéressant à savoir pour par exemple supprimer d’une séquence de listes les éléments correspondant à la liste vide ou à None.

Filtrer négativement

Si nous reprenons la liste ["tyrell", "stark", "lannister", "tarly", "baratheon", "targaryen"], imaginons que je veuille garder toutes les valeurs qui ne commencent pas par la lettre “t”. Je pourrais écrire :

1
2
houses = ["tyrell", "stark", "lannister", "tarly", "baratheon", "targaryen"]
houses_not_starting_with_t = filter(lambda s: not s.startswith("t"), houses)

Vous avez une alternative fournit par le module itertools, avec la fonction https://docs.python.org/fr/3/library/itertools.html#itertools.filterfalse[filterfalse].

1
2
3
4
5
6
import itertools

houses = ["tyrell", "stark", "lannister", "tarly", "baratheon", "targaryen"]
houses_not_starting_with_t = itertools.filterfalse(lambda s: s.startswith("t"), houses)
print("La fonction filterfalse retourne un objet iterator :", houses_not_starting_with_t)
print("L'objet iterator converti en liste :", list(houses_not_starting_with_t))

Ce qui donne si on exécute le script :

1
2
La fonction filterfalse retourne un objet iterator : <itertools.filterfalse object at 0x0000018FCCA338E0>
L'objet iterator converti en liste : ['stark', 'lannister', 'baratheon']

Tout comme filter, filterfalse prend en paramètre une fonction et une séquence et retourne un iterator.

Tout comme filter, on peut donner à filterfalse la valeur None à la place d’une fonction, avec un fonctionnement similaire : c’est la fonction identité qui sera utilisé avec une interprétation de la valeur retournée dans un contexte booléen, sauf que cette fois-ci ce sont les valeurs s’évaluant à True qui seront supprimées.

1
2
3
4
5
6
7
import itertools

falsy_values = [False, None, [], (), {}, set(), "", range(0), 0, 0.0, 0j]
truthy_values = [True, "Not falsy", 1, [0, 1, 2]]
falsy_and_truthy_values = falsy_values + truthy_values
print("Exemple d'une liste de valeurs 'falsy' et 'truthy'", falsy_and_truthy_values)
print("Liste précédente filtrée de ces valeurs 'truthy' (avec filterfalse) :", list(itertools.filterfalse(None,falsy_and_truthy_values)))

Ce qui donnera :

1
2
Exemple d'une liste de valeurs 'falsy' et 'truthy' [False, None, [], (), {}, set(), '', range(0, 0), 0, 0.0, 0j, True, 'Not falsy', 1, [0, 1, 2]]
Liste précédente filtrée de ces valeurs 'truthy' (avec filterfalse) : [False, None, [], (), {}, set(), '', range(0, 0), 0, 0.0, 0j]

Synthèse

La fonction filter est une fonction native de Python. Elle prend en paramètre une fonction et un iterator ; elle retourne un nouvel iterator ne contenant que les éléments de l'iterator passé en paramètre pour lesquels la fonction passée en paramètre renvoie vrai.

On peut passer la valeur None au lieu de passer une fonction, dans ce cas c’est la fonction identité qui sera utilisée, les valeurs étant alors interprétées dans un contexte booléen.

La fonction filter a un double négatif caché dans itertools, https://docs.python.org/fr/3/library/itertools.html#itertools.filterfalse[filterfalse].

L’ensemble du code du billet est disponible dans un gist.

Ressources