Python >> Tutoriel Python >  >> Python

L'optimisation prématurée est la racine de tout Mal

Ce brouillon de chapitre fait partie de mon prochain livre "The Art of Clean Code" (NoStarch 2022).


L'art du code propre

La plupart des développeurs de logiciels perdent des milliers d'heures à travailler avec du code trop complexe. Les huit principes fondamentaux de The Art of Clean Coding vous apprendront à écrire un code clair et maintenable sans compromettre les fonctionnalités. Le principe directeur du livre est la simplicité :réduisez et simplifiez, puis réinvestissez de l'énergie dans les parties importantes pour vous faire gagner d'innombrables heures et faciliter la tâche souvent onéreuse de maintenance du code.

  1. Concentrez-vous sur l'essentiel avec le principe 80/20 — concentrez-vous sur les 20 % de votre code qui comptent le plus
  2. Évitez de coder de manière isolée :créez un produit minimum viable pour obtenir des commentaires rapides
  3. Écrivez le code proprement et simplement pour éliminer l'encombrement
  4. Éviter une optimisation prématurée qui risque de trop compliquer le code
  5. Équilibrez vos objectifs, vos capacités et vos commentaires pour atteindre l'état productif de Flow
  6. Appliquez le bien faire une chose philosophie pour améliorer considérablement la fonctionnalité
  7. Concevez des interfaces utilisateur efficaces avec Moins c'est plus principe
  8. Regroupez vos nouvelles compétences en un seul principe unificateur :Concentrez-vous

L'art du codage propre basé sur Python convient aux programmeurs de tous niveaux, avec des idées présentées de manière indépendante du langage.


Vous découvrirez le concept d'optimisation prématurée et pourquoi cela nuit à la productivité de votre programmation. L'optimisation prématurée est l'un des principaux problèmes d'un code mal écrit. Mais qu'est-ce que c'est ?

Définition de l'optimisation prématurée

Définition  : L'optimisation prématurée consiste à consacrer des ressources précieuses, telles que du temps, des efforts, des lignes de code ou même de la simplicité, à des optimisations de code inutiles.

Il n'y a rien de mal avec un code optimisé.

Le problème est qu'il n'y a pas de repas gratuit. Si vous pensez optimiser des extraits de code, ce que vous faites en réalité consiste à échanger une variable (par exemple, la complexité) contre une autre variable (par exemple, les performances).

Parfois, vous pouvez obtenir du code propre qui est également plus performant et plus facile à lire, mais vous devez passer du temps pour arriver à cet état ! D'autres fois, vous passez prématurément plus de lignes de code sur un algorithme de pointe pour améliorer la vitesse d'exécution. Par exemple, vous pouvez ajouter 30 % de lignes de code supplémentaires pour améliorer la vitesse d'exécution de 0,1 %. Ces types de compromis vont bousiller tout votre processus de développement logiciel lorsqu'ils sont répétés.

Donald Knuth cite une optimisation prématurée

Mais ne me croyez pas sur parole. Voici ce que dit l'un des informaticiens les plus célèbres de tous les temps, Donald Knuth, à propos de l'optimisation prématurée :

"Les programmeurs perdent énormément de temps à penser ou à s'inquiéter de la vitesse des parties non critiques de leurs programmes, et ces tentatives d'efficacité ont en fait un fort impact négatif lorsque le débogage et la maintenance sont pris en compte. Nous devrions oublier les petites efficacités, disons environ 97 % du temps :l'optimisation prématurée est la racine de tous les maux."Donald Knuth

Knuth soutient que la plupart du temps, vous ne devriez pas vous soucier de peaufiner votre code pour obtenir de petits gains d'efficacité. Plongeons-nous dans cinq exemples pratiques d'optimisation prématurée pour voir comment cela peut vous aider.

Six exemples d'optimisation prématurée

Il existe de nombreuses situations où une optimisation prématurée peut se produire. Attention à ceux-là ! Ensuite, je vais vous montrer six exemples, mais je suis sûr qu'il y en a plus.

Optimisation prématurée des fonctions de code

