Utilisation du principe de responsabilité unique (SRP) en Python lorsque les appels sont coûteux

12

Quelques points de base:

  • Les appels de méthode Python sont "coûteux" en raison de leur nature interprétée . En théorie, si votre code est assez simple, la décomposition du code Python a un impact négatif en plus de la lisibilité et de la réutilisation ( ce qui est un gros gain pour les développeurs, pas tant pour les utilisateurs ).
  • Le principe de responsabilité unique (SRP) maintient le code lisible, est plus facile à tester et à maintenir.
  • Le projet a un arrière-plan spécial où nous voulons du code lisible, des tests et des performances temporelles.

Par exemple, un code comme celui-ci qui appelle plusieurs méthodes (x4) est plus lent que le suivant qui n'en est qu'une.

from operator import add

class Vector:
    def __init__(self,list_of_3):
        self.coordinates = list_of_3

    def move(self,movement):
        self.coordinates = list( map(add, self.coordinates, movement))
        return self.coordinates

    def revert(self):
        self.coordinates = self.coordinates[::-1]
        return self.coordinates

    def get_coordinates(self):
        return self.coordinates

## Operation with one vector
vec3 = Vector([1,2,3])
vec3.move([1,1,1])
vec3.revert()
vec3.get_coordinates()

Par rapport à cela:

from operator import add

def move_and_revert_and_return(vector,movement):
    return list( map(add, vector, movement) )[::-1]

move_and_revert_and_return([1,2,3],[1,1,1])

Si je dois paralléliser quelque chose comme ça, c'est assez objectif de perdre des performances. Attention, ce n'est qu'un exemple; mon projet a plusieurs mini routines avec des mathématiques comme ça - Bien qu'il soit beaucoup plus facile de travailler avec, nos profileurs ne l'aiment pas.


Comment et où adopter le SRP sans compromettre les performances en Python, car sa mise en œuvre inhérente le touche directement?

Existe-t-il des solutions de contournement, comme une sorte de pré-processeur qui met les choses en ligne pour la sortie?

Ou est-ce que Python est tout simplement mauvais pour gérer la panne de code?

lucasgcb
la source
19
Pour ce que ça vaut, vos deux exemples de code ne diffèrent pas en nombre de responsabilités. Le SRP n'est pas un exercice de comptage de méthodes.
Robert Harvey
2
@RobertHarvey Vous avez raison, désolé pour le mauvais exemple et j'en éditerai un meilleur quand j'aurai le temps. Dans les deux cas, la lisibilité et la maintenabilité en souffrent et finalement le SRP tombe en panne dans la base de code alors que nous réduisons les classes et leurs méthodes.
lucasgcb
4
noter que les appels de fonction sont coûteuses dans toutes les langues , bien que les compilateurs AOT ont le luxe de inline
Evoli
6
Utilisez une implémentation JITted de python telle que PyPy. Devrait principalement résoudre ce problème.
Bakuriu

Réponses:

17

Python est-il tout simplement mauvais pour gérer la décomposition du code?

Malheureusement oui, Python est lent et il existe de nombreuses anecdotes sur les gens qui augmentent considérablement les performances en insérant des fonctions et en rendant leur code laid.

Il existe un travail autour, Cython, qui est une version compilée de Python et beaucoup plus rapide.

