Skip to article frontmatterSkip to article content

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 doit pouvoir faire ceci

>>> myprint(1, 2, 3, sep='+')
HELLO 1+2+3

et une variante

# on doit pouvoir faire ceci

>>> myprint2('HEY', 1, 2, 3, sep='==')
HEY 1==2==3

implé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é :

# 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

les 4 sortes de paramètres

(I)

def foo(x):

paramètre positionnel ou ordonné ou usuel/normal

(II)

def foo(x=10):

paramètre avec valeur par défaut

(III)

def foo(*args):

correspond aux arguments non nommés “en plus”

(IV)

def foo(**kwds):

correspond aux arguments nommés “en plus”

les 2+2 sortes d’arguments

la différence fondamentale est à faire entre

(A)

foo(argument)

argument non nommé

(B)

foo(parametre=argument):

argument nommé

les paramètres

(I) 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

# 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):

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:

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 given

ex. 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

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 * (1 max),

forme ** (1 max)

arguments

dans un appel de fonction, on recommande de matérialiser deux groupes

  1. en premier les non-nommés:

    • argument(s) positionnels (name),

    • forme(s) *name

  2. 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 * (1 max),

forme ** (1 max)

reste une bonne approximation, mais:

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

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

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 !

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 !

  1. en premier on résoud les arguments positionnels et *args

  2. puis 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 * (1 max),

forme ** (1 max)