Tout d'abord, vous passez beaucoup de temps à optimiser une fonction de code ou un extrait de code que vous ne supportez pas de laisser non optimisé. Vous soutenez que c'est un mauvais style de programmation d'utiliser la méthode naïve et que vous devriez utiliser des structures de données ou des algorithmes plus efficaces pour résoudre le problème. Ainsi, vous plongez en mode apprentissage, et vous trouvez des algorithmes de mieux en mieux. Enfin, vous choisissez celui qui est considéré comme le meilleur, mais il vous faut des heures et des heures pour le faire fonctionner. L'optimisation était prématurée car, en fin de compte, votre extrait de code n'est exécuté que rarement et n'entraîne pas d'améliorations significatives des performances.

Optimisation prématurée des fonctionnalités du produit logiciel

Deuxièmement, vous ajoutez plus de fonctionnalités à votre produit logiciel parce que vous pensez que les utilisateurs en auront besoin. Vous optimisez pour les besoins attendus mais non prouvés des utilisateurs. Supposons que vous développiez une application pour smartphone qui traduit du texte en codes morse. Au lieu de développer le produit minimum viable (MVP, voir le chapitre 3) qui fait exactement cela, vous ajoutez de plus en plus de fonctionnalités que vous pensez être nécessaires, comme une conversion texte-audio et même un récepteur qui traduit les signaux lumineux en texte. Plus tard, vous découvrirez que vos utilisateurs n'utilisent jamais ces fonctionnalités. Une optimisation prématurée a considérablement ralenti votre cycle de développement de produits et réduit votre vitesse d'apprentissage.

Optimisation prématurée de la phase de planification

Troisièmement, vous optimisez prématurément votre phase de planification, en essayant de trouver des solutions à toutes sortes de problèmes qui peuvent survenir. Bien qu'il soit très coûteux d'éviter de planifier, de nombreuses personnes n'arrêtent jamais de planifier, ce qui peut être tout aussi coûteux ! Ce n'est que maintenant que les coûts sont des coûts d'opportunité de ne pas agir. Faire d'un produit logiciel une réalité nécessite que vous expédiez quelque chose de valeur dans le monde réel, même si cette chose n'est pas encore parfaite. Vous avez besoin des commentaires des utilisateurs et d'une vérification de la réalité avant même de savoir quels problèmes vous frapperont le plus durement. La planification peut vous aider à éviter de nombreux pièges, mais si vous êtes le genre de personne sans penchant pour l'action, toute votre planification n'aura aucune valeur.

Optimisation prématurée de l'évolutivité

Quatrièmement, vous optimisez prématurément l'évolutivité de votre application. Attendant des millions de visiteurs, vous concevez une architecture distribuée qui ajoute dynamiquement des machines virtuelles pour gérer les pics de charge si nécessaire. Les systèmes distribués sont complexes et sujets aux erreurs, et il vous faut des mois pour faire fonctionner votre système. Pire encore, j'ai vu plus de cas où la distribution a réduit l'évolutivité d'une application en raison d'une surcharge accrue pour la communication et la cohérence des données. Les systèmes distribués évolutifs ont toujours un prix :êtes-vous sûr de devoir le payer ? Quel est l'intérêt de pouvoir évoluer vers des millions d'utilisateurs si vous n'avez même pas servi votre premier ?

Optimisation prématurée de la conception des tests

Cinquièmement, vous croyez au développement piloté par les tests et vous insistez sur une couverture de test à 100 %. Certaines fonctions ne se prêtent pas aux tests unitaires en raison de leur entrée non déterministe (par exemple, les fonctions qui traitent le texte libre des utilisateurs). Même s'il a peu de valeur, vous optimisez prématurément pour une couverture parfaite des tests unitaires, et cela ralentit le cycle de développement logiciel tout en introduisant une complexité inutile dans le projet.

Optimisation prématurée de la construction d'un monde orienté objet

Sixièmement, vous croyez à l'orientation objet et insistez pour modéliser le monde à l'aide d'une hiérarchie complexe de classes. Par exemple, vous écrivez un petit jeu vidéo sur la course automobile. Vous créez une hiérarchie de classes où la classe Porsche hérite de la classe Car, qui hérite de la classe Vehicle. Dans de nombreux cas, ces types de structures d'héritage empilés ajoutent une complexité inutile et pourraient être évités. Vous avez prématurément optimisé votre code pour modéliser un monde avec plus de détails que les besoins de l'application.

Exemple de code d'optimisation prématurée qui a mal tourné