- Modifier Je voulais juste répondre à certains des commentaires et autres réponses. Bien que leur objectif ne soit peut-être pas spécifique à Python. mais une optimisation plus générale.

  1. N'optimisez pas jusqu'à ce que vous ayez un problème, puis recherchez les goulots d'étranglement

    Généralement de bons conseils. Mais l'hypothèse est que le code «normal» est généralement performant. Ce n'est pas toujours le cas. Les langages et les cadres individuels ont chacun leurs propres particularités. Dans ce cas, les appels de fonction.

  2. Ce n'est que quelques millisecondes, d'autres choses seront plus lentes

    Si vous exécutez votre code sur un ordinateur de bureau puissant, vous ne vous en souciez probablement pas tant que votre code d'utilisateur unique s'exécute en quelques secondes.

    Mais le code métier a tendance à fonctionner pour plusieurs utilisateurs et nécessite plus d'une machine pour prendre en charge la charge. Si votre code s'exécute deux fois plus vite, cela signifie que vous pouvez avoir deux fois plus d'utilisateurs ou la moitié du nombre de machines.

    Si vous êtes propriétaire de vos machines et de votre centre de données, vous disposez généralement d'une grande partie des frais généraux liés à la puissance du processeur. Si votre code est un peu lent, vous pouvez l'absorber, au moins jusqu'à ce que vous ayez besoin d'acheter une deuxième machine.

    En ces jours de cloud computing où vous n'utilisez que la puissance de calcul dont vous avez besoin et pas plus, il y a un coût direct pour le code non performant.

    L'amélioration des performances peut réduire considérablement les dépenses principales d'une entreprise basée sur le cloud et les performances devraient être au premier plan.

Ewan
la source
1
Bien que la réponse de Robert aide à couvrir certaines bases de malentendus potentiels derrière ce type d'optimisation (qui correspond à cette question ), je pense que cela répond à la situation un peu plus directement et en ligne avec le contexte Python.
lucasgcb
2
désolé c'est un peu court. Je n'ai pas le temps d'écrire plus. Mais je pense que Robert a tort sur ce point. Le meilleur conseil avec python semble être de profiler pendant que vous codez. Ne présumez pas qu'il sera performant et n'optimisez que si vous trouvez un problème
Ewan
2
@Ewan: Vous n'avez pas besoin d'écrire tout le programme en premier pour suivre mes conseils. Une ou deux méthodes sont plus que suffisantes pour obtenir un profilage adéquat.
Robert Harvey
1
vous pouvez également essayer pypy, qui est un python JITted
Evoli
2
@Ewan Si vous êtes vraiment préoccupé par la surcharge de performances des appels de fonction, quoi que vous fassiez n'est probablement pas adapté à python. Mais je ne peux vraiment pas penser à beaucoup d'exemples là-bas. La grande majorité du code métier est limitée par les E / S et les tâches lourdes du processeur sont généralement traitées en appelant les bibliothèques natives (numpy, tensorflow, etc.).
Voo
50

De nombreux problèmes de performances potentiels ne sont pas vraiment un problème dans la pratique. Le problème que vous soulevez peut être l'un d'entre eux. Dans la langue vernaculaire, nous appelons s'inquiéter de ces problèmes sans preuve qu'il s'agit de problèmes réels d' optimisation prématurée.

Si vous écrivez un frontal pour un service Web, vos performances ne seront pas affectées de manière significative par les appels de fonction, car le coût d'envoi de données sur un réseau dépasse de loin le temps nécessaire pour effectuer un appel de méthode.

Si vous écrivez une boucle serrée qui rafraîchit un écran vidéo soixante fois par seconde, cela peut avoir de l'importance. Mais à ce stade, je prétends que vous avez des problèmes plus importants si vous essayez d'utiliser Python pour ce faire, un travail pour lequel Python n'est probablement pas bien adapté.

Comme toujours, la façon dont vous le découvrez est de mesurer. Exécutez un profileur de performances ou des minuteurs sur votre code. Voyez si c'est un vrai problème dans la pratique.


Le principe de responsabilité unique n'est pas une loi ou un mandat; c'est une ligne directrice ou un principe. La conception de logiciels est toujours une question de compromis; il n'y a pas d'absolu. Il n'est pas rare de compromis la lisibilité et / ou la maintenabilité pour la vitesse, vous devrez donc peut-être sacrifier SRP sur l'autel de la performance. Mais ne faites pas ce compromis sauf si vous savez que vous avez un problème de performances.

