Dans ce texte les termes Python et CPython, qui est l'implémentation de référence du langage, sont utilisés de manière interchangeable. Cet article concerne spécifiquement CPython et ne concerne aucune autre implémentation de Python.
Python est un langage magnifique qui permet à un programmeur d'exprimer ses idées en termes simples, laissant la complexité de la mise en œuvre réelle dans les coulisses.
L'une des choses qu'il résume est le tri.
Vous pouvez facilement trouver la réponse à la question « comment le tri est implémenté en Python ? » ce qui répond presque toujours à une autre question : « Quel algorithme de tri Python utilise-t-il ?
Cependant, cela laisse souvent derrière lui quelques détails d'implémentation intéressants.
Il y a un détail d'implémentation qui, à mon avis, n'est pas suffisamment discuté, même s'il a été introduit il y a plus de sept ans dans python 3.7 :
sorted() et list.sort() ont été optimisés pour les cas courants afin d'être jusqu'à 40 à 75 % plus rapides. (Contribution d'Elliot Gorokhovsky dans bpo-28685.)
Mais avant de commencer...
Lorsque vous devez trier une liste en python, vous avez deux options :
Si vous devez trier un autre itérable intégré, vous ne pouvez utiliser sorted que quel que soit le type d'itérable ou de générateur passé en paramètre.
sorted renvoie toujours une liste car il utilise list.sort en interne.
Voici un équivalent approximatif de l'implémentation C triée de CPython réécrite en python pur :
def sorted(iterable: Iterable[Any], key=None, reverse=False): new_list = list(iterable) new_list.sort(key=key, reverse=reverse) return new_list
Oui, c'est aussi simple que cela.
Comme le dit la documentation interne de Python pour le tri :
Il est parfois possible de remplacer des comparaisons spécifiques à un type plus rapides par le PyObject_RichCompareBool générique plus lent
Et en bref cette optimisation peut être décrite ainsi :
Lorsqu'une liste est homogène, Python utilise fonction de comparaison spécifique au type
Une liste homogène est une liste qui contient des éléments d'un seul type.
Par exemple:
homogeneous = [1, 2, 3, 4]
En revanche, il ne s'agit pas d'une liste homogène :
heterogeneous = [1, "2", (3, ), {'4': 4}]
Fait intéressant, le didacticiel officiel de Python indique :
Les listes sont mutables et leurs éléments sont généralement homogènes et sont accessibles en itérant sur la liste
Ce même tutoriel indique :
Les tuples sont immuables et contiennent généralement une séquence hétérogène d'éléments
Donc, si jamais vous vous demandez quand utiliser un tuple ou une liste, voici une règle générale :
si les éléments sont du même type, utilisez une liste, sinon utilisez un tuple
Python implémente un objet conteneur de tableau homogène pour les valeurs numériques.
Cependant, depuis Python 3.12, les tableaux n'implémentent pas leur propre méthode de tri.
La seule façon de les trier est d'utiliser sorted, qui crée en interne une liste à partir du tableau, effaçant ainsi toutes les informations liées au type.
Les comparaisons en python sont coûteuses, car Python effectue diverses vérifications avant d'effectuer une comparaison réelle.
Voici une explication simplifiée de ce qui se passe sous le capot lorsque vous comparez deux valeurs en python :
En plus de cela, les fonctions de comparaison propres à chaque type implémentent des vérifications supplémentaires.
Par exemple, lors de la comparaison de chaînes, Python vérifiera si les caractères de la chaîne occupent plus d'un octet de mémoire, et la comparaison float comparera différemment une paire de float, un float et un int.
Une explication et un diagramme plus détaillés peuvent être trouvés ici : Ajout d'optimisations de tri basées sur les données à CPython
Avant l'introduction de cette optimisation, Python devait exécuter toutes ces vérifications spécifiques au type et non spécifiques au type chaque fois que deux valeurs étaient comparées lors du tri.
Il n'y a pas de moyen magique de savoir si tous les éléments d'une liste sont du même type, si ce n'est de parcourir la liste et de vérifier chaque élément.
Python fait presque exactement cela : vérifier les types de clés de tri générées par la fonction clé transmise à list.sort ou triées en tant que paramètre
Si une fonction clé est fournie, Python l'utilise pour construire une liste de clés, sinon il utilise les propres valeurs de la liste comme clés de tri.
De manière simplifiée à l'extrême, la construction des clés peut être exprimée comme le code python suivant.
if key is None: keys = list_items else: keys = [key(list_item) for list_item in list_item]
Notez que les clés utilisées en interne dans CPython sont un tableau C de références d'objets CPython, et non une liste Python
Une fois les clés construites, Python vérifie leurs types.
Lors de la vérification des types de clés, l'algorithme de tri de Python tente de déterminer si tous les éléments du tableau de clés sont soit str, int, float ou tuple, ou simplement du même type, avec quelques contraintes pour les types de base.
Il convient de noter que la vérification des types de clés ajoute un travail supplémentaire au départ. Python fait cela parce que cela s'avère généralement rentable en rendant le tri plus rapide, en particulier pour les listes plus longues.
int ne devrait pas être un bignum
En pratique, cela signifie que pour que cette optimisation fonctionne, l'entier doit être inférieur à 2^30 - 1 (cela peut varier selon la plateforme)
En remarque, voici un excellent article qui explique comment Python gère les gros entiers : # Comment Python implémente les entiers super longs ?
Tous les caractères d'une chaîne doivent occuper moins d'un octet de mémoire, ce qui signifie qu'ils doivent être représentés par des valeurs entières comprises entre 0 et 255
En pratique, cela signifie que les chaînes ne doivent être constituées que de caractères latins, d'espaces et de certains caractères spéciaux trouvés dans la table ASCII.
Il n'y a aucune contrainte pour les flottants pour que cette optimisation fonctionne.
Tout d’abord, n’est-ce pas passionnant à savoir ?
Deuxièmement, mentionner ces connaissances pourrait être une bonne idée lors d'un entretien avec un développeur Python.
En ce qui concerne le développement réel du code, comprendre cette optimisation peut vous aider à améliorer les performances de tri.
Selon le benchmark du PR qui a introduit cette optimisation, le tri d'une liste composée uniquement de flottants plutôt que d'une liste de flottants avec ne serait-ce qu'un seul entier à la fin est presque deux fois plus rapide.
Alors, quand il est temps d'optimiser, transformer une liste comme celle-ci
floats_and_int = [1.0, -1.0, -0.5, 3]
Dans une liste qui ressemble à ceci
just_floats = [1.0, -1.0, -0.5, 3.0] # note that 3.0 is a float now
pourrait améliorer les performances.
Bien que l'optimisation du tri de Python fonctionne bien avec les types intégrés, il est important de comprendre comment elle interagit avec les classes personnalisées.
Lors du tri des objets de classes personnalisées, Python s'appuie sur les méthodes de comparaison que vous définissez, telles que __lt__ (inférieur à) ou __gt__ (supérieur à).
Cependant, l'optimisation spécifique au type ne s'applique pas aux classes personnalisées.
Python utilisera toujours la méthode de comparaison générale pour ces objets.
Voici un exemple :
class MyClass: def __init__(self, value): self.value = value def __lt__(self, other): return self.valueDans ce cas, Python utilisera la méthode __lt__ pour les comparaisons, mais il ne bénéficiera pas de l'optimisation spécifique au type. Le tri fonctionnera toujours correctement, mais il ne sera peut-être pas aussi rapide que le tri des types intégrés.
Si les performances sont essentielles lors du tri des objets personnalisés, envisagez d'utiliser une fonction clé qui renvoie un type intégré :
sorted_list = sorted(my_list, key=lambda x: x.value)Épilogue
Une optimisation prématurée, en particulier en Python, est mauvaise.
Vous ne devez pas concevoir l'intégralité de votre application autour d'optimisations spécifiques dans CPython, mais il est bon d'être conscient de ces optimisations : bien connaître vos outils est un moyen de devenir un développeur plus compétent.
Être attentif à de telles optimisations vous permet d'en profiter lorsque la situation l'exige, notamment lorsque les performances deviennent critiques :
Considérez un scénario dans lequel votre tri est basé sur des horodatages : l'utilisation d'une liste homogène d'entiers (horodatages Unix) au lieu d'objets datetime pourrait exploiter efficacement cette optimisation.
Cependant, il est crucial de se rappeler que la lisibilité et la maintenabilité du code doivent avoir la priorité sur de telles optimisations.
Bien qu'il soit important de connaître ces détails de bas niveau, il est tout aussi important d'apprécier les abstractions de haut niveau de Python qui en font un langage si productif.
Python est un langage étonnant, et explorer ses profondeurs peut vous aider à mieux le comprendre et à devenir un meilleur programmeur Python.
Clause de non-responsabilité: Toutes les ressources fournies proviennent en partie d'Internet. En cas de violation de vos droits d'auteur ou d'autres droits et intérêts, veuillez expliquer les raisons détaillées et fournir une preuve du droit d'auteur ou des droits et intérêts, puis l'envoyer à l'adresse e-mail : [email protected]. Nous nous en occuperons pour vous dans les plus brefs délais.
Copyright© 2022 湘ICP备2022001581号-3