Le contexte
Supposons que j'ai le code Python suivant:
def example_function(numbers, n_iters):
sum_all = 0
for number in numbers:
for _ in range(n_iters):
number = halve(number)
sum_all += number
return sum_all
ns = [1, 3, 12]
print(example_function(ns, 3))
example_function
ici, il suffit de parcourir chacun des éléments de la ns
liste et de les diviser par 3, tout en accumulant les résultats. Le résultat de l'exécution de ce script est simplement:
2.0
Puisque 1 / (2 ^ 3) * (1 + 3 + 12) = 2.
Maintenant, disons que (pour une raison quelconque, peut-être le débogage ou la journalisation), je voudrais afficher un certain type d'informations sur les étapes intermédiaires que example_function
prend. Peut-être que je réécrirais alors cette fonction en quelque chose comme ceci:
def example_function(numbers, n_iters):
sum_all = 0
for number in numbers:
print('Processing number', number)
for i_iter in range(n_iters):
number = number/2
print(number)
sum_all += number
print('sum_all:', sum_all)
return sum_all
qui maintenant, lorsqu'il est appelé avec les mêmes arguments que précédemment, génère ce qui suit:
Processing number 1
0.5
0.25
0.125
sum_all: 0.125
Processing number 3
1.5
0.75
0.375
sum_all: 0.5
Processing number 12
6.0
3.0
1.5
sum_all: 2.0
Cela réalise exactement ce que je voulais. Cependant, cela va un peu à l'encontre du principe selon lequel une fonction ne devrait faire qu'une seule chose, et maintenant le code de example_function
est légèrement plus long et plus complexe. Pour une fonction aussi simple, ce n'est pas un problème, mais dans mon contexte, j'ai des fonctions assez compliquées qui s'appellent les unes les autres, et les instructions d'impression impliquent souvent des étapes plus compliquées que celles présentées ici, ce qui entraîne une augmentation substantielle de la complexité de mon code (pour un de mes fonctions, il y avait plus de lignes de code liées à la journalisation que de lignes liées à son objectif réel!).
De plus, si je décide plus tard que je ne veux plus imprimer d'instructions dans ma fonction, je devrais parcourir example_function
et supprimer toutes les print
instructions manuellement, ainsi que toutes les variables liées à cette fonctionnalité, un processus à la fois fastidieux et erroné. -enclin.
La situation devient encore pire si je souhaite toujours avoir la possibilité d'imprimer ou de ne pas imprimer pendant l'exécution de la fonction, ce qui m'amène à déclarer deux fonctions extrêmement similaires (une avec les print
instructions, une sans), ce qui est terrible pour la maintenance, ou pour définir quelque chose comme:
def example_function(numbers, n_iters, debug_mode=False):
sum_all = 0
for number in numbers:
if debug_mode:
print('Processing number', number)
for i_iter in range(n_iters):
number = number/2
if debug_mode:
print(number)
sum_all += number
if debug_mode:
print('sum_all:', sum_all)
return sum_all
ce qui se traduit par une fonction gonflée et (espérons-le) inutilement compliquée, même dans le cas simple de notre example_function
.
Question
Existe-t-il un moyen pythonique de "découpler" la fonctionnalité d'impression de la fonctionnalité d'origine du example_function
?
Plus généralement, existe-t-il un moyen pythonique de dissocier les fonctionnalités facultatives de l'objectif principal d'une fonction?
Ce que j'ai essayé jusqu'à présent:
La solution que j'ai trouvée pour le moment utilise des rappels pour le découplage. Par exemple, on peut réécrire example_function
comme ceci:
def example_function(numbers, n_iters, callback=None):
sum_all = 0
for number in numbers:
for i_iter in range(n_iters):
number = number/2
if callback is not None:
callback(locals())
sum_all += number
return sum_all
puis en définissant une fonction de rappel qui exécute la fonctionnalité d'impression que je souhaite:
def print_callback(locals):
print(locals['number'])
et appeler example_function
comme ça:
ns = [1, 3, 12]
example_function(ns, 3, callback=print_callback)
qui sort ensuite:
0.5
0.25
0.125
1.5
0.75
0.375
6.0
3.0
1.5
2.0
Cela dissocie avec succès la fonctionnalité d'impression de la fonctionnalité de base de example_function
. Cependant, le principal problème avec cette approche est que la fonction de rappel ne peut être exécutée que sur une partie spécifique de example_function
(dans ce cas, juste après la réduction de moitié du nombre actuel), et toute l'impression doit se produire exactement là. Cela oblige parfois la conception de la fonction de rappel à être assez compliquée (et rend certains comportements impossibles à réaliser).
Par exemple, si l'on souhaite obtenir exactement le même type d'impression que je l'ai fait dans une partie précédente de la question (montrant quel numéro est en cours de traitement, avec ses moitiés correspondantes), le rappel résultant serait:
def complicated_callback(locals):
i_iter = locals['i_iter']
number = locals['number']
if i_iter == 0:
print('Processing number', number*2)
print(number)
if i_iter == locals['n_iters']-1:
print('sum_all:', locals['sum_all']+number)
ce qui donne exactement la même sortie que précédemment:
Processing number 1.0
0.5
0.25
0.125
sum_all: 0.125
Processing number 3.0
1.5
0.75
0.375
sum_all: 0.5
Processing number 12.0
6.0
3.0
1.5
sum_all: 2.0
mais est pénible à écrire, à lire et à déboguer.
logging
module pythonlogging
module pourrait aider ici. Bien que ma question utilise desprint
instructions lors de la configuration du contexte, je cherche en fait une solution pour dissocier tout type de fonctionnalité facultative de l'objectif principal d'une fonction. Par exemple, je veux peut-être qu'une fonction trace les choses en cours d'exécution. Dans ce cas, je pense que lelogging
module ne serait même pas applicable.logging
montrent les suggestions d'utilisation ), mais pas comment séparer le code arbitraire.Réponses:
Si vous avez besoin de fonctionnalités en dehors de la fonction pour utiliser des données de l'intérieur de la fonction, il doit y avoir un système de messagerie à l'intérieur de la fonction pour prendre en charge cela. Il n'y a aucun moyen de contourner cela. Les variables locales dans les fonctions sont totalement isolées de l'extérieur.
Le module de journalisation est assez bon pour mettre en place un système de messagerie. Il ne s'agit pas seulement d'imprimer les messages du journal - en utilisant des gestionnaires personnalisés, vous pouvez tout faire.
L'ajout d'un système de messagerie est similaire à votre exemple de rappel, sauf que les endroits où les «rappels» (gestionnaires de journalisation) sont traités peuvent être spécifiés n'importe où à l'intérieur de
example_function
(en envoyant les messages à l'enregistreur). Toutes les variables nécessaires aux gestionnaires de journalisation peuvent être spécifiées lorsque vous envoyez le message (vous pouvez toujours les utiliserlocals()
, mais il est préférable de déclarer explicitement les variables dont vous avez besoin).Un nouveau
example_function
pourrait ressembler à:Cela spécifie trois emplacements où les messages pourraient être traités. En soi, cela
example_function
ne fera rien d'autre que la fonctionnalité deexample_function
lui - même. Il n'imprimera rien ni ne fera aucune autre fonctionnalité.Pour ajouter des fonctionnalités supplémentaires à
example_function
, vous devrez ajouter des gestionnaires à l'enregistreur.Par exemple, si vous souhaitez effectuer une impression des variables envoyées (similaire à votre
debugging
exemple), vous définissez le gestionnaire personnalisé et l'ajoutez à l'example_function
enregistreur:Si vous souhaitez tracer les résultats sur un graphique, définissez simplement un autre gestionnaire:
Vous pouvez définir et ajouter les gestionnaires que vous souhaitez. Ils seront totalement distincts de la fonctionnalité de la
example_function
, et ne peuvent utiliser que les variables que laexample_function
leur donne.Bien que la journalisation puisse être utilisée comme système de messagerie, il pourrait être préférable de passer à un système de messagerie à part entière, tel que PyPubSub , afin qu'il n'interfère pas avec la journalisation réelle que vous pourriez effectuer:
la source
logging
module est en effet plus organisé et maintenable que ce que j'ai proposé d'utiliserprint
et lesif
déclarations. Cependant, il ne dissocie pas la fonctionnalité d'impression de la fonctionnalité principale de laexample_function
fonction. Autrement dit, le principal problème d'avoir àexample_function
faire deux choses à la fois reste toujours, ce qui rend son code plus compliqué que ce que je voudrais qu'il soit.example_function
n'a désormais qu'une seule fonctionnalité et l'impression (ou toute autre fonctionnalité que nous aimerions avoir) se produit en dehors de celle-ci.example_function
est découplé de la fonctionnalité d'impression - la seule fonctionnalité ajoutée à la fonction est l'envoi des messages. Il est similaire à votre exemple de rappel, sauf qu'il n'envoie que les variables spécifiques que vous souhaitez, plutôt que toutes les variableslocals()
. C'est aux gestionnaires de journaux (que vous attachez à l'enregistreur ailleurs) de faire les fonctionnalités supplémentaires (impression, représentation graphique, etc.). Vous n'avez pas besoin de joindre de gestionnaire du tout, auquel cas rien ne se passera lorsque les messages seront envoyés. J'ai mis à jour mon message pour que cela soit plus clair.example_function
. Merci d'avoir rendu cela plus clair maintenant! J'aime vraiment cette réponse, le seul prix à payer est la complexité supplémentaire de la transmission des messages qui, comme vous l'avez mentionné, semble inévitable. Merci également pour la référence à PyPubSub, qui m'a amené à lire sur le modèle d'observateur .Si vous souhaitez vous en tenir à des instructions d'impression, vous pouvez utiliser un décorateur qui ajoute un argument qui active / désactive l'impression sur la console.
Voici un décorateur qui ajoute l'argument de mot clé uniquement et la valeur par défaut de
verbose=False
à n'importe quelle fonction, met à jour la docstring et la signature. L'appel de la fonction telle quelle renvoie la sortie attendue. L'appel de la fonction avecverbose=True
activera les instructions d'impression et renverra la sortie attendue. Cela a l'avantage supplémentaire de ne pas avoir à faire précéder chaque impression d'unif debug:
bloc.Envelopper votre fonction vous permet désormais d'activer / désactiver les fonctions d'impression à l'aide de
verbose
.Exemples:
Lorsque vous inspectez
example_function
, vous verrez également la documentation mise à jour. Puisque votre fonction n'a pas de docstring, c'est juste ce qu'il y a dans le décorateur.En termes de philosophie de codage. Avoir une fonction qui n'entraîne pas d'effets secondaires est un paradigme de programmation fonctionnelle. Python peut être un langage fonctionnel, mais il n'est pas conçu pour être exclusivement de cette façon. Je conçois toujours mon code en pensant à l'utilisateur.
Si l'ajout de l'option pour imprimer les étapes de calcul est un avantage pour l'utilisateur, il n'y a RIEN de mal à le faire. Du point de vue de la conception, vous allez être coincé avec l'ajout des commandes d'impression / journalisation quelque part.
la source
print
etif
énonce. De plus, il parvient à découpler une partie de la fonctionnalité d'impression deexample_function
la fonctionnalité principale de, ce qui était très agréable (j'ai aussi aimé que le décorateur ajoute automatiquement à la docstring, nice touch). Cependant, il ne dissocie pas complètement la fonctionnalité d'impression de la fonctionnalité principale deexample_function
: vous devez toujours ajouter lesprint
instructions et toute logique qui l'accompagne au corps de la fonction.example_function
corps du, pour que sa complexité ne reste associée qu'à la complexité de sa fonctionnalité principale. Dans mon application réelle de tout cela, j'ai une fonction principale qui est déjà considérablement complexe. Ajouter des instructions d'impression / traçage / journalisation à son corps en fait une bête qui a été assez difficile à maintenir et à déboguer.Vous pouvez définir une fonction encapsulant la
debug_mode
condition et transmettre la fonction facultative souhaitée et ses arguments à cette fonction (comme suggéré ici ):Notez que vous
debug_mode
devez évidemment avoir reçu une valeur avant d'appelerDEBUG
.Il est bien sûr possible d'invoquer des fonctions autres que
print
.Vous pouvez également étendre ce concept à plusieurs niveaux de débogage en utilisant une valeur numérique pour
debug_mode
.la source
if
déclarations partout, et facilite également l'activation et la désactivation de l'impression. Cependant, il ne dissocie pas la fonctionnalité d'impression de la fonctionnalité principale deexample_function
. Comparez cela avec par exemple ma suggestion de rappel. À l'aide des rappels, example_function n'a désormais qu'une seule fonctionnalité, et l'impression (ou toute autre fonctionnalité que nous aimerions avoir) se produit en dehors de celle-ci.J'ai mis à jour ma réponse avec une simplification: la fonction
example_function
reçoit un seul rappel ou un crochet avec une valeur par défaut telle qu'elleexample_function
n'a plus besoin de tester pour voir si elle a été transmise ou non:Ce qui précède est une expression lambda qui renvoie
None
etexample_function
peut appeler cette valeur par défaut pourhook
avec n'importe quelle combinaison de paramètres de position et de mot-clé à divers endroits de la fonction.Dans l'exemple ci-dessous, je ne suis intéressé que par les événements
"end_iteration"
et"result
".Tirages:
La fonction crochet peut être aussi simple ou élaborée que vous le souhaitez. Ici, il vérifie le type d'événement et effectue une simple impression. Mais il pourrait obtenir une
logger
instance et enregistrer le message. Vous pouvez avoir toute la richesse de la journalisation si vous en avez besoin mais la simplicité si vous n'en avez pas.la source
example_function
.if
déclarations :)