Robert Harvey
la source
3
Je pense que c'était vrai, jusqu'à ce que nous inventions le cloud computing. Maintenant, l'une des deux fonctions coûte effectivement 4 fois plus cher que l'autre
Ewan
2
@Ewan 4 fois peut ne pas avoir d'importance jusqu'à ce que vous l'ayez mesuré pour qu'il soit suffisamment significatif pour vous en soucier. Si Foo prend 1 ms et Bar prend 4 ms, ce n'est pas bon. Jusqu'à ce que vous réalisiez que la transmission des données sur le réseau prend 200 ms. À ce stade, la lenteur de Bar n'a pas tellement d'importance. (Un seul exemple possible où le fait d'être X fois plus lent ne fait pas de différence notable ou percutante, pas nécessairement super réaliste.)
Becuzz
8
@Ewan Si la réduction de la facture vous fait économiser 15 $ / mois mais qu'il faudra un entrepreneur de 125 $ / heure 4 heures pour le réparer et le tester, je pourrais facilement justifier que cela ne vaut pas le temps d'une entreprise à faire (ou du moins ne pas bien faire maintenant si le temps de mise sur le marché est crucial, etc.). Il y a toujours des compromis. Et ce qui a du sens dans une circonstance pourrait ne pas l'être dans une autre.
Becuzz
3
vos factures AWS sont en effet très faibles
Ewan
6
@Ewan AWS arrondit au plafond par lots de toute façon (la norme est de 100 ms). Ce qui signifie que ce type d'optimisation ne vous permet d'économiser quoi que ce soit s'il évite systématiquement de vous pousser vers le segment suivant.
Delioth
2

Tout d'abord, quelques clarifications: Python est un langage. Il existe plusieurs interpréteurs différents qui peuvent exécuter du code écrit en langage Python. L'implémentation de référence (CPython) est généralement ce qui est référencé lorsque quelqu'un parle de "Python" comme s'il s'agissait d'une implémentation, mais il est important d'être précis lorsque l'on parle des caractéristiques de performances, car elles peuvent différer énormément entre les implémentations.

Comment et où adopter le SRP sans compromettre les performances en Python, car sa mise en œuvre inhérente le touche directement?

Cas 1.) Si vous avez du code Python pur (<= Python Language version 3.5, 3.6 a un "support de niveau bêta") qui ne repose que sur des modules Python purs, vous pouvez adopter SRP partout et utiliser PyPy pour l'exécuter. PyPy ( https://morepypy.blogspot.com/2019/03/pypy-v71-released-now-uses-utf-8.html ) est un interpréteur Python qui a un compilateur Just in Time (JIT) et peut supprimer la fonction appelez le temps système tant qu'il a suffisamment de temps pour "s'échauffer" en traçant le code exécuté (quelques secondes IIRC). **

Si vous êtes limité à utiliser l'interpréteur CPython, vous pouvez extraire les fonctions lentes dans des extensions écrites en C, qui seront précompilées et ne souffriront d'aucune surcharge d'interpréteur. Vous pouvez toujours utiliser SRP partout, mais votre code sera divisé entre Python et C. Que ce soit meilleur ou pire pour la maintenabilité que d'abandonner sélectivement SRP mais s'en tenir uniquement au code Python dépend de votre équipe, mais si vous avez des sections critiques de performances de votre , il sera sans aucun doute plus rapide que le code Python pur le plus optimisé interprété par CPython. Beaucoup des bibliothèques mathématiques les plus rapides de Python utilisent cette méthode (numpy et scipy IIRC). Ce qui est une belle transition vers le cas 2 ...

