Petit benchmark...

Coucou,

Au hasard de mes pérégrinations, j’ai souhaité vérifié qu’un fichier correspond à une extension précise, parmi une liste d’extensions bien entendu.

Je fais donc une recherche, pour chaque fichier, parmi toutes les extensions listées pour voir s’il en fait partie ou pas.

J’ai donc commencé par un truc tout bête, genre :

if name[:-3] in (liste des extensions):

Et forcément, ça ne fonctionne que pour des extensions de 3 caractères. Oubliées les “.md” et autres “.epub”

J’ai donc amélioré la chose avec une simple liste de compréhension :

[1 for extension in test_extensions if name.endswith(extension)]

Ici, name est le nom du fichier, test_extensions est la liste d’extensions à tester. Je crée donc une liste qui contiendra un élément si mon fichier correspond à une extension, sinon la liste sera vide. Il me suffit alors de vérifier la longueur de la liste résultante.

Et puis j’ai pensé à utiliser filter qui est souvent bien pratique :

list(filter(lambda x: name.endswith(x), test_extensions))

C’est plus court, mais pas forcément plus lisible je trouve. Pour faire plus court, on peut remplacer list par set (oui, on y gagne 1 seul caractère, c’est de l’optimisation de forcené qui n’a rien d’autre à faire de ses journées :smiley: )

J’ai aussi voulu tester avec map :

list(filter(None, map(name.endswith, test_extensions)))

Et c’est encore plus court finalement ! Ah ah ah ! Mais peut-être encore moins lisible ?

Bon, et forcément, vu que mon projet n’est pas si important, j’ai largement le temps de faire un poil de benchmark :

import pathlib
import random
import timeit

# On prépare les données
extensions = ("txt", "png", "md", "ico", "gif", "zip", "rst", "epub",)
test_extensions = (".txt", ".rst", ".md")
files = []
for i in range(10):
    files.append(f"test.{random.choice(extensions)}")

# On lance les banchmarks
print("set, filter map")
print(timeit.timeit("[len(set(filter(None, map(name.endswith, test_extensions)))) for name in files]", globals=globals(), number=100000))
print("list, filter map")
print(timeit.timeit("[len(list(filter(None, map(name.endswith, test_extensions)))) for name in files]", globals=globals(), number=100000))
print("set, filter lambda")
print(timeit.timeit("[len(set(filter(lambda x: name.endswith(x), test_extensions))) for name in files]", globals=globals(), number=100000))
print("list, filter lambda")
print(timeit.timeit("[len(list(filter(lambda x: name.endswith(x), test_extensions))) for name in files]", globals=globals(), number=100000))
print("simple")
print(timeit.timeit("[len([1 for extension in test_extensions if name.endswith(extension)]) for name in files]", globals=globals(), number=100000))
print("pathlib")
print(timeit.timeit("[pathlib.PurePosixPath(name) in test_extensions for name in files]", globals=globals(), number=100000))

Et voici les résultats (obtenus sur mon PC, sous Windows 10 (je suis au bureau, désolé)) :

set, filter map
0.8425506
list, filter map
0.870825
set, filter lambda
1.0246678000000002
list, filter lambda
1.0588134
simple
0.7103302
pathlib
3.2523364

Émerveillement de la nature, on s’aperçoit que list est légèrement plus lente que set, mais surtout qu’on s’en sort très bien sans filter.
Pour information, l’ajout du calcul de la longueur ne joue pas sur les temps de réponse.

La morale de cette histoire ?

  1. Les fonctions intégrées sont souvent intéressantes, mais elles ne font pas tout
  2. J’ai vraiment du temps à perdre ce matin

Voilà, et bonne journée à vous !

EDIT : Ajout de pathlib, qui est donc à oublier même si très pratique :wink:

3 « J'aime »

J’ajouterais que là où le set serait intéressant c’est sur l’ensemble des extensions autorisées plutôt que sur comme type de retour.

Sinon tu n’as pas trouvé ton bonheur dans le module pathlib ?

1 « J'aime »

Je ne connaissais pas : ajouté !

En fait pour moi la question dépend surtout du nombre d’extensions que tu as à tester. Là tu n’en as que 3 donc c’est ok d’en parcourir la liste pour les tester une à une sur ton nom de fichier.
Même si je ne comprends pas la méthode que tu appliques (que ce soit avec filter ou avec la liste en intension) : tu devrais pouvoir utiliser un any pour t’arrêter à la première occurrence qui matche.

Par contre si tu en avais 100 tu verrais une sacrée différence qui te ferait relativiser ton benchmark, surtout si tu utilisais un set pour stocker ces extensions.
Ensuite tu peux utiliser pathlib ou os.path, c’est une affaire de goûts.

Voilà par exemple ce que ça donne chez moi avec ta liste de 3 éléments :

set, filter map
0.35120165099942824
list, filter map
0.37269529799959855
set, filter lambda
0.4123875030018098
list, filter lambda
0.445023153002694
simple
0.32722216399997706
pathlib
1.315772272002505
pathlib with set
1.4545590229972731
os.path with set
0.5664776830017217

Et maintenant avec 100 extensions

4.246700222000072
list, filter map
4.2696502130020235
set, filter lambda
8.551697688999411
list, filter lambda
10.448909032998927
simple
7.010992734998581
pathlib
10.5267230999998
pathlib with set
1.952741484001308
os.path with set
0.7419984330008447
3 « J'aime »

Fichtre ! C’est intéressant !
Et oui j’ai (re)découvert le any ! Quel idiot-bête d’être passé à côté de ça.
Je vais rejeter un oeil sur tout ça, mais bien entendu la prod a explosée pendant ce temps-là (vendredi tout ça…).

Ne pas oublier trop vite ce package tres pratique - oui c’est beaucoup plus lent si ton seul usage est de créer l’objet Path pour chercher son extension et ensuite le jeter mais je trouve pathlib tres utile pour écrire du code propre, facile a partager en équipe et cross-platform (Windows + Linux) et si t’utilises l’objet Path plus généralement dans ton code alors tu paies la création une seule fois pour bénéficier de ses avantages à chaque inspection / concaténation / itération.

2 « J'aime »

Merci pour ce rappel, je me suis exprimé trop rapidement : à jeter dans mon cas très spécifique uniquement :+1:

1 « J'aime »

.endwith peut prendre un tuple donc pas besoin de map(name.endswith, test_extensions) ou autre truc du genre:

[name.endswith(test_extensions) for name in files]
2 « J'aime »

Ça me fait penser que si on voulait une solution optimale, peut-être que l’idéal serait de construire un arbre de recherche pour vérifier la fin du nom ?

def get_tree():
    tree = {}
    for ext in test_extensions:
	subtree = tree
        for c in reversed(ext):
            subtree = subtree.setdefault(c, {})
        subtree[None] = True
    return tree


def check_tree(file, tree):
    for c in reversed(file):
	tree = tree.get(c)
        if not tree:
            return False
        if None in tree:
            return True

def find_tree(files, tree):
    return [file for file in files if check_tree(file, tree)]

print("find tree")
tree = get_tree()
print(timeit.timeit("find_tree(files, tree)", globals=globals(), number=100000))

Ce qui donne une fois exécuté dans le même environnement que dans mon précédent message (avec test_extensions une liste de 100 éléments) :

find tree
0.17265215200313833

La génération de l’arbre de recherche n’est pas comptabilisée dans le temps de calcul mais est négligeable puisque l’opération n’est exécutée qu’une fois.

1 « J'aime »

Whoup houp houp !

Je vais lire et digérer tout ça, merci pour les infos en tout cas !