Considérons une petite application Python qui devrait servir d'exemple pour un cas où l'optimisation prématurée a mal tourné. Dites, trois collègues Alice, Bob et Carl jouent régulièrement à des parties de poker le soir. Ils doivent garder une trace pendant une soirée de jeu qui doit à qui. Comme Alice est une programmeuse passionnée, elle décide de créer une petite application qui suit les soldes de plusieurs joueurs.

Elle trouve le code qui sert bien l'objectif.

transactions = []
balances = {}


def transfer(sender, receiver, amount):
    transactions.append((sender, receiver, amount))
    if not sender in balances:
        balances[sender] = 0
    if not receiver in balances:
        balances[receiver] = 0
    balances[sender] -= amount
    balances[receiver] += amount


def get_balance(user):
    return balances[user]


def max_transaction():
    return max(transactions, key=lambda x:x[2])


transfer('Alice', 'Bob', 2000)
transfer('Bob', 'Carl', 4000)
transfer('Alice', 'Carl', 2000)

print('Balance Alice: ' + str(get_balance('Alice')))
print('Balance Bob: ' + str(get_balance('Bob')))
print('Balance Carl: ' + str(get_balance('Carl')))

print('Max Transaction: ' + str(max_transaction()))

transfer('Alice', 'Bob', 1000)
transfer('Carl', 'Alice', 8000)

print('Balance Alice: ' + str(get_balance('Alice')))
print('Balance Bob: ' + str(get_balance('Bob')))
print('Balance Carl: ' + str(get_balance('Carl')))

print('Max Transaction: ' + str(max_transaction()))

Liste :script simple pour suivre les transactions et les soldes.

Le script a deux variables globales transactions et balances . La liste transactions suit les transactions telles qu'elles se sont produites lors d'une soirée de jeux. Chaque transaction est un tuple de l'identifiant de l'expéditeur, de l'identifiant du destinataire et du montant à transférer de l'expéditeur au destinataire. Le dictionnaire balances suit le mappage de l'identifiant de l'utilisateur au nombre de crédits en fonction des transactions effectuées.

La fonction transfer(sender, receiver, amount) crée et stocke une nouvelle transaction dans la liste globale, crée de nouveaux soldes pour les utilisateurs expéditeur et destinataire s'ils n'ont pas déjà été créés, et met à jour les soldes en fonction de la transaction. La fonction get_balance(user) renvoie le solde de l'utilisateur donné en argument. La fonction max_transaction() passe en revue toutes les transactions et renvoie celle qui a la valeur maximale dans le troisième élément du tuple :le montant de la transaction.

L'application fonctionne :elle renvoie le résultat suivant :

Balance Alice: -4000
Balance Bob: -2000
Balance Carl: 6000
Max Transaction: ('Bob', 'Carl', 4000)
Balance Alice: 3000
Balance Bob: -1000
Balance Carl: -2000
Max Transaction: ('Carl', 'Alice', 8000)

Mais Alice n'est pas satisfaite de l'application. Elle se rend compte qu'en appelant le max_transaction() entraîne certaines inefficacités dues à des calculs redondants - le script parcourt deux fois la liste des transactions pour trouver la transaction avec le montant maximum. La deuxième fois, il pourrait théoriquement réutiliser le résultat du premier appel et ne regarder que les nouvelles transactions.

Pour rendre le code plus efficace, elle ajoute une autre variable globale max_transaction qui garde une trace du montant maximum de transaction jamais vu.

transactions = []
balances = {}
max_transaction = ('X', 'Y', -9999999)


def transfer(sender, receiver, amount):
…
    if amount > max_transaction[2]:
        max_transaction = (sender, receiver, amount)

En ajoutant plus de complexité au code, il est désormais plus performant, mais à quel prix ? La complexité supplémentaire n'entraîne aucun avantage significatif en termes de performances pour les petites applications pour lesquelles Alice utilise le code. Cela le rend plus compliqué et réduit la maintenabilité. Personne ne reconnaîtra jamais les avantages en termes de performances lors des sessions de jeu en soirée. Mais les progrès d'Alice ralentiront à mesure qu'elle ajoutera de plus en plus de variables globales (par exemple, suivre les montants minimaux des transactions, etc.). L'optimisation était clairement une optimisation prématurée sans besoin d'une application concrète.


Voulez-vous développer les compétences d'un professionnel Python complet —tout en étant payé dans le processus ? Devenez freelance Python et commandez votre livre Leaving the Rat Race with Python sur Amazon (Kindle/Print ) !


Post précédent