paramètres multiples¶
Python propose une large gamme de mécanismes pour le passage de paramètres,
pour définir aussi bien que pour appeler une fonction
chacun de ces mécanismes est assez simple pris individuellement,
mais un peu de soin est nécessaire pour bien expliquer le mécanisme général
use case: un wrapper¶
on veut écrire un wrapper autour de
print(), c’est-à-direune fonction
myprint()qui ajouteHELLOau début de chaque impressionmais sinon l’interface de
myprint()doit être exactement celle deprint(), i.e.nombre variable de paramètres
réglages inchangés - e.g.
myprint(..., file=f)
# on doit pouvoir faire ceci
>>> myprint(1, 2, 3, sep='+')
HELLO 1+2+3et une variante¶
ou encore, on veut pouvoir écrire une variante de
myprint,qui attend un premier argument obligatoire, le texte pour pour remplacer
HELLO,…
# on doit pouvoir faire ceci
>>> myprint2('HEY', 1, 2, 3, sep='==')
HEY 1==2==3implémentation¶
et pour commencer voyons comment on ferait ça en Python
# la première variante
def myprint(*args, **kwds):
print("HELLO", end=" ")
print(*args, **kwds)# ajout automatique de 'HELLO', et on peut utiliser tous
# les paramètres spéciaux de print()
myprint(1, 2, 3, sep='+')HELLO 1+2+3
# la deuxième variante, avec le premier paramètre obligatoire
def myprint2(obligatoire,
*args, **kwds):
print(obligatoire, end=" ")
print(*args, **kwds)# le premier paramètre sert à remplacer 'HELLO'
myprint2('HEY', 1, 2, 3, sep='==')HEY 1==2==3
vocabulaire¶
paramètres et arguments¶
précisons le vocabulaire lorsqu’il peut y avoir ambiguïté :
paramètre: le nom qui apparaît dans ledefargument: l’objet réellement passé à la fonction
# ici x est un PARAMÈTRE
def foo(x):
print(x)# et ici a est un ARGUMENT
a = 134 + 245
foo(a)379
le sujet que nous abordons ici, ce sont les règles qui permettent de lier les arguments aux paramètres
de façon à ce que tous les arguments soient exposés à la fonction
il y a 4 manières de déclarer un paramètre
et 2 manières de passer un argument à une fonction (en fait 2+2)
les deux familles se ressemblent un peu, mais il y a tout de même des différences
les 4 sortes de paramètres¶
(I) |
| paramètre positionnel ou ordonné ou usuel/normal |
(II) |
| paramètre avec valeur par défaut |
(III) |
| correspond aux arguments non nommés “en plus” |
(IV) |
| correspond aux arguments nommés “en plus” |
les 2+2 sortes d’arguments¶
la différence fondamentale est à faire entre
(A) |
| argument non nommé |
(B) |
| argument nommé |
les paramètres¶
(I) paramètre positionnel¶
c’est le mécanisme le plus simple et le plus répandu
les paramètres obtiennent un rang de gauche à droite
par exemple ci-dessous le paramètreprenomest le deuxième paramètre positionnel
# pour afficher quel argument est attaché à quel paramètre
def agenda(nom, prenom, tel, age, job):
print(f"{nom=}, {prenom=}, {tel=}, {age=}, {job=}")appel¶
comment peut-on alors appeler la fonction ?
# appel usuel, sans nommage
# c'est l'ordre des arguments qui compte
agenda('doe', 'alice', '0404040404', 35, 'medecin')nom='doe', prenom='alice', tel='0404040404', age=35, job='medecin'
# et aussi, en nommant les arguments lors de l’appel
# on peut les mettre dans n’importe quel ordre
agenda(prenom='alice', nom='doe', age=35, tel='0404040404', job='medecin')nom='doe', prenom='alice', tel='0404040404', age=35, job='medecin'
(II) paramètre avec valeur par défaut¶
dans le code précédent, qu’on les mette dans l’ordre ou pas, on doit passer à la fonction 5 arguments
parfois on veut dire “si on ne passe pas d’argument pour job, alors on prendra par défaut "medecin”
# ┌──────┬─────┬────────────────── positionnels
# │ │ │ ┌─────────┬─── avec valeurs par défaut
# ↓ ↓ ↓ ↓ ↓
def agenda(nom, prenom, tel, age = 35, job = 'medecin'):
print(f"{nom=}, {prenom=}, {tel=}, {age=}, {job=}")appels¶
comment on peut alors appeler la fonction ?
# appel en suivant la signature
# il manque deux arguments, on utilise les valeurs par défaut
agenda('Dupont', 'Jean', '123456789')nom='Dupont', prenom='Jean', tel='123456789', age=35, job='medecin'
# on peut aussi nommer les arguments, et à nouveau
# ça permet de mélanger l'ordre des paramètres imposés
# ici aussi job est manquant, on utilise la valeur par défaut
agenda(prenom = 'alice', nom = 'doe', age = 25, tel = '0404040404')nom='doe', prenom='alice', tel='0404040404', age=25, job='medecin'
# on peut mixer les deux approches
# ici les trois premiers sont liés dans l'ordre
agenda('Dupont', 'Jean', '123456789', age = 25, job = 'avocat')nom='Dupont', prenom='Jean', tel='123456789', age=25, job='avocat'
(III) paramètre multiple *args¶
jusqu’ici c’est assez simple, c’est maintenant que ça devient un peu plus inhabituel
le paramètre *args s’appelle aussi parfois attrape-tout; lorsqu’on en met un, (et on n’a droit d’en mettre qu’un seul):
alors Python collecte tous les arguments non nommés restants - i.e. non liés à un paramètre
il les range dans un tuple
qu’il lie au paramètre
args
de cette façon on peut donc créer très simplement une fonction qui accepte un nombre variable d’arguments (non nommés)
et pour les exploiter la fonction n’a qu’à, par exemple, itérer sur le paramètre args
ex. avec 0 ou plus arguments¶
# définition
def variable(*args):
print(f"args={args}")# 0 argument
variable()args=()
# 1 argument
variable(1)args=(1,)
# 5 arguments
variable(1, 2, 3, 4, "cinq")args=(1, 2, 3, 4, 'cinq')
ex. avec au moins 2 arguments¶
on peut aussi très simplement créer une fonction qui attend au moins deux arguments, le reste étant optionnel
# au moins deux arguments
def variable2(one, two, *args):
print(f"one={one}, two={two}, args={args}")# 2 arguments
variable2(1, 2)one=1, two=2, args=()
# 3 arguments
variable2(1, 2, 3)one=1, two=2, args=(3,)
# 5 arguments
variable2(1, 2, 3, 4, "cinq")one=1, two=2, args=(3, 4, 'cinq')
# 1 seul argument -> TypeError
variable2(1)---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[22], line 3
1 # 1 seul argument -> TypeError
----> 3 variable2(1)
TypeError: variable2() missing 1 required positional argument: 'two'un seul *args¶
redisons-le: *args ne peut apparaître qu’une fois (car sinon il y aurait ambiguïté)
# si on met plusieurs paramètres *args, python n'est pas content
def variable(*args1, *args2):
pass Cell In[23], line 3
def variable(*args1, *args2):
^
SyntaxError: * argument may appear only once
(IV) paramètre multiple **kwds¶
le mécanisme est exactement le même, mais avec les arguments nommés:
on regarde tous ceux qui n’ont pas encore été liés à un paramètre,
au lieu de créer un tuple, on crée cette fois un un dictionnaire, de façon à mémoriser les noms en plus des valeurs
et c’est ce dictionnaire qui est affecté au paramètre
kwds
ici encore le nombre d’arguments nommés peut être quelconque
ex. 1¶
# cette fonction peut être appelée avec autant d'arguments
# qu'on veut, mais il doivent tous être nommés
def named_args(**kwds):
print(f"kwds={kwds}")
# var_named
named_args()kwds={}
named_args(a = 1)kwds={'a': 1}
named_args(a = 1, b = 2)kwds={'a': 1, 'b': 2}
# si on essaie de lui passer un argument non nommé, python n'est pas content !
named_args(10)---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[27], line 3
1 # si on essaie de lui passer un argument non nommé, python n'est pas content !
----> 3 named_args(10)
TypeError: named_args() takes 0 positional arguments but 1 was givenex. 2¶
# pareil ici, autant d'arguments nommés qu'on veut
# et cette fois on peut aussi lui passer un argument nommé
def named_args1(a=0, **kwds):
print(f"a={a} kwds={kwds}")
# var_named
named_args1(a=1)a=1 kwds={}
named_args1(1, b=2)a=1 kwds={'b': 2}
named_args1(a = 1, b = 2)a=1 kwds={'b': 2}
named_args1(b = 2, c=3)a=0 kwds={'b': 2, 'c': 3}
un seul **kwds¶
ici encore cette forme de paramètre ne peut apparaître qu’une fois
car sinon, comme avec
*args, la liaison serait ambigüe
ordre des paramètres et arguments¶
paramètres¶
l’ordre dans lequel sont déclarés les différents types de paramètres est imposé par le langage
historiquement à l’origine, on devait déclarer dans cet ordre :
positionnels, | avec défaut, | forme | forme |
arguments¶
dans un appel de fonction, on recommande de matérialiser deux groupes
en premier les non-nommés:
argument(s) positionnels (
name),forme(s)
*name
puis ensuite les arguments nommés
argument(s) nommés (
name=value),forme(s)
**name
exemple: appel¶
# une fonction passe-partout qui affiche juste ses paramètres
# pour nous permettre d'illustrer les appels
def show_any_args(*args, **kwds):
print(f"args={args} - kwds={kwds}")# les cas simples pour la voir marcher
show_any_args(1)args=(1,) - kwds={}
# et
show_any_args(x=1)args=() - kwds={'x': 1}
# on recommande de mettre les arguments non-nommés en premier
show_any_args(1, 4, 5, 3, x = 1, y = 2)args=(1, 4, 5, 3) - kwds={'x': 1, 'y': 2}
# car ceci est illégal et déclenche une SyntaxError
foo(1, x=1, 4, 5, 3, y=2) Cell In[36], line 3
foo(1, x=1, 4, 5, 3, y=2)
^
SyntaxError: positional argument follows keyword argument
exemple: définition¶
# même punition ici: SyntaxError, on n'a pas respecté le bon ordre !
def foo(b=10, a):
pass Cell In[37], line 3
def foo(b=10, a):
^
SyntaxError: parameter without a default follows parameter with a default
keyword-only / positional-only¶
(avancé)
l’ordre dans lequel on devait déclarer les paramètres
positionnels, | avec défaut, | forme | forme |
reste une bonne approximation, mais:
en Python-3 on a introduit les paramètres keyword-only
on peut ainsi définir un paramètre qu’il faut impérativement nommer lors de l’appel
et également en 3.8 les paramètres positional-only
qui introduit des paramètres usuels qu’on ne peut pas nommer lors de l’appel
voyons comment marchent ces deux mécanismes
paramètre keyword-only¶
on va prendre une fonction qui combine un peu tous les types de paramètres, et pour commencer on va les mettre dans l’ordre standard
# une fonction qui combine les différents types de paramètres
def normal(a, b=100, *args, **kwds):
print(f"a={a}, b={b}, args={args}, kwds={kwds}")et profitons-en pour voir comment on peut l’appeler et ce que ça donne:
normal(1)a=1, b=100, args=(), kwds={}
normal(1, 2)a=1, b=2, args=(), kwds={}
normal(1, 2, 3)a=1, b=2, args=(3,), kwds={}
normal(1, 2, 3, bar=1000)a=1, b=2, args=(3,), kwds={'bar': 1000}
normal(1, 2, 3, bar=1000)a=1, b=2, args=(3,), kwds={'bar': 1000}
imaginons maintenant que je veuille imposer à l’appelant de nommer b
pour cela il me suffit de déplacer l’attrape-tout avant le paramètre b comme ceci
# on peut déclarer un paramètre nommé **après** l'attrape-tout *args
# du coup ici le paramètre nommé `b` devient un *keyword-only* parameter
def must_name_b(a, *args, b=100, **kwds):
print(f"a={a}, b={b}, args={args}, kwds={kwds}")avec cette déclaration, je dois nommer le paramètre b
# je peux toujours faire ceci
must_name_b(1)a=1, b=100, args=(), kwds={}
# mais si je fais ceci l'argument 2
# va aller dans args
must_name_b(1, 2)a=1, b=100, args=(2,), kwds={}
# pour passer b=2, je **dois** nommer mon argument
must_name_b(1, b=2)a=1, b=2, args=(), kwds={}
paramètre positional-only¶
en général on peut toujours nommer, des arguments même si le paramètre, est positionnel
# on peut nommer un paramètre positionnel
def normal(a, b, c):
print(f"{a=} {b=} {c=}")# la preuve
normal(b=2, a=1, c=3)a=1 b=2 c=3
imaginons que je veuille maintenant, au contraire empêcher l’appelant de nommer a
pour cela je vais insérer artificiellement un / dans les paramètres; tous ceux qui sont déclarés avant le / seront dits positional-only, ce qui signifie qu’on ne pourra plus les nommer
# avec cette déclaration, on ne pourra plus nommer a
def cannot_name_a(a, /, b, c):
print(f"{a=} {b=} {c=}")# la preuve
cannot_name_a(b=2, a=1, c=3)---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[51], line 3
1 # la preuve
----> 3 cannot_name_a(b=2, a=1, c=3)
TypeError: cannot_name_a() got some positional-only arguments passed as keyword arguments: 'a'# par contre on peut toujours nommer les deux autres
cannot_name_a(1, c=3, b=2)a=1 b=2 c=3
unpacking des arguments¶
où on voit comment sont traités les formes *L et **D, mais dans les arguments de la fonction cette fois - on avait appelé ça (C) et (D)
cette fois le mécanisme est plutôt simple: cela revient à “déballer” le contenu de L (qui doit être itérable) ou D (qui doit être un dictionnaire)
voici un exemple: admettons que l’on ait calculé ces deux trucs
L = [10, 20]
D = ['a': 1, 'b': 2]alors l’appel foo(100, *L, 1000, *L, x=0, **D1) sera récrit comme ceci par Python
comme on le voit, cela revient à insérer les contenus en place dans les arguments
les éléments de
Lcomme des arguments non nomméset ceux de
Dcomme des arguments nommés
ex. (C) avec *¶
def f4(a, b, c, d):
print(f"{a=} {b=} {c=} {d=}")L = [1, 2, 3, 4]
f4(*L)a=1 b=2 c=3 d=4
# n'importe quel itérable
f4(*"abcd")a='a' b='b' c='c' d='d'
L1, L2 = (1, 2), (3, 4)
# 2 *params dans le même appel
# ne posent pas problème
f4(*L1, *L2)a=1 b=2 c=3 d=4
# et on peut utiliser * avec une expression
f4(*range(1, 3), *range(10, 12))a=1 b=2 c=10 d=11
ex. (D) avec **¶
def f3(a, b, c):
print(f"{a=} {b=} {c=}")
D = {'a': 1, 'c': 3, 'b': 2}
# équivalent à func(a=1, b=2, c=3)
f3(**D)a=1 b=2 c=3
retombées sur la syntaxe de base¶
sachez qu’on peut également faire ceci - qui n’a plus rien à voir avec les appels de fonction, mais qui utilise le même principe
# construire une liste avec *args
l1 = [2, 3]
l2 = [4, 5]
[1, *l1, *l2, 6][1, 2, 3, 4, 5, 6]# pareil avec un dictionnaire
d1 = {2: 'b', 3: 'c'}
d2 = {4: 'd', 5: 'e'}
{1: 'a', **d1, **d2, 6: 'f' }{1: 'a', 2: 'b', 3: 'c', 4: 'd', 5: 'e', 6: 'f'}piège fréquent avec les arguments par défaut¶
les valeurs par défaut sont évaluées à l’endroit de la déclaration de la fonction
i = 5
def f(arg = i): # i vaut 5 au moment de la déclaration
print(arg)
i = 6 # i est mis à 6 après la déclaration, ça
# n’est pas pris en compte
f()5
pas de mutable !¶
les valeurs par défaut de f ne sont évaluées qu’une fois à la création de l’objet fonction (et mises dans f.defaults)
si la valeur par défaut est mutable, elle pourra être modifiée dans la fonction
et dans ce cas, la valeur par défaut est modifiée pour l’appel suivant !!
exemple¶
# on pourrait penser en lisant ceci, que sans préciser L on devrait
# toujours retourner une liste [a]
def f(a, L = []):
L.append(a)
return L# MAIS: la valeur par défaut est évaluée par l'instruction def:
f.__defaults__([],)# donc ici le premier coup OK, ça fait ce qu'on attend
f(1)[1]# sauf que ATTENTION, on a modifié ceci
f.__defaults__([1],)# si bien qu'à l'appel suivant il se passe ceci !
f(2)[1, 2]comment faire alors ?¶
la bonne pratique consiste à remplacer le mutable par None et à tester “à la main”
de cette manière l’expression [] - qui crée effectivement la liste - est exécutée à chaque fois que nécessaire,
au lieu d’une seule fois au moment du def
# la bonne pratique
def f(a, L=None):
if L is None:
L = []
L.append(a)
print(L)# comme ça pas d'embrouille
f(1)
f(2)
f(3)[1]
[2]
[3]
pour résumer¶
2 groupes d’arguments : positionnels et nommés¶
attention: les arguments ne sont pas pris dans l’ordre de l’appel !
en premier on résoud les arguments positionnels et
*argspuis les arguments nommés et
**kwds
le bon ordre pour les paramètres¶
l’ordre dans lequel il est conseillé de déclarer sa fonction reste toujours
positionnels, | avec défaut, | forme | forme |