Cas 2.) Si vous avez du code Python qui utilise des extensions C (ou s'appuie sur des bibliothèques qui utilisent des extensions C), PyPy peut être utile ou non selon la façon dont il est écrit. Voir http://doc.pypy.org/en/latest/extending.html pour plus de détails, mais le résumé est que CFFI a une surcharge minimale tandis que CTypes est plus lent (son utilisation avec PyPy peut être encore plus lente que CPython)

Cython ( https://cython.org/ ) est une autre option avec laquelle je n'ai pas autant d'expérience. Je le mentionne par souci d'exhaustivité afin que ma réponse puisse "se suffire à elle-même", mais ne revendique aucune expertise. De mon utilisation limitée, j'ai eu l'impression de devoir travailler plus dur pour obtenir les mêmes améliorations de vitesse que je pouvais obtenir "gratuitement" avec PyPy, et si j'avais besoin de quelque chose de mieux que PyPy, il était tout aussi facile d'écrire ma propre extension C ( ce qui présente l'avantage si je réutilise le code ailleurs ou en extrait une partie dans une bibliothèque, tout mon code peut toujours être exécuté sous n'importe quel interpréteur Python et il n'est pas nécessaire qu'il soit exécuté par Cython).

J'ai peur d'être «enfermé» dans Cython, alors que tout code écrit pour PyPy peut également s'exécuter sous CPython.

** Quelques notes supplémentaires sur PyPy en production

Soyez très prudent lorsque vous faites des choix qui ont pour effet pratique de "vous enfermer" dans PyPy dans une grande base de code. Parce que certaines bibliothèques tierces (très populaires et utiles) ne fonctionnent pas bien pour les raisons mentionnées précédemment, cela peut entraîner des décisions très difficiles plus tard si vous réalisez que vous avez besoin de l'une de ces bibliothèques. Mon expérience consiste principalement à utiliser PyPy pour accélérer certains (mais pas tous) les microservices qui sont sensibles aux performances dans un environnement d'entreprise où il ajoute une complexité négligeable à notre environnement de production (nous avons déjà plusieurs langues déployées, certaines avec différentes versions majeures comme 2.7 vs 3.5 fonctionnant quand même).

J'ai trouvé que l'utilisation de PyPy et de CPython m'obligeait régulièrement à écrire du code qui ne repose que sur les garanties apportées par la spécification du langage lui-même, et non sur les détails d'implémentation qui sont susceptibles de changer à tout moment. Vous pouvez trouver la réflexion sur ces détails comme un fardeau supplémentaire, mais je l'ai trouvé précieux dans mon développement professionnel, et je pense que c'est "sain" pour l'écosystème Python dans son ensemble.

Steven Jackson
la source
Ouais! J'ai envisagé de me concentrer sur les extensions C pour ce cas au lieu d'abandonner le principe et d'écrire du code sauvage, les autres réponses m'ont donné l'impression que ce serait lent, à moins que je n'échange de l'interpréteur de référence - Pour le clarifier, la POO serait toujours être une approche sensée à votre avis?
lucasgcb
1
dans le cas 1 (2e paragraphe), n'obtenez-vous pas les mêmes frais généraux en appelant les fonctions, même si les fonctions elles-mêmes sont respectées?
Ewan
CPython est le seul interprète généralement pris au sérieux. PyPy est intéressant , mais il ne connaît certainement pas d'adoption généralisée. De plus, son comportement diffère de CPython et ne fonctionne pas avec certains packages importants, par exemple scipy. Peu de développeurs sensés recommanderaient PyPy pour la production. En tant que telle, la distinction entre la langue et la mise en œuvre est sans conséquence dans la pratique.
jpmc26
Je pense que vous avez touché le clou sur la tête. Il n'y a aucune raison pour laquelle vous ne pourriez pas avoir un meilleur interprète ou un compilateur. Ce n'est pas intrinsèque au python en tant que langage. Vous êtes juste coincé avec des réalités pratiques
Ewan
@ jpmc26 J'ai utilisé PyPy en production et je recommande de le faire à d'autres développeurs expérimentés. Il est idéal pour les microservices utilisant falconframework.org pour les API de repos légères (à titre d'exemple). Comportement différent car les développeurs s'appuient sur des détails d'implémentation qui NE SONT PAS une garantie du langage n'est pas une raison pour ne pas utiliser PyPy. C'est une raison pour réécrire votre code. Le même code peut de toute façon casser si CPython apporte des modifications à son implémentation (ce qu'il est libre de faire tant qu'il est toujours conforme aux spécifications du langage).
Steven Jackson