En fait, le titre de ce merveilleux article de Jeff Knapp, auteur de "
Writing Idiomatic Python " reflète pleinement son essence. Lisez attentivement et n'hésitez pas à commenter.
Comme nous ne voulions vraiment pas laisser le terme important en lettres latines dans le texte, nous nous sommes permis de traduire le mot "docstring" par "docstring", après avoir découvert ce
terme dans
plusieurs sources en langue russe .
En Python, comme dans la plupart des langages de programmation modernes, une fonction est la principale méthode d'abstraction et d'encapsulation. En tant que développeur, vous avez probablement déjà écrit des centaines de fonctions. Mais fonctions à fonctions - discorde. De plus, si vous écrivez de "mauvaises" fonctions, cela affectera immédiatement la lisibilité et la prise en charge de votre code. Alors, qu'est-ce qu'une «mauvaise» fonction et, surtout, comment en faire une «bonne»?
Actualiser le sujet
Les mathématiques regorgent de fonctions, mais il est difficile de les rappeler. Revenons donc à notre discipline préférée: l'analyse. Vous avez probablement vu des formules comme
f(x) = 2x + 3
. Il s'agit d'une fonction appelée
f
qui prend un argument
x
puis «retourne» deux fois
x + 3
. Bien qu'il ne soit pas trop similaire aux fonctions auxquelles nous sommes habitués en Python, il est complètement similaire au code suivant:
def f(x): return 2*x + 3
Les fonctions existent depuis longtemps en mathématiques, mais en informatique elles sont complètement transformées. Cependant, ce pouvoir n'est pas donné en vain: vous devez passer divers pièges. Voyons ce que devrait être une «bonne» fonction et quelles sont les «cloches et sifflets» typiques des fonctions qui peuvent nécessiter une refactorisation.
Secrets de bonne fonction
Qu'est-ce qui distingue une «bonne» fonction Python d'une fonction médiocre? Vous serez surpris du nombre d'interprétations autorisées par le mot «bon». Dans le cadre de cet article, je considérerai la fonction Python «bonne» si elle satisfait la
plupart des éléments de la liste suivante (parfois, il n'est pas possible de compléter tous les éléments pour une fonction particulière):
- Il est clairement nommé
- Conforme au principe du devoir unique
- Contient Dock
- Renvoie une valeur
- Ne comprend pas plus de 50 lignes
- Elle est idempotente et, si possible, pure
Pour beaucoup d'entre vous, ces exigences peuvent sembler excessivement sévères. Cependant, je vous le promets: si vos fonctions respectent ces règles, elles se révéleront si belles qu'elles transperceront même une licorne avec une larme. Ci-dessous, je vais consacrer une section à chacun des éléments de la liste ci-dessus, puis je terminerai l'histoire en racontant comment ils s'harmonisent les uns avec les autres et aident à créer de bonnes fonctions.
NommerVoici ma citation préférée à ce sujet, souvent attribuée à tort à Donald, mais appartenant en fait à
Phil Carleton :
L'informatique présente deux défis: l'invalidation du cache et la dénomination.
Aussi stupide que cela puisse paraître, la dénomination est vraiment une chose délicate. Voici un exemple d'un "mauvais" nom de fonction:
def get_knn_from_df(df):
Maintenant, de mauvais noms me viennent presque partout, mais cet exemple est tiré du domaine de la science des données (plus précisément, de l'apprentissage automatique), où les praticiens écrivent généralement du code dans un cahier Jupyter, puis essaient d'assembler un programme digestible à partir de ces cellules.
Le premier problème avec le nom de cette fonction est qu'elle utilise des abréviations.
Il est préférable d'utiliser des mots anglais complets plutôt que des abréviations et des abréviations peu connues . La seule raison pour laquelle je veux raccourcir les mots est de ne pas perdre de temps à taper trop de texte, mais
tout éditeur moderne a une fonction de saisie semi-automatique , vous devez donc taper le nom complet de la fonction une seule fois. L'abréviation est un problème, car elle est souvent spécifique à un domaine. Dans le code ci-dessus,
knn
signifie «K-voisins les plus proches» et
df
signifie «DataFrame», une structure de données couramment utilisée dans la bibliothèque
pandas . Si un programmeur qui ne connaît pas ces abréviations lit le code, il ne comprendra presque rien dans le nom de la fonction.
Il existe deux autres défauts mineurs dans le nom de cette fonction. Premièrement, le mot
"get"
redondant. Dans la plupart des fonctions nommées avec compétence, il est immédiatement clair que cette fonction renvoie quelque chose, qui se reflète spécifiquement dans le nom. L'élément
from_d
f n'est pas non plus nécessaire. Soit dans le dock des fonctions, soit (s'il est à la périphérie) dans l'annotation de type, le type du paramètre sera décrit si ces informations
ne sont pas déjà évidentes d'après le nom du paramètre .
Alors, comment renommer cette fonctionnalité? Juste:
def k_nearest_neighbors(dataframe):
Maintenant, même un profane comprend ce qui est calculé dans cette fonction, et le nom du paramètre
(dataframe)
ne laisse aucun doute quant à l'argument qui doit lui être transmis.
Responsabilité exclusive
En développant l'idée de Bob Martin, je dirai que le
principe de la responsabilité exclusive s'applique non moins aux fonctions que les classes et les modules (à propos desquels M. Martin a écrit à l'origine). Selon ce principe (dans notre cas), une fonction devrait avoir une seule responsabilité. Autrement dit, elle doit faire une et une seule chose. L'une des raisons les plus convaincantes pour cela: si une fonction ne fait qu'une chose, alors elle devra être réécrite dans le seul cas: si cette chose doit être faite d'une nouvelle manière. Il devient également clair quand une fonction peut être supprimée; si, en apportant des modifications ailleurs, nous comprenons que le seul devoir d'une fonction n'est plus pertinent, alors nous nous en débarrasserons simplement.
Il vaut mieux donner un exemple. Voici une fonction qui fait plus d'une «chose»:
def calculate_and print_stats(list_of_numbers): sum = sum(list_of_numbers) mean = statistics.mean(list_of_numbers) median = statistics.median(list_of_numbers) mode = statistics.mode(list_of_numbers) print('-----------------Stats-----------------') print('SUM: {}'.format(sum) print('MEAN: {}'.format(mean) print('MEDIAN: {}'.format(median) print('MODE: {}'.format(mode)
A savoir, deux: calcule un ensemble de statistiques sur une liste de nombres et les affiche dans
STDOUT
. Une fonction viole une règle: il doit y avoir une seule raison spécifique pour laquelle elle doit être modifiée. Dans ce cas, il y a deux raisons évidentes pour lesquelles cela est nécessaire: soit vous devez calculer des statistiques nouvelles ou différentes, soit vous devez changer le format de sortie. Par conséquent, il est préférable de réécrire cette fonction sous la forme de deux fonctions distinctes: l'une effectuera les calculs et renverra leurs résultats, et l'autre recevra ces résultats et les affichera dans la console.
Une fonction (ou plutôt, elle a deux responsabilités) avec des abats donne le mot et son nom .
Cette séparation simplifie également considérablement le test de la fonction et vous permet également non seulement de la diviser en deux fonctions au sein du même module, mais même de séparer ces deux fonctions en modules complètement différents, le cas échéant. Cela contribue en outre à des tests plus propres et simplifie la prise en charge du code.
En fait, les fonctions qui effectuent exactement deux choses sont rares. Plus souvent, vous rencontrez des fonctions qui font beaucoup, beaucoup plus d'opérations. Encore une fois, pour des raisons de lisibilité et de testabilité, ces fonctions "multipostes" devraient être divisées en tâches simples, chacune contenant un seul aspect du travail.
Docstrings
Il semblerait que tout le monde soit conscient qu'il existe un document
PEP-8 qui donne des recommandations sur le style du code Python, mais il y a beaucoup moins de personnes parmi nous qui connaissent
PEP-257 , dans lesquelles les mêmes recommandations sont données concernant les cordes d'ancrage. Afin de ne pas relire le contenu du PEP-257, je vous envoie vous-même ce document - à lire pendant votre temps libre. Cependant, ses principales idées sont les suivantes:
- Chaque fonction a besoin d'une chaîne doc.
- Il doit respecter la grammaire et la ponctuation; écrire des phrases complètes
- La docstring commence par une brève description (en une phrase) de ce que fait la fonction.
- La docstring est formulée dans un style prescriptif plutôt que descriptif
Tous ces points sont faciles à suivre lors de l'écriture de fonctionnalités. L'écriture de docstrings devrait devenir une habitude, et essayez de les écrire avant de continuer avec le code de la fonction elle-même. Si vous ne pouvez pas écrire une chaîne de document claire décrivant la fonction, c'est une bonne raison de réfléchir à la raison pour laquelle vous écrivez cette fonction.
Valeurs de retour
Les fonctions peuvent (et
doivent )
être interprétées comme de petits programmes autonomes. Ils prennent des informations sous forme de paramètres et renvoient le résultat. Bien entendu, les paramètres sont facultatifs.
Mais les valeurs de retour sont requises du point de vue de la structure interne de Python . Si vous essayez même d'écrire une fonction qui ne renvoie pas de valeur, vous ne pouvez pas. Si la fonction ne retourne même pas de valeurs, alors l'interpréteur Python la "forcera" à retourner
None
. Ne croyez pas? Essayez-le vous-même:
❯ python3 Python 3.7.0 (default, Jul 23 2018, 20:22:55) [Clang 9.1.0 (clang-902.0.39.2)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> def add(a, b): ... print(a + b) ... >>> b = add(1, 2) 3 >>> b >>> b is None True
Comme vous pouvez le voir, la valeur de
b
est essentiellement
None
. Ainsi, même si vous écrivez une fonction sans instruction de retour, elle renverra toujours quelque chose. Et ça devrait. Après tout, c'est un petit programme, non? Quelle est l'utilité des programmes dont il n'y a pas de conclusion - et donc il est impossible de juger si ce programme a été exécuté correctement? Mais surtout, comment allez-vous
tester un tel programme?
Je n'ai même pas peur de dire ce qui suit: chaque fonction doit renvoyer une valeur utile, au moins pour des raisons de testabilité. Le code que j'écris doit être testé (ce n'est pas discuté). Imaginez à quel point les tests maladroits de la fonction d'
add
ci-dessus peuvent se révéler (indice: vous devrez rediriger les entrées / sorties, après quoi tout ira mal). De plus, en renvoyant une valeur, nous pouvons enchaîner des méthodes et donc écrire du code comme ceci:
with open('foo.txt', 'r') as input_file: for line in input_file: if line.strip().lower().endswith('cat'):
Chaîne
if line.strip().lower().endswith('cat'):
fonctionne car chacune des méthodes de chaîne (
strip()
,
lower()
,
endswith()
) renvoie une chaîne suite à l'appel de la fonction.
Voici quelques raisons courantes qu'un programmeur peut vous donner pour expliquer pourquoi une fonction qu'il écrit ne renvoie pas de valeur:
«C'est juste [une sorte d'opération liée à l'entrée / sortie, par exemple, le stockage d'une valeur dans une base de données]. Ici, je ne peux rien rendre utile. »
Je ne suis pas d'accord. La fonction peut renvoyer True si l'opération s'est terminée avec succès.
"Ici, nous modifions l'un des paramètres disponibles, nous l'utilisons comme paramètre de référence." ""
Voici deux points. Tout d'abord, faites de votre mieux pour ne pas le faire. Deuxièmement, fournir une fonction avec une sorte d'argument pour découvrir qu'elle a changé est surprenant au mieux, et tout simplement dangereux au pire. Au lieu de cela, comme avec les méthodes de chaîne, essayez de renvoyer une nouvelle instance du paramètre qui reflète déjà les modifications qui lui sont appliquées. Même si cela ne fonctionne pas, étant donné que la création d'une copie de certains paramètres est lourde de coûts excessifs, vous pouvez toujours revenir à l'option «Retourner
True
si l'opération s'est terminée avec succès» proposée ci-dessus.
«Je dois renvoyer plusieurs valeurs. Il n'y a pas de valeur unique qu'il serait souhaitable dans ce cas de restituer. »
Cet argument est un peu tiré par les cheveux, mais je l'ai entendu. La réponse, bien sûr, est précisément ce que l'auteur voulait faire - mais ne savait pas comment:
utiliser un tuple pour renvoyer plusieurs valeurs .
Enfin, l'argument le plus fort selon lequel il vaut mieux renvoyer une valeur utile dans tous les cas est que l'appelant peut toujours à juste titre ignorer ces valeurs. En bref, renvoyer une valeur à partir d'une fonction est presque certainement une bonne idée, et il est très peu probable que nous endommagions quoi que ce soit de cette manière, même dans les bases de code existantes.
Longueur de fonction
J'ai admis plus d'une fois que je suis assez stupide. Je peux garder environ trois choses en tête en même temps. Si vous me laissez lire la fonction 200 lignes et demandez ce qu'elle fait, je vais probablement la regarder pendant au moins 10 secondes.
La longueur d'une fonction affecte directement sa lisibilité et donc son support . Par conséquent, essayez de garder vos fonctions courtes. 50 lignes - une valeur prise complètement au plafond, mais cela me semble raisonnable. (J'espère) que la plupart des fonctions que vous écrivez seront beaucoup plus courtes.
Si une fonction est conforme au principe de responsabilité unique, elle sera probablement suffisamment brève. S'il est en train de lire ou idempotent (nous en parlerons) ci-dessous - alors, probablement, cela se révélera également court. Toutes ces idées sont harmonieusement combinées les unes aux autres et aident à écrire du code propre et bon.
Que faire si votre fonction est trop longue?
REFACTOR! Vous devrez probablement refaire tout le temps, même si vous ne connaissez pas le terme.
Le refactoring consiste simplement à changer la structure d'un programme, sans changer son comportement. Par conséquent, extraire plusieurs lignes de code d'une fonction longue et les transformer en fonction indépendante est l'un des types de refactoring. Il s'avère que c'est également le moyen le plus courant et le plus rapide de raccourcir de manière productive les fonctions longues. Comme vous donnez à ces nouvelles fonctions des noms appropriés, le code résultant est beaucoup plus facile à lire. J'ai écrit un livre entier sur le refactoring (en fait, je le fais tout le temps), donc je ne vais pas entrer dans les détails ici. Sachez simplement que si vous avez une fonction trop longue, vous devez la refactoriser.
Idempotence et propreté fonctionnelle
Le titre de cette section peut sembler un peu intimidant, mais conceptuellement, la section est simple. Une fonction idempotente avec le même ensemble d'arguments renvoie toujours la même valeur, quel que soit le nombre de fois où elle est appelée. Le résultat ne dépend pas de variables non locales, de la variabilité des arguments ou des données provenant des flux d'entrée / sortie. La fonction
add_three(number)
suivante est idempotente:
def add_three(number): """ ** + 3.""" return number + 3
Quel que soit le nombre de fois que nous appelons
add_three(7)
, la réponse sera toujours 10. Mais un autre cas est une fonction qui n'est pas idempotente:
def add_three(): """ 3 + , .""" number = int(input('Enter a number: ')) return number + 3
Cette fonction franchement artificielle n'est pas idempotente, car la valeur de retour de la fonction dépend de l'entrée / sortie, à savoir du nombre entré par l'utilisateur. Bien sûr, avec différents appels à
add_three()
valeurs de retour seront différentes. Si nous appelons cette fonction deux fois, l'utilisateur dans le premier cas peut entrer 3 et dans le second - 7, puis deux appels à
add_three()
respectivement 6 et 10.
En dehors de la programmation, il existe également des exemples d'idempotence - par exemple, le bouton haut de l'ascenseur est conçu selon ce principe. En appuyant dessus pour la première fois, nous «informons» l'ascenseur que nous voulons monter. Étant donné que le bouton est idempotent, peu importe combien vous appuyez dessus plus tard, rien de mauvais ne se produira. Le résultat sera toujours le même.
Pourquoi l'idempotence est si importante
Prise en charge de la testabilité et de l'utilisabilité. Les fonctions idempotentes sont faciles à tester, car elles sont garanties de retourner le même résultat dans tous les cas si vous les appelez avec les mêmes arguments. Le test revient à vérifier qu'avec une variété d'appels, la fonction renvoie toujours la valeur attendue. De plus, ces tests seront rapides: la vitesse des tests est un problème important qui est souvent négligé dans les tests unitaires. Et la refactorisation lorsque vous travaillez avec des fonctions idempotentes est généralement une marche facile. Peu importe la façon dont vous changez le code en dehors de la fonction - le résultat de l'appel avec les mêmes arguments sera toujours le même.
Qu'est-ce qu'une fonction «pure»?
En programmation fonctionnelle, une fonction est considérée comme pure si, d'une
part , elle est idempotente, et d'
autre part , elle ne provoque pas les
effets secondaires observés . N'oubliez pas: une fonction est idempotente si elle retourne toujours le même résultat avec un ensemble d'arguments spécifique. Cependant, cela ne signifie pas que la fonction ne peut pas affecter d'autres composants - par exemple, des variables non locales ou des flux d'entrée / sortie. Par exemple, si la version idempotente de la fonction
add_three(number)
ci-dessus
add_three(number)
le résultat à la console, puis le renvoie uniquement, il sera toujours considéré comme idempotent, car lorsqu'il accède au flux d'entrée / sortie, cette opération d'accès n'affecte pas la valeur renvoyée de la fonction. L'appel
print()
n'est qu'un
effet secondaire : interaction avec le reste du programme ou du système en tant que tel, se produisant avec la valeur de retour.
Développons un peu notre exemple avec
add_three(number)
. Vous pouvez écrire le code suivant pour déterminer combien de fois
add_three(number)
a été appelé:
add_three_calls = 0 def add_three(number): """ ** + 3.""" global add_three_calls print(f'Returning {number + 3}') add_three_calls += 1 return number + 3 def num_calls(): """, *add_three*.""" return add_three_calls
Maintenant, nous exécutons la sortie vers la console (c'est un effet secondaire) et modifions la variable non locale (un autre effet secondaire), mais comme aucune de ces valeurs n'affecte la valeur retournée par la fonction, elle est de toute façon idempotente.
La fonction pure n'a pas d'effets secondaires. Non seulement il n'utilise pas de "données externes" lors du calcul de la valeur, mais n'interagit pas avec le reste du programme / système, calcule et renvoie uniquement la valeur spécifiée. Par conséquent, bien que notre nouvelle définition de
add_three(number)
reste idempotente, cette fonction n'est plus pure.
Dans les fonctions pures, il n'y a aucune instruction de journalisation ni appel
print()
. Lorsqu'ils travaillent, ils n'accèdent pas à la base de données et n'utilisent pas de connexions Internet. N'accédez pas et ne modifiez pas les variables non locales.
Et n'appelez pas d'autres fonctions non pures .
En bref, ils n'ont pas "une action terrible à long terme", comme le disent les mots d'Einstein (mais dans le contexte de l'informatique, pas de la physique). Ils n'altèrent en rien le reste du programme ou du système. En
programmation impérative (ce que vous faites lorsque vous écrivez du code en Python), ces fonctions sont les plus sûres. Ils sont connus pour leur testabilité et leur facilité de support; de plus, comme elles sont idempotentes, le test de telles fonctions est garanti aussi rapide que leur exécution. Les tests eux-mêmes sont également simples: vous n'avez pas besoin de vous connecter à la base de données ou de simuler des ressources externes, de préparer la configuration initiale du code et, à la fin du travail, vous n'avez rien à nettoyer.
Honnêtement, l'idempotence et la propreté sont très souhaitables, mais pas obligatoires.
Autrement dit, nous aimerions écrire uniquement des fonctions pures ou idempotentes, en tenant compte de tous les avantages ci-dessus, mais ce n'est pas toujours possible. Le but, cependant, est d'apprendre à écrire du code, en évitant naturellement les effets secondaires et les dépendances externes. Ainsi, chaque ligne de code que nous écrivons deviendra plus facile à tester, même si nous ne pouvons pas gérer avec uniquement des fonctions propres ou idempotentes.Conclusion
C’est tout. , – . . , . – ! . , , , « ». .