Définition de votre propre fonction Python – Real Python

By | mars 9, 2020

trouver un expert Python

Tout au long des didacticiels précédents de cette série, vous avez vu de nombreux exemples illustrant l'utilisation des fonctions Python intégrées. Dans ce didacticiel, vous apprendrez à définir votre propre fonction Python. Vous apprendrez quand diviser votre programme en fonctions distinctes définies par l'utilisateur et quels outils vous aurez besoin pour ce faire.

Voici ce que vous apprendrez dans ce didacticiel:

  • Comment les fonctions travailler en Python et pourquoi ils sont bénéfiques
  • Comment définir et appeler votre propre fonction Python
  • Mécanismes pour passer des arguments à votre fonction
  • Comment renvoyer des données de votre fonction à l'environnement d'appel

Fonctions en Python

Vous connaissez peut-être le concept mathématique d'un une fonction. Une fonction est une relation ou un mappage entre une ou plusieurs entrées et un ensemble de sorties. En mathématiques, une fonction est généralement représentée comme ceci:

fonction mathématique

Ici, F est une fonction qui opère sur les entrées X et y. La sortie de la fonction est z. Cependant, les fonctions de programmation sont beaucoup plus généralisées et polyvalentes que cette définition mathématique. En fait, une définition et une utilisation appropriées des fonctions sont si essentielles au bon développement logiciel que pratiquement tous les langages de programmation modernes prennent en charge les fonctions intégrées et définies par l'utilisateur.

En programmation, un une fonction est un bloc de code autonome qui encapsule une tâche spécifique ou un groupe de tâches associé. Dans les didacticiels précédents de cette série, vous avez été familiarisé avec certaines des fonctions intégrées fournies par Python. id (), par exemple, prend un argument et renvoie l'identifiant entier unique de cet objet:

>>>

>>> s = «foobar»
>>> id(s)
56313440

len () renvoie la longueur de l'argument qui lui est passé:

>>>

>>> une = [[[['foo', 'bar', «baz», «qux»]
>>> len(une)
4

tout() prend un itérable comme argument et retourne Vrai si l'un des éléments de l'itérable est véridique et Faux autrement:

>>>

>>> tout([[[[Faux, Faux, Faux])
Faux
>>> tout([[[[Faux, Vrai, Faux])
Vrai

>>> tout([[[['bar' == «baz», len('foo') == 4, «qux» dans 'foo', 'bar', «baz»])
Faux
>>> tout([[[['bar' == «baz», len('foo') == 3, «qux» dans 'foo', 'bar', «baz»])
Vrai

Chacune de ces fonctions intégrées exécute une tâche spécifique. Le code qui accomplit la tâche est défini quelque part, mais vous n'avez pas besoin de savoir où ni même comment fonctionne le code. Tout ce que vous devez savoir, c'est interface:

  1. Quoi arguments (le cas échéant) il faut
  2. Quoi valeurs (le cas échéant) il retourne

Ensuite, vous appelez la fonction et passez les arguments appropriés. L'exécution du programme va au corps de code désigné et fait sa chose utile. Lorsque la fonction est terminée, l'exécution revient à votre code là où elle s'était arrêtée. La fonction peut renvoyer ou non des données pour que votre code les utilise, comme le font les exemples ci-dessus.

Lorsque vous définissez votre propre fonction Python, cela fonctionne de la même manière. Quelque part dans votre code, vous appellerez votre fonction Python et l'exécution du programme sera transférée vers le corps de code qui compose la fonction.

Une fois la fonction terminée, l'exécution revient à l'emplacement où la fonction a été appelée. Selon la façon dont vous avez conçu l'interface de la fonction, les données peuvent être transmises lors de l'appel de la fonction et les valeurs renvoyées peuvent être renvoyées à la fin.

L'importance des fonctions Python

Pratiquement tous les langages de programmation utilisés aujourd'hui prennent en charge une forme de fonctions définies par l'utilisateur, bien qu'ils ne soient pas toujours appelés fonctions. Dans d'autres langues, vous pouvez les voir sous l'un des noms suivants:

  • Sous-programmes
  • Procédures
  • Les méthodes
  • Sous-programmes

Alors, pourquoi s'embêter à définir des fonctions? Il y a plusieurs très bonnes raisons. Passons en revue quelques-uns maintenant.

Abstraction et réutilisabilité

Supposons que vous écriviez du code qui fait quelque chose d'utile. Au fur et à mesure que vous poursuivez le développement, vous constatez que la tâche effectuée par ce code est celle dont vous avez souvent besoin, dans de nombreux emplacements différents de votre application. Que devrais tu faire? Eh bien, vous pouvez simplement répliquer le code encore et encore, en utilisant la fonction de copier-coller de votre éditeur.

Plus tard, vous déciderez probablement que le code en question doit être modifié. Soit vous y trouverez quelque chose qui ne va pas, qui doit être corrigé, soit vous souhaitez l'améliorer d'une manière ou d'une autre. Si des copies du code sont éparpillées dans votre application, vous devrez effectuer les modifications nécessaires à chaque emplacement.

Une meilleure solution consiste à définir une fonction Python qui exécute la tâche. N'importe où dans votre application dont vous avez besoin pour accomplir la tâche, vous appelez simplement la fonction. Au bout du compte, si vous décidez de modifier son fonctionnement, il vous suffit de modifier le code à un emplacement, qui est l'endroit où la fonction est définie. Les modifications seront automatiquement récupérées partout où la fonction est appelée.

le abstraction de fonctionnalité dans une définition de fonction est un exemple du principe de ne pas se répéter (DRY) du développement logiciel. C'est sans doute la motivation la plus forte pour utiliser des fonctions.

Modularité

Les fonctions permettent processus complexes être divisé en étapes plus petites. Imaginez, par exemple, que vous disposez d'un programme qui lit un fichier, traite le contenu du fichier, puis écrit un fichier de sortie. Votre code pourrait ressembler à ceci:

# Programme principal

# Code dans lequel lire le fichier
<déclaration>
<déclaration>
<déclaration>
<déclaration>

# Code pour traiter le fichier
<déclaration>
<déclaration>
<déclaration>
<déclaration>

# Code pour écrire le fichier
<déclaration>
<déclaration>
<déclaration>
<déclaration>

Dans cet exemple, le programme principal est un tas de code enchaîné dans une longue séquence, avec des espaces et des commentaires pour aider à l'organiser. Cependant, si le code devenait beaucoup plus long et plus complexe, vous auriez de plus en plus de mal à vous en tenir à la tête.

Alternativement, vous pouvez structurer le code plus comme suit:

def read_file():
    # Code dans lequel lire le fichier
    <déclaration>
    <déclaration>
    <déclaration>
    <déclaration>

def fichier_processus():
    # Code pour traiter le fichier
    <déclaration>
    <déclaration>
    <déclaration>
    <déclaration>

def fichier_écriture():
    # Code pour écrire le fichier
    <déclaration>
    <déclaration>
    <déclaration>
    <déclaration>


# Programme principal
read_file()
fichier_processus()
fichier_écriture()

Cet exemple est modularisé. Au lieu que tout le code soit enchaîné, il est divisé en fonctions distinctes, chacune se concentrant sur une tâche spécifique. Ces tâches sont lis, processus, et écrire. Le programme principal n'a plus qu'à appeler chacun d'eux à tour de rôle.

Dans la vie, vous faites ce genre de choses tout le temps, même si vous n'y pensez pas explicitement. Si vous vouliez déplacer des étagères pleines de trucs d'un côté à l'autre de votre garage, alors avec un peu de chance, vous ne vous tiendriez pas là et penseriez sans but: «Oh, bon sang. J'ai besoin de déplacer tout ça là-bas! Comment je fais ça???" Vous divisez le travail en étapes gérables:

  1. Prendre tous les trucs sur les étagères.
  2. Prendre les étagères à part.
  3. Porter les pièces de l'étagère à travers le garage vers le nouvel emplacement.
  4. Remonter les étagères.
  5. Porter les trucs à travers le garage.
  6. Mettre les trucs de retour sur les étagères.

La division d'une tâche volumineuse en sous-tâches plus petites et plus petites facilite la réflexion et la gestion de la tâche volumineuse. À mesure que les programmes deviennent plus compliqués, il devient de plus en plus avantageux de les modulariser de cette manière.

Séparation des espaces de noms

UNE espace de noms est une région d'un programme dans lequel identifiants avoir un sens. Comme vous le verrez ci-dessous, lorsqu'une fonction Python est appelée, un nouvel espace de noms est créé pour cette fonction, distinct de tous les autres espaces de noms qui existent déjà.

Le résultat pratique de ceci est que les variables peuvent être définies et utilisées dans une fonction Python même si elles ont le même nom que les variables définies dans d'autres fonctions ou dans le programme principal. Dans ces cas, il n'y aura pas de confusion ou d'interférence car ils sont conservés dans des espaces de noms séparés.

Cela signifie que lorsque vous écrivez du code dans une fonction, vous pouvez utiliser des noms de variables et des identifiants sans vous soucier de savoir s'ils sont déjà utilisés ailleurs en dehors de la fonction. Cela permet de réduire considérablement les erreurs de code.

J'espère que vous êtes suffisamment convaincu des vertus des fonctions et désireux d'en créer! Voyons comment.

Appels de fonctions et définition

La syntaxe habituelle pour définir une fonction Python est la suivante:

def <nom_fonction>([[[[<paramètres>]):
    <déclaration(s)>

Les composants de la définition sont expliqués dans le tableau ci-dessous:

Composant Sens
def Le mot-clé qui informe Python qu'une fonction est en cours de définition
Un identifiant Python valide qui nomme la fonction
Une liste de paramètres optionnels, séparés par des virgules, qui peuvent être passés à la fonction
: Ponctuation qui indique la fin de l'en-tête de la fonction Python (le nom et la liste des paramètres)
Un bloc d'instructions Python valides

Le dernier élément, , est appelé corps de la fonction. Le corps est un bloc d'instructions qui sera exécuté lors de l'appel de la fonction. Le corps d'une fonction Python est défini par indentation conformément à la règle du hors-jeu. C'est la même chose que les blocs de code associés à une structure de contrôle, comme un si ou tandis que déclaration.

La syntaxe pour appeler une fonction Python est la suivante:

<nom_fonction>([[[[<arguments>])

sont les valeurs passées dans la fonction. Ils correspondent aux dans la définition de la fonction Python. Vous pouvez définir une fonction qui ne prend aucun argument, mais les parenthèses sont toujours obligatoires. Une définition de fonction et un appel de fonction doivent toujours inclure des parenthèses, même s'ils sont vides.

Comme d'habitude, vous commencerez par un petit exemple et ajouterez de la complexité à partir de là. En gardant à l'esprit la tradition mathématique séculaire, vous appellerez votre première fonction Python F(). Voici un fichier de script, foo.py, qui définit et appelle F():

    1 def F():
    2     s = «- Inside f ()»
    3     impression(s)
    4 
    5 impression(«Avant d'appeler f ()»)
    6 F()
    sept impression(«Après avoir appelé f ()»)

Voici comment ce code fonctionne:

  1. Ligne 1 utilise le def mot-clé pour indiquer qu'une fonction est en cours de définition. Exécution du def déclaration crée simplement la définition de F(). Toutes les lignes suivantes en retrait (lignes 2 à 3) font partie du corps de F() et sont stockés comme sa définition, mais ils ne sont pas encore exécutés.

  2. Ligne 4 est un peu d'espace entre la définition de la fonction et la première ligne du programme principal. Bien que ce ne soit pas syntaxiquement nécessaire, c'est bien d'avoir. Pour en savoir plus sur les espaces autour des définitions de fonction Python de niveau supérieur, consultez Écrire un beau code Pythonic avec PEP 8.

  3. Ligne 5 est la première déclaration qui n'est pas en retrait car elle ne fait pas partie de la définition de F(). C’est le début du programme principal. Lorsque le programme principal s'exécute, cette instruction est exécutée en premier.

  4. Ligne 6 est un appel à F(). Notez que des parenthèses vides sont toujours requises à la fois dans une définition de fonction et dans un appel de fonction, même en l'absence de paramètres ou d'arguments. L'exécution se poursuit F() et les déclarations dans le corps de F() sont exécutés.

  5. Ligne 7 est la ligne suivante à exécuter une fois que le corps de F() avoir fini. L'exécution revient à cela impression() déclaration.

La séquence d'exécution (ou contrôler le flux) pour foo.py est illustré dans le diagramme suivant:

Appel de fonction Python

Quand foo.py est exécuté à partir d'une invite de commande Windows, le résultat est le suivant:

C:  Users  john  Documents  Python  doc>python foo.py
Avant d'appeler f ()
- Intérieur f ()
Après avoir appelé f ()

Parfois, vous souhaiterez peut-être définir une fonction vide qui ne fait rien. C'est ce qu'on appelle un talon, qui est généralement un espace réservé temporaire pour une fonction Python qui sera entièrement implémentée ultérieurement. Tout comme un bloc dans une structure de contrôle ne peut pas être vide, le corps d’une fonction non plus. Pour définir une fonction de stub, utilisez le passer déclaration:

>>>

>>> def F():
...     passer
...
>>> F()

Comme vous pouvez le voir ci-dessus, un appel à une fonction de stub est syntaxiquement valide mais ne fait rien.

Passage d'argument

Jusqu'à présent dans ce didacticiel, les fonctions que vous avez définies n'ont pris aucun argument. Cela peut parfois être utile, et vous écrirez occasionnellement de telles fonctions. Plus souvent, cependant, vous voudrez passer des données dans une fonction de sorte que son comportement peut varier d'une invocation à l'autre. Voyons comment procéder.

Arguments positionnels

La façon la plus simple de passer des arguments à une fonction Python est avec arguments positionnels (aussi appelé arguments requis). Dans la définition de fonction, vous spécifiez une liste de paramètres séparés par des virgules entre parenthèses:

>>>

>>> def F(qté, article, prix):
...     impression(F"qté article    coût $prix: .2f")
...

Lorsque la fonction est appelée, vous spécifiez une liste d'arguments correspondante:

>>>

>>> F(6, 'bananes', 1,74)
6 bananes coûtent 1,74 $

Les paramètres (qté, article, et prix) se comporter comme les variables qui sont définis localement à la fonction. Lorsque la fonction est appelée, les arguments passés (6, 'bananes', et 1,74) sont lié aux paramètres dans l'ordre, comme par affectation de variable:

Paramètre Argument
qté 6
article bananes
prix 1,74

Dans certains textes de programmation, les paramètres donnés dans la définition de fonction sont appelés paramètres formelset les arguments de l'appel de fonction sont appelés paramètres réels:

Différence entre paramètres et arguments

Bien que les arguments positionnels soient le moyen le plus simple de transmettre des données à une fonction, ils offrent également le moins de flexibilité. Pour commencer, le commande des arguments de l'appel doit correspondre à l'ordre des paramètres dans la définition. Rien ne vous empêche de spécifier des arguments de position dans le désordre, bien sûr:

>>>

>>> F('bananes', 1,74, 6)
les bananes 1,74 coûtent 6,00 $

La fonction peut même continuer de fonctionner, comme dans l'exemple ci-dessus, mais il est très peu probable qu'elle produise les résultats corrects. C'est la responsabilité du programmeur qui définit la fonction de documenter ce que le arguments appropriés devrait être, et il incombe à l'utilisateur de la fonction de prendre connaissance de ces informations et de les respecter.

Avec les arguments positionnels, les arguments de l'appel et les paramètres de la définition doivent concorder non seulement dans l'ordre mais nombre ainsi que. C’est la raison pour laquelle les arguments positionnels sont également appelés arguments requis. Vous ne pouvez en laisser aucun lors de l'appel de la fonction:

>>>

>>> # Trop peu d'arguments
>>> F(6, 'bananes')
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
    F(6, 'bananes')
Erreur-type: f () manque 1 argument positionnel requis: «prix»

Vous ne pouvez pas non plus en spécifier d'autres:

>>>

>>> # Trop d'arguments
>>> F(6, 'bananes', 1,74, «kumquats»)
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
    F(6, 'bananes', 1,74, «kumquats»)
Erreur-type: f () prend 3 arguments positionnels mais 4 ont été donnés

Les arguments de position sont conceptuellement simples à utiliser, mais ils ne sont pas très indulgents. Vous devez spécifier le même nombre d'arguments dans l'appel de fonction car il y a des paramètres dans la définition, et exactement dans le même ordre. Dans les sections qui suivent, vous verrez des techniques de transmission d'arguments qui assouplissent ces restrictions.

Arguments des mots clés

Lorsque vous appelez une fonction, vous pouvez spécifier des arguments sous la forme =. Dans ce cas, chaque doit correspondre à un paramètre dans la définition de la fonction Python. Par exemple, la fonction précédemment définie F() peut être appelé avec arguments de mots clés comme suit:

>>>

>>> F(qté=6, article='bananes', prix=1,74)
6 bananes coûtent 1,74 $

Le référencement d'un mot clé qui ne correspond à aucun des paramètres déclarés génère une exception:

>>>

>>> F(qté=6, article='bananes', Coût=1,74)
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
Erreur-type: f () a obtenu un argument de mot clé inattendu 'cost'

L'utilisation d'arguments de mots clés lève la restriction sur l'ordre des arguments. Chaque argument de mot clé désigne explicitement un paramètre spécifique par son nom, vous pouvez donc le spécifier dans n'importe quel ordre et Python saura toujours quel argument va avec quel paramètre:

>>>

>>> F(article='bananes', prix=1,74, qté=6)
6 bananes coûtent 1,74 $

Comme pour les arguments positionnels, cependant, le nombre d'arguments et de paramètres doit toujours correspondre:

>>>

>>> # Encore trop peu d'arguments
>>> F(qté=6, article='bananes')
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
    F(qté=6, article='bananes')
Erreur-type: f () manque 1 argument positionnel requis: «prix»

Ainsi, les arguments de mots clés permettent une flexibilité dans l'ordre de spécification des arguments de fonction, mais le nombre d'arguments est toujours rigide.

Vous pouvez appeler une fonction à l'aide d'arguments positionnels et de mots clés:

>>>

>>> F(6, prix=1,74, article='bananes')
6 bananes coûtent 1,74 $

>>> F(6, 'bananes', prix=1,74)
6 bananes coûtent 1,74 $

Lorsque les arguments de position et de mot clé sont tous les deux présents, tous les arguments de position doivent venir en premier:

>>>

>>> F(6, article='bananes', 1,74)
SyntaxError: l'argument positionnel suit l'argument mot clé

Une fois que vous avez spécifié un argument de mot clé, il ne peut plus y avoir d'argument positionnel à sa droite.

Paramètres par défaut

Si un paramètre spécifié dans une définition de fonction Python a la forme =, puis devient une valeur par défaut pour ce paramètre. Les paramètres définis de cette façon sont appelés paramètres par défaut ou facultatifs. Un exemple de définition de fonction avec des paramètres par défaut est présenté ci-dessous:

>>>

>>> def F(qté=6, article='bananes', prix=1,74):
...     impression(F"qté article    coût $prix: .2f")
...

Lorsque cette version de F() est appelé, tout argument omis prend sa valeur par défaut:

>>>

>>> F(4, 'pommes', 2,24)
4 pommes coûtent 2,24 $
>>> F(4, 'pommes')
4 pommes coûtent 1,74 $

>>> F(4)
4 bananes coûtent 1,74 $
>>> F()
6 bananes coûtent 1,74 $

>>> F(article=«kumquats», qté=9)
9 kumquats coûtent 1,74 $
>>> F(prix=2.29)
6 bananes coûtent 2,29 $

En résumé:

  • Arguments positionnels doit correspondre dans l'ordre et le nombre aux paramètres déclarés dans la définition de la fonction.
  • Arguments des mots clés doit être d'accord avec les paramètres déclarés en nombre, mais ils peuvent être spécifiés dans un ordre arbitraire.
  • Paramètres par défaut permettre à certains arguments d'être omis lors de l'appel de la fonction.

Valeurs des paramètres par défaut mutables

Les choses peuvent devenir étranges si vous spécifiez une valeur de paramètre par défaut qui est un objet mutable. Considérez cette définition de fonction Python:

>>>

>>> def F(ma liste=[]):
...     ma liste.ajouter('###')
...     revenir ma liste
...

F() prend un seul paramètre de liste, ajoute la chaîne '###' à la fin de la liste et renvoie le résultat:

>>>

>>> F([[[['foo', 'bar', «baz»])
['foo', 'bar', 'baz', '###']

>>> F([[[[1, 2, 3, 4, 5])
[1, 2, 3, 4, 5, '###']

La valeur par défaut du paramètre ma liste est la liste vide, donc si F() est appelé sans aucun argument, la valeur de retour est une liste avec l'élément unique '###':

Jusqu'à présent, tout a du sens. Maintenant, à quoi vous attendriez-vous si F() est appelé sans aucun paramètre une deuxième et une troisième fois? Voyons voir:

>>>

>>> F()
['###', '###']
>>> F()
['###', '###', '###']

Oups! Vous vous attendiez peut-être à ce que chaque appel suivant renvoie également la liste des singleton ['###'], tout comme le premier. Au lieu de cela, la valeur de retour continue de croître. Qu'est-il arrivé?

En Python, les valeurs des paramètres par défaut sont défini une seule fois lorsque la fonction est définie (c'est-à-dire lorsque le def est exécutée). La valeur par défaut n'est pas redéfinie à chaque appel de la fonction. Ainsi, chaque fois que vous appelez F() sans paramètre, vous jouez .ajouter() sur la même liste.

Vous pouvez le démontrer avec id ():

>>>

>>> def F(ma liste=[]):
...     impression(id(ma liste))
...     ma liste.ajouter('###')
...     revenir ma liste
...
>>> F()
140095566958408
['###']        
>>> F()
140095566958408
['###', '###']
>>> F()
140095566958408
['###', '###', '###']

le identifiant d'objet affiché confirme que, lorsque ma liste est autorisé par défaut, la valeur est le même objet à chaque appel. Étant donné que les listes sont modifiables, chaque .ajouter() appel rallonge la liste. Il s'agit d'un piège courant et assez bien documenté lorsque vous utilisez un objet modifiable comme valeur par défaut d'un paramètre. Cela conduit potentiellement à un comportement déroutant du code, et il est probablement préférable de l'éviter.

Pour contourner ce problème, envisagez d'utiliser une valeur d'argument par défaut qui signale aucun argument n'a été spécifié. La plupart des valeurs fonctionneraient, mais Aucun est un choix courant. Lorsque la valeur sentinelle indique qu'aucun argument n'est donné, créez une nouvelle liste vide à l'intérieur de la fonction:

>>>

>>> def F(ma liste=Aucun):
...     si ma liste est Aucun:
...         ma liste = []
...         ma liste.ajouter('###')
...     revenir ma liste
...

>>> F()
['###']
>>> F()
['###']
>>> F()
['###']

>>> F([[[['foo', 'bar', «baz»])
['foo', 'bar', 'baz', '###']

>>> F([[[[1, 2, 3, 4, 5])
[1, 2, 3, 4, 5, '###']

Notez comment cela garantit que ma liste maintenant par défaut vraiment une liste vide chaque fois F() est appelé sans argument.

Pass-By-Value vs Pass-By-Reference en Pascal

Dans la conception d'un langage de programmation, il existe deux paradigmes courants pour passer un argument à une fonction:

  1. Pass-by-value: Une copie de l'argument est passée à la fonction.
  2. Pass-by-reference: Une référence à l'argument est passée à la fonction.

Il existe d'autres mécanismes, mais ce sont essentiellement des variations de ces deux. Dans cette section, vous allez faire un petit détour par Python et regarder brièvement Pascal, un langage de programmation qui fait une distinction particulièrement claire entre ces deux.

Voici ce que vous devez savoir sur la syntaxe Pascal:

  • Procédures: Une procédure en Pascal est similaire à une fonction Python.
  • Colon-égal: Cet opérateur (: =) est utilisé pour l'affectation en Pascal. Il est analogue au signe égal (=) en Python.
  • writeln (): Cette fonction affiche des données sur la console, similaires à celles de Python impression().

Avec ce travail préparatoire en place, voici le premier exemple Pascal:

    1 // Exemple Pascal # 1
    2 
    3 procédure F(fx : entier);
    4 commencer
    5     writeln('Démarrer f (): fx =', fx);
    6     fx : = dix;
    sept     writeln('Fin f (): fx =', fx);
    8 fin;
    9 
dix // Programme principal
11 var
12     X : entier;
13 
14 commencer
15     X : = 5;
16     writeln(«Avant f (): x =», X);
17     F(X);
18     writeln(«Après f (): x =», X);
19 fin.

Voici ce qui se passe:

  • Ligne 12: Le programme principal définit une variable entière X.
  • Ligne 15: Il attribue initialement X la valeur 5.
  • Ligne 17: Il appelle ensuite la procédure F(), qui passe X comme argument.
  • Ligne 5: À l'intérieur F(), les writeln () indique que le paramètre correspondant fx est initialement 5, la valeur est passée.
  • Ligne 6: fx se voit alors attribuer la valeur dix.
  • Ligne 7: Cette valeur est vérifiée par ce writeln () instruction exécutée juste avant F() sort.
  • Ligne 18: De retour dans l'environnement d'appel du programme principal, ce writeln () déclaration montre qu'après F() Retour, X est toujours 5, comme c'était le cas avant l'appel de procédure.

L'exécution de ce code génère la sortie suivante:

Avant F():  X = 5
Début  F():  fx = 5
Fin    F():  fx = dix
Après  F():  X = 5

Dans cet exemple, X est passé par valeur, donc F() ne reçoit qu'une copie. Lorsque le paramètre correspondant fx est modifié, X n'est pas affecté.

Maintenant, comparez ceci avec l'exemple suivant:

    1 // Exemple Pascal # 2
    2 
    3 procédure F(var fx : entier);
    4 commencer
    5     writeln('Démarrer f (): fx =', fx);
    6     fx : = dix;
    sept     writeln('Fin f (): fx =', fx);
    8 fin;
    9 
dix // Programme principal
11 var
12     X : entier;
13 
14 commencer
15     X : = 5;
16     writeln(«Avant f (): x =», X);
17     F(X);
18     writeln(«Après f (): x =», X);
19 fin.

Ce code est identique au premier exemple, avec une modification. C'est la présence du mot var devant de fx dans la définition de la procédure F() à la ligne 3. Cela indique que l'argument F() est passé par référence. Modifications apportées au paramètre correspondant fx modifiera également l'argument dans l'environnement appelant.

La sortie de ce code est la même qu'avant, à l'exception de la dernière ligne:

Avant F():  X = 5
Début  F():  fx = 5
Fin    F():  fx = dix
Après  F():  X = dix

Encore, fx se voit attribuer la valeur dix à l'intérieur F() comme avant. Mais cette fois, quand F() Retour, X dans le programme principal a également été modifié.

Dans de nombreux langages de programmation, c'est essentiellement la distinction entre le passage par valeur et le passage par référence:

  • Si une variable est passée par valeur, alors la fonction a une copie sur laquelle travailler, mais elle ne peut pas modifier la valeur d'origine dans l'environnement appelant.
  • Si une variable est passée par référence, toutes les modifications apportées par la fonction au paramètre correspondant affecteront la valeur dans l'environnement appelant.

La raison pour laquelle vient de quel référence signifie dans ces langues. Les valeurs variables sont stockées en mémoire. En Pascal et dans des langages similaires, une référence est essentiellement l'adresse de cet emplacement mémoire, comme illustré ci-dessous:

Illustration du passage par valeur et référence en python

Dans le schéma de gauche, X a de la mémoire allouée dans l'espace de noms du programme principal. Quand F() est appelé, X est passé par valeur, donc mémoire pour le paramètre correspondant fx est alloué dans l'espace de noms de F()et la valeur de X y est copié. Quand F() modifie fx, c'est cette copie locale qui est modifiée. La valeur de X dans l'environnement d'appel reste inchangé.

Dans le schéma de droite, X est passé par référence. Le paramètre correspondant fx pointe vers l'adresse réelle dans l'espace de noms du programme principal où la valeur de X est stocké. Quand F() modifie fx, cela modifie la valeur à cet endroit, tout comme si le programme principal modifiait X lui-même.

Pass-By-Value vs Pass-By-Reference en Python

Les paramètres en Python sont-ils pass-by-value ou pass-by-reference? La réponse est qu'ils ne sont ni l'un ni l'autre, exactement. C'est parce qu'une référence ne signifie pas tout à fait la même chose en Python qu'en Pascal.

Rappelons qu'en Python, chaque donnée est un objet. Une référence pointe vers un objet, pas vers un emplacement mémoire spécifique. Cela signifie que l'affectation n'est pas interprétée de la même manière en Python qu'en Pascal. Considérez la paire d'instructions suivante en Pascal:

Ceux-ci sont interprétés de cette façon:

  • La variable X fait référence à un emplacement mémoire spécifique.
  • La première déclaration met la valeur 5 à cet endroit.
  • La prochaine déclaration écrase le 5 et met dix là à la place.

En revanche, en Python, les instructions d'affectation analogues sont les suivantes:

Ces instructions d'affectation ont la signification suivante:

  • La première déclaration les causes X pointer vers un objet dont la valeur est 5.
  • La prochaine déclaration réaffecte X comme une nouvelle référence à un objet différent dont la valeur est dix. Dit d'une autre manière, la deuxième affectation renoue X à un objet différent avec une valeur dix.

En Python, lorsque vous passez un argument à une fonction, un objet similaire reliure se produit. Considérez cet exemple:

>>>

    1 >>> def F(fx):
    2 ...     fx = dix
    3 ...
    4 >>> X = 5
    5 >>> F(X)
    6 >>> X
    sept 5

Dans le programme principal, la déclaration x = 5 sur la ligne 5 crée une référence nommée X lié à un objet dont la valeur est 5. F() est ensuite appelé sur la ligne 7, avec X comme argument. Quand F() commence d'abord, une nouvelle référence appelée fx est créé, qui pointe initialement vers le même 5 objet comme X Est-ce que:

Illustration de l'appel de fonction

Cependant, lorsque la déclaration fx = 10 sur la ligne 2 est exécuté, F() renoue fx à un nouvel objet dont la valeur est dix. Les deux références, X et fx, sont découplé l'un de l'autre. Rien d'autre que F() cela affectera X, et quand F() se termine, X pointera toujours vers l'objet 5, comme c'était le cas avant l'appel de fonction:

Appel de fonction Python

Vous pouvez confirmer tout cela en utilisant id (). Voici une version légèrement augmentée de l'exemple ci-dessus qui affiche les identificateurs numériques des objets impliqués:

>>>

    1 >>> def F(fx):
    2 ...     impression('fx =', fx, '/ id (fx) =', id(fx))
    3 ...     fx = dix
    4 ...     impression('fx =', fx, '/ id (fx) =', id(fx))
    5 ...
    6 
    sept >>> X = 5
    8 >>> impression('x =', X, '/ id (x) =', id(X))
    9 x = 5 / id (x) = 1357924048
dix 
11 >>> F(X)
12 fx = 5 / id (fx) = 1357924048
13 fx = 10 / id (fx) = 1357924128
14 
15 >>> impression('x =', X, '/ id (x) =', id(X))
16 x = 5 / id (x) = 1357924048

Quand F() commence d'abord, fx et X les deux pointent vers le même objet, dont id () est 1357924048. Après F() exécute l'instruction fx = 10 sur la ligne 3, fx pointe vers un objet différent dont id () est 1357924128. La connexion à l'objet d'origine dans l'environnement appelant est perdue.

L'argument passant en Python est en quelque sorte un hybride entre passe-par-valeur et passe-par-référence. Ce qui est transmis à la fonction est une référence à un objet, mais la référence est transmise par valeur.

Le point clé à retenir ici est qu'une fonction Python ne peut pas modifier la valeur d'un argument en réaffectant le paramètre correspondant à autre chose. L'exemple suivant le démontre:

>>>

>>> def F(X):
...     X = 'foo'
...
>>> pour je dans (
...         40,
...         dicter(foo=1, bar=2),
...         1, 2, 3,
...         'bar',
...         [[[['foo', 'bar', «baz»]):
...     F(je)
...     impression(je)
...
40
'foo': 1, 'bar': 2
1, 2, 3
bar
['foo', 'bar', 'baz']

Ici, des objets de type int, dicter, ensemble, str, et liste sont passés à F() comme arguments. F() essaie d'affecter chacun à l'objet chaîne 'foo', mais comme vous pouvez le voir, une fois de retour dans l'environnement d'appel, ils sont tous inchangés. Aussitôt que F() exécute l'affectation x = 'foo', la référence est rebondet la connexion à l'objet d'origine est perdue.

Cela signifie-t-il qu'une fonction Python ne peut jamais modifier ses arguments? En fait, non, ce n'est pas le cas! Regardez ce qui se passe ici:

>>>

>>> def F(X):
...     X[[[[0] = «---»
...

>>> ma liste = [[[['foo', 'bar', «baz», «qux»]

>>> F(ma liste)
>>> ma liste
['---', 'bar', 'baz', 'qux']

Dans ce cas, l'argument de F() est une liste. Quand F() est appelé, une référence à ma liste est passé. Vous l'avez déjà vu F() ne peut pas réaffecter ma liste de gros. Si X ont été affectés à quelque chose d'autre, alors il serait lié à un autre objet, et la connexion à ma liste serait perdu.

cependant, F() peut utiliser la référence pour apporter des modifications à l'intérieur ma liste. Ici, F() a modifié le premier élément. Vous pouvez voir qu'une fois la fonction revenue, ma liste a, en fait, été modifié dans l'environnement d'appel. Le même concept s'applique à un dictionnaire:

>>>

>>> def F(X):
...     X[[[['bar'] = 22
...

>>> my_dict = 'foo': 1, 'bar': 2, «baz»: 3

>>> F(my_dict)
>>> my_dict
'foo': 1, 'bar': 22, 'baz': 3

Ici, F() les usages X comme référence pour faire un changement à l'intérieur my_dict. Ce changement se reflète dans l'environnement d'appel après F() Retour.

Résumé de la réussite des arguments

L'argument passant en Python peut être résumé comme suit. Passer un objet immuable, comme un int, str, tuple, ou frozenset, à une fonction Python agit comme une valeur de passage. La fonction ne peut pas modifier l'objet dans l'environnement appelant.

Passer un objet mutable tel qu'un liste, dicter, ou ensemble agit quelque peu, mais pas exactement, comme un passage par référence. La fonction ne peut pas réaffecter l'objet en gros, mais elle peut modifier les éléments en place au sein de l'objet, et ces modifications seront reflétées dans l'environnement appelant.

Effets secondaires

Ainsi, en Python, il vous est possible de modifier un argument depuis une fonction afin que le changement soit reflété dans l'environnement appelant. Mais devriez-vous faire cela? Ceci est un exemple de ce que l'on appelle dans le jargon de programmation un effet secondaire.

Plus généralement, une fonction Python est censée provoquer un effet secondaire si elle modifie son environnement d'appel de quelque manière que ce soit. La modification de la valeur d'un argument de fonction n'est qu'une des possibilités.

Lorsqu'ils sont cachés ou inattendus, les effets secondaires peuvent entraîner des erreurs de programme très difficiles à localiser. En règle générale, il vaut mieux les éviter.

le revenir Déclaration

Qu'est-ce qu'une fonction Python à faire alors? Après tout, dans de nombreux cas, si une fonction n'entraîne pas de changement dans l'environnement d'appel, il est inutile de l'appeler du tout. Comment une fonction doit-elle affecter son appelant?

Eh bien, une possibilité est d'utiliser valeurs de retour de fonction. UNE revenir dans une fonction Python sert deux objectifs:

  1. Il met immédiatement fin à la fonction et renvoie le contrôle d'exécution à l'appelant.
  2. Il fournit un mécanisme par lequel la fonction peut transmettre des données à l'appelant.

Quitter une fonction

Au sein d'une fonction, un revenir L'instruction provoque la sortie immédiate de la fonction Python et le transfert de l'exécution à l'appelant:

>>>

>>> def F():
...     impression('foo')
...     impression('bar')
...     revenir
...

>>> F()
foo
bar

Dans cet exemple, le revenir déclaration est en fait superflue. Une fonction retournera à l'appelant lorsqu'elle tombe de la fin– c'est-à-dire après l'exécution de la dernière instruction du corps de la fonction. So, this function would behave identically without the revenir statement.

cependant, revenir statements don’t need to be at the end of a function. They can appear anywhere in a function body, and even multiple times. Consider this example:

>>>

    1 >>> def f(X):
    2 ...     if X < 0:
    3 ...         revenir
    4 ...     if X > 100:
    5 ...         revenir
    6 ...     impression(X)
    sept ...
    8 
    9 >>> f(-3)
dix >>> f(105)
11 >>> f(64)
12 64

The first two calls to f() don’t cause any output, because a revenir statement is executed and the function exits prematurely, before the print() statement on line 6 is reached.

This sort of paradigm can be useful for error checking in a function. You can check several error conditions at the start of the function, with revenir statements that bail out if there’s a problem:

def f():
    if error_cond1:
        revenir
    if error_cond2:
        revenir
    if error_cond3:
        revenir

    <Ordinaire processing>

If none of the error conditions are encountered, then the function can proceed with its normal processing.

Returning Data to the Caller

In addition to exiting a function, the revenir statement is also used to pass data back to the caller. If a revenir statement inside a Python function is followed by an expression, then in the calling environment, the function call evaluates to the value of that expression:

>>>

    1 >>> def f():
    2 ...     revenir 'foo'
    3 ...
    4 
    5 >>> s = f()
    6 >>> s
    sept 'foo'

Here, the value of the expression f() on line 5 is 'foo', which is subsequently assigned to variable s.

A function can return any type of object. In Python, that means pretty much anything whatsoever. In the calling environment, the function call can be used syntactically in any way that makes sense for the type of object the function returns.

For example, in this code, f() returns a dictionary. In the calling environment then, the expression f() represents a dictionary, and f()['baz'] is a valid key reference into that dictionary:

>>>

>>> def f():
...     revenir dict(foo=1, bar=2, baz=3)
...

>>> f()
'foo': 1, 'bar': 2, 'baz': 3
>>> f()[[[['baz']
3

In the next example, f() returns a string that you can slice like any other string:

>>>

>>> def f():
...     revenir 'foobar'
...

>>> f()[[[[2:4]
'ob'

Ici, f() returns a list that can be indexed or sliced:

>>>

>>> def f():
...     revenir [[[['foo', 'bar', 'baz', 'qux']
...  

>>> f()
['foo', 'bar', 'baz', 'qux']
>>> f()[[[[2]
'baz'
>>> f()[::[::[::[::-1]
['qux', 'baz', 'bar', 'foo']

If multiple comma-separated expressions are specified in a revenir statement, then they’re packed and returned as a tuple:

>>>

>>> def f():
...     revenir 'foo', 'bar', 'baz', 'qux'
...

>>> type(f())

>>> t = f()
>>> t
('foo', 'bar', 'baz', 'qux')

>>> une, b, c,  = f()
>>> impression(f'a = a, b = b, c = c, d = d')
a = foo, b = bar, c = baz, d = qux

When no return value is given, a Python function returns the special Python value Aucun:

>>>

>>> def f():
...     revenir
...

>>> impression(f())
Aucun

The same thing happens if the function body doesn’t contain a revenir statement at all and the function falls off the end:

>>>

>>> def g():
...     pass
...

>>> impression(g())
Aucun

Recall that Aucun is falsy when evaluated in a boolean context.

Since functions that exit through a bare revenir statement or fall off the end return Aucun, a call to such a function can be used in a boolean context:

>>>

>>> def f():
...     revenir
...
>>> def g():
...     pass
...

>>> if f() ou g():
...     impression('yes')
... autre:
...     impression('no')
...
no  

Here, calls to both f() et g() are falsy, so f() or g() is as well, and the autre clause executes.

Revisiting Side Effects

Suppose you want to write a function that takes an integer argument and doubles it. That is, you want to pass an integer variable to the function, and when the function returns, the value of the variable in the calling environment should be twice what it was. In Pascal, you could accomplish this using pass-by-reference:

    1 procedure double(var X : integer);
    2 commencer
    3     X := X * 2;
    4 end;
    5 
    6 var
    sept     X : integer;
    8 
    9 commencer
dix     X := 5;
11     writeln('Before procedure call: ', X);
12     double(X);
13     writeln('After procedure call:  ', X);
14 end.

Executing this code produces the following output, which verifies that double() does indeed modify X in the calling environment:

Avant procedure appel: 5
Après procedure appel:  dix

In Python, this won’t work. As you now know, Python integers are immutable, so a Python function can’t change an integer argument by side effect:

>>>

>>> def double(X):
...     X *= 2
...  

>>> X = 5
>>> double(X)
>>> X
5

However, you can use a return value to obtain a similar effect. Simply write double() so that it takes an integer argument, doubles it, and returns the doubled value. Then, the caller is responsible for the assignment that modifies the original value:

>>>

>>> def double(X):
...     revenir X * 2
...  

>>> X = 5
>>> X = double(X)
>>> X
dix

This is arguably preferable to modifying by side effect. It’s very clear that X is being modified in the calling environment because the caller is doing so itself. Anyway, it’s the only option, because modification by side effect doesn’t work in this case.

Still, even in cases where it’s possible to modify an argument by side effect, using a return value may still be clearer. Suppose you want to double every item in a list. Because lists are mutable, you could define a Python function that modifies the list in place:

>>>

>>> def double_list(X):
...     je = 0
...     while je < len(X):
...             X[[[[je] *= 2
...             je += 1
...  

>>> une = [[[[1, 2, 3, 4, 5]
>>> double_list(une)
>>> une
[2, 4, 6, 8, 10]

contrairement à double() in the previous example, double_list() actually works as intended. If the documentation for the function clearly states that the list argument’s contents are changed, then this may be a reasonable implementation.

However, you can also write double_list() to pass the desired list back by return value and allow the caller to make the assignment, similar to how double() was re-written in the previous example:

>>>

>>> def double_list(X):
...     r = []
...     pour je dans X:
...             r.append(je * 2)
...     revenir r
...

>>> une = [[[[1, 2, 3, 4, 5]
>>> une = double_list(une)
>>> une
[2, 4, 6, 8, 10]

Either approach works equally well. As is often the case, this is a matter of style, and personal preferences vary. Side effects aren’t necessarily consummate evil, and they have their place, but because virtually anything can be returned from a function, the same thing can usually be accomplished through return values as well.

Variable-Length Argument Lists

In some cases, when you’re defining a function, you may not know beforehand how many arguments you’ll want it to take. Suppose, for example, that you want to write a Python function that computes the average of several values. You could start with something like this:

>>>

>>> def avg(une, b, c):
...     revenir (une + b + c) / 3
...

All is well if you want to average three values:

However, as you’ve already seen, when positional arguments are used, the number of arguments passed must agree with the number of parameters declared. Clearly then, all isn’t well with this implementation of avg() for any number of values other than three:

>>>

>>> avg(1, 2, 3, 4)
Traceback (most recent call last):
  File "", line 1, in 
    avg(1, 2, 3, 4)
TypeError: avg() takes 3 positional arguments but 4 were given

You could try to define avg() with optional parameters:

>>>

>>> def avg(une, b=0, c=0, =0, e=0):
...     .
...     .
...     .
...

This allows for a variable number of arguments to be specified. The following calls are at least syntactically correct:

avg(1)
avg(1, 2)
avg(1, 2, 3)
avg(1, 2, 3, 4)
avg(1, 2, 3, 4, 5)

But this approach still suffers from a couple of problems. For starters, it still only allows up to five arguments, not an arbitrary number. Worse yet, there’s no way to distinguish between the arguments that were specified and those that were allowed to default. The function has no way to know how many arguments were actually passed, so it doesn’t know what to divide by:

>>>

>>> def avg(une, b=0, c=0, =0, e=0):
...     revenir (une + b + c +  + e) / # Divided by what???
...

Evidently, this won’t do either.

You could write avg() to take a single list argument:

>>>

>>> def avg(une):
...     total = 0
...     pour v dans une:
...             total += v
...     revenir total / len(une)
...  

>>> avg([[[[1, 2, 3])
2.0  
>>> avg([[[[1, 2, 3, 4, 5])
3.0  

At least this works. It allows an arbitrary number of values and produces a correct result. As an added bonus, it works when the argument is a tuple as well:

>>>

>>> t = (1, 2, 3, 4, 5)
>>> avg(t)
3.0

The drawback is that the added step of having to group the values into a list or tuple is probably not something the user of the function would expect, and it isn’t very elegant. Whenever you find Python code that looks inelegant, there’s probably a better option.

In this case, indeed there is! Python provides a way to pass a function a variable number of arguments with argument tuple packing and unpacking using the asterisk (*) operator.

Argument Tuple Packing

When a parameter name in a Python function definition is preceded by an asterisk (*), it indicates argument tuple packing. Any corresponding arguments in the function call are packed into a tuple that the function can refer to by the given parameter name. Voici un exemple:

>>>

>>> def f(*args):
...     impression(args)
...     impression(type(args), len(args))
...     pour X dans args:
...             impression(X)
...

>>> f(1, 2, 3)
(1, 2, 3)        
    3
1
2
3

>>> f('foo', 'bar', 'baz', 'qux', 'quux')
('foo', 'bar', 'baz', 'qux', 'quux')
    5
foo
bar
baz
qux
quux

In the definition of f(), the parameter specification *args indicates tuple packing. In each call to f(), the arguments are packed into a tuple that the function can refer to by the name args. Any name can be used, but args is so commonly chosen that it’s practically a standard.

Using tuple packing, you can clean up avg() like this:

>>>

>>> def avg(*args):
...     total = 0
...     pour je dans args:
...         total += je
...     revenir total / len(args)
...

>>> avg(1, 2, 3)
2.0
>>> avg(1, 2, 3, 4, 5)
3.0

Better still, you can tidy it up even further by replacing the pour loop with the built-in Python function sum(), which sums the numeric values in any iterable:

>>>

>>> def avg(*args):
...     revenir sum(args) / len(args)
...

>>> avg(1, 2, 3)
2.0
>>> avg(1, 2, 3, 4, 5)
3.0

Maintenant, avg() is concisely written and works as intended.

Still, depending on how this code will be used, there may still be work to do. As written, avg() will produce a TypeError exception if any arguments are non-numeric:

>>>

>>> avg(1, 'foo', 3)
Traceback (most recent call last):
  File "", line 1, in 
  
  
  
  File "", line 2, in avg
TypeError: unsupported operand type(s) for +: 'int' and 'str'

To be as robust as possible, you should add code to check that the arguments are of the proper type. Later in this tutorial series, you’ll learn how to catch exceptions like TypeError and handle them appropriately. You can also check out Python Exceptions: An Introduction.

Argument Tuple Unpacking

An analogous operation is available on the other side of the equation in a Python function call. When an argument in a function call is preceded by an asterisk (*), it indicates that the argument is a tuple that should be unpacked and passed to the function as separate values:

>>>

>>> def f(X, y, z):
...     impression(f'x = x')
...     impression(f'y = y')
...     impression(f'z = z')
...

>>> f(1, 2, 3)
x = 1
y = 2
z = 3

>>> t = ('foo', 'bar', 'baz')
>>> f(*t)
x = foo
y = bar
z = baz

In this example, *t in the function call indicates that t is a tuple that should be unpacked. The unpacked values 'foo', 'bar', et 'baz' are assigned to the parameters X, y, et z, respectively.

Although this type of unpacking is called tuple unpacking, it doesn’t only work with tuples. The asterisk (*) operator can be applied to any iterable in a Python function call. For example, a list or set can be unpacked as well:

>>>

>>> une = [[[['foo', 'bar', 'baz']
>>> type(une)

>>> f(*une)
x = foo
y = bar
z = baz

>>> s = 1, 2, 3
>>> type(s)

>>> f(*s)
x = 1
y = 2
z = 3

You can even use tuple packing and unpacking at the same time:

>>>

>>> def f(*args):
...     impression(type(args), args)
...

>>> une = [[[['foo', 'bar', 'baz', 'qux']
>>> f(*une)
    ('foo', 'bar', 'baz', 'qux')

Ici, f(*a) indicates that list une should be unpacked and the items passed to f() as individual values. The parameter specification *args causes the values to be packed back up into the tuple args.

Argument Dictionary Packing

Python has a similar operator, the double asterisk (**), which can be used with Python function parameters and arguments to specify dictionary packing and unpacking. Preceding a parameter in a Python function definition by a double asterisk (**) indicates that the corresponding arguments, which are expected to be key=value pairs, should be packed into a dictionary:

>>>

>>> def f(**kwargs):
...     impression(kwargs)
...     impression(type(kwargs))
...     pour key, val dans kwargs.items():
...             impression(key, '->', val)
...

>>> f(foo=1, bar=2, baz=3)
'foo': 1, 'bar': 2, 'baz': 3

foo -> 1
bar -> 2
baz -> 3

In this case, the arguments foo=1, bar=2, et baz=3 are packed into a dictionary that the function can reference by the name kwargs. Again, any name can be used, but the peculiar kwargs (which is short for keyword args) is nearly standard. You don’t have to adhere to it, but if you do, then anyone familiar with Python coding conventions will know straightaway what you mean.

Argument Dictionary Unpacking

Argument dictionary unpacking is analogous to argument tuple unpacking. When the double asterisk (**) precedes an argument in a Python function call, it specifies that the argument is a dictionary that should be unpacked, with the resulting items passed to the function as keyword arguments:

>>>

>>> def f(une, b, c):
...     impression(F'a = a')
...     impression(F'b = b')
...     impression(F'c = c')
...

>>>  = 'a': 'foo', 'b': 25, 'c': 'qux'
>>> f(**)
a = foo
b = 25
c = qux

The items in the dictionary are unpacked and passed to f() as keyword arguments. Donc, f(**d) is equivalent to f(a='foo', b=25, c='qux'):

>>>

>>> f(une='foo', b=25, c='qux')
a = foo
b = 25
c = qux

In fact, check this out:

>>>

>>> f(**dict(une='foo', b=25, c='qux'))
a = foo
b = 25
c = qux

Ici, dict(a='foo', b=25, c='qux') creates a dictionary from the specified key/value pairs. Then, the double asterisk operator (**) unpacks it and passes the keywords to f().

Putting It All Together

Think of *args as a variable-length positional argument list, and **kwargs as a variable-length keyword argument list.

All three—standard positional parameters, *args, et **kwargs—can be used in one Python function definition. If so, then they should be specified in that order:

>>>

>>> def f(une, b, *args, **kwargs):
...     impression(F'a = a')
...     impression(F'b = b')
...     impression(F'args = args')
...     impression(F'kwargs = kwargs')
...

>>> f(1, 2, 'foo', 'bar', 'baz', 'qux', X=100, y=200, z=300)
a = 1
b = 2
args = ('foo', 'bar', 'baz', 'qux')
kwargs = 'x': 100, 'y': 200, 'z': 300

This provides just about as much flexibility as you could ever need in a function interface!

Multiple Unpackings in a Python Function Call

Python version 3.5 introduced support for additional unpacking generalizations, as outlined in PEP 448. One thing these enhancements allow is multiple unpackings in a single Python function call:

>>>

>>> def f(*args):
...     pour je dans args:
...             impression(je)
...

>>> une = [[[[1, 2, 3]
>>> t = (4, 5, 6)
>>> s = sept, 8, 9

>>> f(*une, *t, *s)
1
2
3
4
5
6
8
9
sept

You can specify multiple dictionary unpackings in a Python function call as well:

>>>

>>> def f(**kwargs):
...     pour k, v dans kwargs.items():
...             impression(k, '->', v)
...

>>> d1 = 'a': 1, 'b': 2
>>> d2 = 'x': 3, 'y': 4

>>> f(**d1, **d2)
a -> 1
b -> 2
x -> 3
y -> 4

By the way, the unpacking operators * et ** don’t apply only to variables, as in the examples above. You can also use them with literals that are iterable:

>>>

>>> def f(*args):
...     pour je dans args:
...             impression(je)
...

>>> f(*[[[[1, 2, 3], *[[[[4, 5, 6])
1
2
3
4
5
6

>>> def f(**kwargs):
...     pour k, v dans kwargs.items():
...             impression(k, '->', v)
...

>>> f(**'a': 1, 'b': 2, **'x': 3, 'y': 4)
a -> 1
b -> 2
x -> 3
y -> 4

Here, the literal lists [1, 2, 3] et [4, 5, 6] are specified for tuple unpacking, and the literal dictionaries 'a': 1, 'b': 2 et 'x': 3, 'y': 4 are specified for dictionary unpacking.

Keyword-Only Arguments

A Python function in version 3.x can be defined so that it takes keyword-only arguments. These are function arguments that must be specified by keyword. Let’s explore a situation where this might be beneficial.

Suppose you want to write a Python function that takes a variable number of string arguments, concatenates them together separated by a dot ("."), and prints them to the console. Something like this will do to start:

>>>

>>> def concat(*args):
...     impression(f'-> ".".join(args)')
...

>>> concat('a', 'b', 'c')
-> a.b.c
>>> concat('foo', 'bar', 'baz', 'qux')
-> foo.bar.baz.qux

As it stands, the output prefix is hard-coded to the string '-> '. What if you want to modify the function to accept this as an argument as well, so the user can specify something else? This is one possibility:

>>>

>>> def concat(prefix, *args):
...     impression(f'prefix".".join(args)')
...

>>> concat('//', 'a', 'b', 'c')
//a.b.c
>>> concat('... ', 'foo', 'bar', 'baz', 'qux')
... foo.bar.baz.qux

This works as advertised, but there are a couple of undesirable things about this solution:

  1. le prefix string is lumped together with the strings to be concatenated. Just from looking at the function call, it isn’t clear that the first argument is treated differently from the rest. To know that, you’d have to go back and look at the function definition.

  2. prefix isn’t optional. It always has to be included, and there’s no way to assume a default value.

You might think you could overcome the second issue by specifying a parameter with a default value, like this, perhaps:

>>>

>>> def concat(prefix='-> ', *args):
...     impression(f'prefix".".join(args)')
...

Unfortunately, this doesn’t work quite right. prefix est un positional parameter, so the interpreter assumes that the first argument specified in the function call is the intended output prefix. This means there isn’t any way to omit it and obtain the default value:

>>>

>>> concat('a', 'b', 'c')
ab.c

What if you try to specify prefix as a keyword argument? Well, you can’t specify it first:

>>>

>>> concat(prefix='//', 'a', 'b', 'c')
  File "", line 1
SyntaxError: positional argument follows keyword argument

As you’ve seen previously, when both types of arguments are given, all positional arguments must come before any keyword arguments.

However, you can’t specify it last either:

>>>

>>> concat('a', 'b', 'c', prefix='... ')
Traceback (most recent call last):
  File "", line 1, in 
TypeError: concat() got multiple values for argument 'prefix'

Encore, prefix is a positional parameter, so it’s assigned the first argument specified in the call (which is 'a' in this case). Then, when it’s specified again as a keyword argument at the end, Python thinks it’s been assigned twice.

Keyword-only parameters help solve this dilemma. In the function definition, specify *args to indicate a variable number of positional arguments, and then specify prefix after that:

>>>

>>> def concat(*args, prefix='-> '):
...     impression(f'prefix".".join(args)')
...

In that case, prefix becomes a keyword-only parameter. Its value will never be filled by a positional argument. It can only be specified by a named keyword argument:

>>>

>>> concat('a', 'b', 'c', prefix='... ')
... une.b.c

Note that this is only possible in Python 3. In versions 2.x of Python, specifying additional parameters after the *args variable arguments parameter raises an error.

Keyword-only arguments allow a Python function to take a variable number of arguments, followed by one or more additional options as keyword arguments. If you wanted to modify concat() so that the separator character can optionally be specified as well, then you could add an additional keyword-only argument:

>>>

>>> def concat(*args, prefix='-> ', sep='.'):
...     impression(f'prefixsep.join(args)')
...

>>> concat('a', 'b', 'c')
-> a.b.c
>>> concat('a', 'b', 'c', prefix='//')
//a.b.c
>>> concat('a', 'b', 'c', prefix='//', sep='-')
//a-b-c

If a keyword-only parameter is given a default value in the function definition (as it is in the example above), and the keyword is omitted when the function is called, then the default value is supplied:

>>>

>>> concat('a', 'b', 'c')
-> a.b.c

If, on the other hand, the parameter isn’t given a default value, then it becomes required, and failure to specify it results in an error:

>>>

>>> def concat(*args, prefix):
...     impression(f'prefix".".join(args)')
...

>>> concat('a', 'b', 'c', prefix='... ')
... une.b.c

>>> concat('a', 'b', 'c')
Traceback (most recent call last):
  File "", line 1, in 
TypeError: concat() missing 1 required keyword-only argument: 'prefix'

What if you want to define a Python function that takes a keyword-only argument but doesn’t take a variable number of positional arguments? For example, the following function performs the specified operation on two numerical arguments:

>>>

>>> def oper(X, y, op='+'):
...     if op == '+':
...             revenir X + y
...     elif op == '-':
...             revenir X - y
...     elif op == '/':
...             revenir X / y
...     autre:
...             revenir Aucun
...

>>> oper(3, 4)
sept
>>> oper(3, 4, '+')
sept
>>> oper(3, 4, '/')
0,75

If you wanted to make op a keyword-only parameter, then you could add an extraneous dummy variable argument parameter and just ignore it:

>>>

>>> def oper(X, y, *ignore, op='+'):
...     if op == '+':
...             revenir X + y
...     elif op == '-':
...             revenir X - y
...     elif op == '/':
...             revenir X / y
...     autre:
...             revenir Aucun
...

>>> oper(3, 4, op='+')
sept
>>> oper(3, 4, op='/')
0,75

The problem with this solution is that *ignore absorbs any extraneous positional arguments that might happen to be included:

>>>

>>> oper(3, 4, "I don't belong here")
sept
>>> oper(3, 4, "I don't belong here", op='/')
0,75

In this example, the extra argument shouldn’t be there (as the argument itself announces). Instead of quietly succeeding, it should really result in an error. The fact that it doesn’t is untidy at best. At worst, it may cause a result that appears misleading:

To remedy this, version 3 allows a variable argument parameter in a Python function definition to be just a bare asterisk (*), with the name omitted:

>>>

>>> def oper(X, y, *, op='+'):
...     if op == '+':
...             revenir X + y
...     elif op == '-':
...             revenir X - y
...     elif op == '/':
...             revenir X / y
...     autre:
...             revenir Aucun
...

>>> oper(3, 4, op='+')
sept
>>> oper(3, 4, op='/')
0,75

>>> oper(3, 4, "I don't belong here")
Traceback (most recent call last):
  File "", line 1, in 
TypeError: oper() takes 2 positional arguments but 3 were given

>>> oper(3, 4, '+')
Traceback (most recent call last):
  File "", line 1, in 
TypeError: oper() takes 2 positional arguments but 3 were given

le bare variable argument parameter * indicates that there aren’t any more positional parameters. This behavior generates appropriate error messages if extra ones are specified. It allows keyword-only parameters to follow.

Positional-Only Arguments

As of Python 3.8, function parameters can also be declared positional-only, meaning the corresponding arguments must be supplied positionally and can’t be specified by keyword.

To designate some parameters as positional-only, you specify a bare slash (/) in the parameter list of a function definition. Any parameters to the left of the slash (/) must be specified positionally. For example, in the following function definition, X et y are positional-only parameters, but z may be specified by keyword:

>>>

>>> # This is Python 3.8
>>> def f(X, y, /, z):
...     impression(f'x: x')
...     impression(f'y: y')
...     impression(f'z: z')
...

This means that the following calls are valid:

>>>

>>> f(1, 2, 3)
x: 1
y: 2
z: 3

>>> f(1, 2, z=3)
x: 1
y: 2
z: 3

The following call to f(), however, is not valid:

>>>

>>> f(X=1, y=2, z=3)
Traceback (most recent call last):
  File "", line 1, in 
TypeError: f() got some positional-only arguments passed as keyword arguments:
'x, y'

The positional-only and keyword-only designators may both be used in the same function definition:

>>>

>>> # This is Python 3.8
>>> def f(X, y, /, z, w, *, une, b):
...     impression(X, y, z, w, une, b)
...

>>> f(1, 2, z=3, w=4, une=5, b=6)
1 2 3 4 5 6

>>> f(1, 2, 3, w=4, une=5, b=6)
1 2 3 4 5 6

In this example:

  • X et y are positional-only.
  • une et b are keyword-only.
  • z et w may be specified positionally or by keyword.

For more information on positional-only parameters, see the Python 3.8 release highlights.

Docstrings

When the first statement in the body of a Python function is a string literal, it’s known as the function’s docstring. A docstring is used to supply documentation for a function. It can contain the function’s purpose, what arguments it takes, information about return values, or any other information you think would be useful.

The following is an example of a function definition with a docstring:

>>>

>>> def avg(*args):
...     """Returns the average of a list of numeric values."""
...     revenir sum(args) / len(args)
...

Technically, docstrings can use any of Python’s quoting mechanisms, but the recommended convention is to triple-quote using double-quote characters ("""), as shown above. If the docstring fits on one line, then the closing quotes should be on the same line as the opening quotes.

Multi-line docstrings are used for lengthier documentation. A multi-line docstring should consist of a summary line, followed by a blank line, followed by a more detailed description. The closing quotes should be on a line by themselves:

>>>

>>> def foo(bar=0, baz=1):
...     """Perform a foo transformation.
...
...                 Keyword arguments:
...                 bar -- magnitude along the bar axis (default=0)
...                 baz -- magnitude along the baz axis (default=1)
...                 """
...     <function_body>
...

Docstring formatting and semantic conventions are detailed in PEP 257.

When a docstring is defined, the Python interpreter assigns it to a special attribute of the function called __doc__. This attribute is one of a set of specialized identifiers in Python that are sometimes called magic attributes ou magic methods because they provide special language functionality.

You can access a function’s docstring with the expression .__doc__. The docstrings for the above examples can be displayed as follows:

>>>

>>> impression(avg.__doc__)
Returns the average of a list of numeric values.

>>> impression(foo.__doc__)
Perform a foo transformation.

                Keyword arguments:
                bar -- magnitude along the bar axis (default=0)
                baz -- magnitude along the baz axis (default=1)

In the interactive Python interpreter, you can type help() to display the docstring for :

>>>

>>> Aidez-moi(avg)
Help on function avg in module __main__:

avg(*args)
                Returns the average of a list of numeric values.

>>> Aidez-moi(foo)
Help on function foo in module __main__:

foo(bar=0, baz=1)
                Perform a foo transformation.

                Keyword arguments:
                bar -- magnitude along the bar axis (default=0)
                baz -- magnitude along the baz axis (default=1)

It’s considered good coding practice to specify a docstring for each Python function you define. For more on docstrings, check out Documenting Python Code: A Complete Guide.

Python Function Annotations

As of version 3.0, Python provides an additional feature for documenting a function called a function annotation. Annotations provide a way to attach metadata to a function’s parameters and return value.

To add an annotation to a Python function parameter, insert a colon (:) followed by any expression after the parameter name in the function definition. To add an annotation to the return value, add the characters -> and any expression between the closing parenthesis of the parameter list and the colon that terminates the function header. Voici un exemple:

The annotation for parameter une is the string '', for b the string '', and for the function return value the string ''.

The Python interpreter creates a dictionary from the annotations and assigns them to another special dunder attribute of the function called __annotations__. The annotations for the Python function f() shown above can be displayed as follows:

The keys for the parameters are the parameter names. The key for the return value is the string 'return':

Note that annotations aren’t restricted to string values. They can be any expression or object. For example, you might annotate with type objects:

>>>

>>> def f(une: int, b: str) -> float:
...     impression(une, b)
...     revenir(3.5)
...

>>> f(1, 'foo')
1 foo
3.5

>>> f.__annotations__
'a': , 'b': , 'return': 

An annotation can even be a composite object like a list or a dictionary, so it’s possible to attach multiple items of metadata to the parameters and return value:

>>>

>>> def area(
...     r: 
...            'desc': 'radius of circle',
...            'type': float
...        ) -> 
...        
...            'desc': 'area of circle',
...            'type': float
...        :
...     revenir 3.14159 * (r ** 2)
...

>>> area(2.5)
19.6349375

>>> area.__annotations__
'r': 'desc': 'radius of circle', 'type': ,
'return': 'desc': 'area of circle', 'type': 

>>> area.__annotations__[[[['r'][[[['desc']
'radius of circle'
>>> area.__annotations__[[[['return'][[[['type']

In the example above, an annotation is attached to the parameter r and to the return value. Each annotation is a dictionary containing a string description and a type object.

If you want to assign a default value to a parameter that has an annotation, then the default value goes after the annotation:

>>>

>>> def f(une: int = 12, b: str = 'baz') -> float:
...     impression(une, b)
...     revenir(3.5)
...

>>> f.__annotations__
'a': , 'b': , 'return': 

>>> f()
12 baz
3.5

What do annotations do? Frankly, they don’t do much of anything. They’re just kind of there. Let’s look at one of the examples from above again, but with a few minor modifications:

>>>

>>> def f(une: int, b: str) -> float:
...     impression(une, b)
...     revenir 1, 2, 3
...

>>> f('foo', 2.5)
foo 2.5
(1, 2, 3)

What’s going on here? The annotations for f() indicate that the first argument is int, the second argument str, and the return value float. But the subsequent call to f() breaks all the rules! The arguments are str et float, respectively, and the return value is a tuple. Yet the interpreter lets it all slide with no complaint at all.

Annotations don’t impose any semantic restrictions on the code whatsoever. They’re simply bits of metadata attached to the Python function parameters and return value. Python dutifully stashes them in a dictionary, assigns the dictionary to the function’s __annotations__ dunder attribute, and that’s it. Annotations are completely optional and don’t have any impact on Python function execution at all.

To quote Amahl in Amahl and the Night Visitors, “What’s the use of having it then?”

For starters, annotations make good Documentation. You can specify the same information in the docstring, of course, but placing it directly in the function definition adds clarity. The types of the arguments and the return value are obvious on sight for a function header like this:

def f(une: int, b: str) -> float:

Granted, the interpreter doesn’t enforce adherence to the types specified, but at least they’re clear to someone reading the function definition.

Deep Dive: Enforcing Type-Checking

If you were inclined to, you could add code to enforce the types specified in the function annotations. Here’s a function that checks the actual type of each argument against what’s specified in the annotation for the corresponding parameter. It displays True if they match ans False if they don’t:

>>>

>>> def f(une: int, b: str, c: float):
...     import inspect
...     args = inspect.getfullargspec(f).args
...     annotations = inspect.getfullargspec(f).annotations
...     pour X dans args:
...         impression(X, '->',
...               'arg is', type(locals()[[[[X]), ',',
...               'annotation is', annotations[[[[X],
...               '/', (type(locals()[[[[X])) est annotations[[[[X])
...

>>> f(1, 'foo', 3.3)
a -> arg is  , annotation is  / True
b -> arg is  , annotation is  / True
c -> arg is  , annotation is  / True

>>> f('foo', 4.3, 9)
a -> arg is  , annotation is  / False
b -> arg is  , annotation is  / False
c -> arg is  , annotation is  / False

>>> f(1, 'foo', 'bar')
a -> arg is  , annotation is  / True
b -> arg is  , annotation is  / True
c -> arg is  , annotation is  / False

(Le inspect module contains functions that obtain useful information about live objects—in this case, function f().)

A function defined like the one above could, if desired, take some sort of corrective action when it detects that the passed arguments don’t conform to the types specified in the annotations.

In fact, a scheme for using annotations to perform static type checking in Python is described in PEP 484. A free static type checker for Python called mypy is available, which is built on the PEP 484 specification.

There’s another benefit to using annotations as well. The standardized format in which annotation information is stored in the __annotations__ attribute lends itself to the parsing of function signatures by automated tools.

When it comes down to it, annotations aren’t anything especially magical. You could even define your own without the special syntax that Python provides. Here’s a Python function definition with type object annotations attached to the parameters and return value:

>>>

>>> def f(une: int, b: str) -> float:
...     revenir
...

>>> f.__annotations__
'a': , 'b': , 'return': 

The following is essentially the same function, with the __annotations__ dictionary constructed manually:

>>>

>>> def f(une, b):
...     revenir
...

>>> f.__annotations__ = 'a': int, 'b': str, 'return': float

>>> f.__annotations__
'a': , 'b': , 'return': 

The effect is identical in both cases, but the first is more visually appealing and readable at first glance.

En fait, le __annotations__ attribute isn’t significantly different from most other attributes of a function. For example, it can be modified dynamically. You could choose to use the return value attribute to count how many times a function is executed:

>>>

>>> def f() -> 0:
...     f.__annotations__[[[['return'] += 1
...     impression(f"f() has been executed f.__annotations__['return']    time(s)")
...

>>> f()
f() has been executed 1 time(s)
>>> f()
f() has been executed 2 time(s)
>>> f()
f() has been executed 3 time(s)

Python function annotations are nothing more than dictionaries of metadata. It just happens that you can create them with convenient syntax that’s supported by the interpreter. They’re whatever you choose to make of them.

Conclusion

As applications grow larger, it becomes increasingly important to modularize code by breaking it up into smaller functions of manageable size. You now hopefully have all the tools you need to do this.

You’ve learned:

  • How to create a user-defined function in Python
  • Several different ways you can pass arguments to a function
  • How you can revenir data from a function to its caller
  • How to add documentation to functions with docstrings et annotations

Next up in this series are two tutorials that cover searching et pattern matching. You will get an in-depth look at a Python module called re, which contains functionality for searching and matching using a versatile pattern syntax called a regular expression.