Se familiariser avec la lecture et l'écriture de fonctions Python avec différents types de paramètres formels, explorer certaines nuances de la sémantique d'exécution des fonctions et plonger dans l'intérieur des fonctions.
Ce TD est particulièrement axé sur la sémantique Python, ce qui peut ne pas sembler passionnant au premier abord. Cependant, la maîtrise de la mécanique des fonctions Python vous donne accès à tout un tas d'outils puissants qui soit n'existent pas, soit sont peu connus ou difficiles à utiliser dans d'autres langages. Les compétences acquises dans TD vous permettront d'écrire (et de déboguer) des codes Python puissants rapidement et facilement !
Comme pour le TD2, nous ne nous attendons pas à ce que vous terminiez tout les exercices pendant la séance de TD. Si vous le faites, c'est parfait! Mais sinon, nous vous encourageons à travailler chez vous à votre propre rythme sur le reste du TD - il explore les aspects intéressants et intrigants des fonctions Python.
Comme toujours, consultez les diapositives du cours si vous avez besoin.
Considérons la fonction suivante :
def print_two(a, b) :
print("Arguments : {0} et {1} ".format(a, b))
Pour chacun des appels de fonction suivants, prévoyez si l'appel est valide ou non. S'il est valide, quel sera le résultat ? S'il n'est pas valide, quelle est la cause de l'erreur ?
*Note : faites vos prédictions avant d'exécuter le code dans l'interpréteur interactif. Vérifiez ensuite vous-même!*
# Valide ou non-valide ?
print_two()
print_two(4, 1)
print_two(41)
print_two(a=4, 1)
print_two(4, a=1)
print_two(4, 1, 1)
print_two(b=4, 1)
print_two(a=4, b=1)
print_two(b=1, a=4)
print_two(1, a=1)
print_two(4, 1, b=1)
Rédigez au moins deux autres exemples d'appels de fonction, non énumérés ci-dessus, et prévoyez leur résultat. Sont-ils valides ou non? Vérifiez votre hypothèse.
Ces problèmes d'écriture supplémentaire vous permettent de clarifier votre propre compréhension de la sémantique des appels de fonction. Vous pouvez les ignorer si vous le souhaitez, mais l'utilisation de l'interpréteur interactif pour tester vos propres hypothèses est une compétence Python cruciale qui vous permet de répondre à des questions de la forme "Mais que se passe-t-il si je... "
Considérons la fonction suivante :
def keyword_args(a, b=1, c='X', d=None) :
print("a :", a)
print("b :", b)
print("c :", c)
print("d :", d)
Pour chacun des appels de fonction suivants, prévoyez si l'appel est valide ou non. S'il est valide, quel sera le résultat? S'il n'est pas valide, quelle est la cause de l'erreur?
keyword_args(5)
keyword_args(a=5)
keyword_args(5, 8)
keyword_args(5, 2, c=4)
keyword_args(5, 0, 1)
keyword_args(5, 2, d=8, c=4)
keyword_args(5, 2, 0, 1, "")
keyword_args(c=7, 1)
keyword_args(c=7, a=1)
keyword_args(5, 2, [], 5)
keyword_args(1, 7, e=6)
keyword_args(1, c=7)
keyword_args(5, 2, b=4)
Rédigez au moins deux autres exemples d'appels de fonction, non énumérés ci-dessus, et prévoyez leur résultat. Sont-ils valides ou non? Vérifiez votre hypothèse.
Comme précédemment, considérez la définition de fonction suivante :
def variadic(*args, **kwargs) :
print("Positional :", args)
print("Keyword :", kwargs)
Pour chacun des appels de fonction suivants, prévoyez si l'appel est valide ou non. S'il est valide, quel sera le résultat? S'il n'est pas valide, quelle est la cause de l'erreur ?
variadic(2, 3, 5, 7)
variadic(1, 1, n=1)
variadic(n=1, 2, 3)
variadic()
variadic(inf="Informatique", asr="ASR")
variadic(inf="Informatique", inf="InFo", inf="INF")
variadic(5, 8, k=1, swap=2)
variadic(8, *[3, 4, 5], k=1, **{'a':5, 'b':'x'})
variadic(*[8, 3], *[4, 5], k=1, **{'a':5, 'b':'x'})
variadic(*[3, 4, 5], 8, *(4, 1), k=1, **{'a':5, 'b':'x'})
variadic({'a':5, 'b':'x'}, *{'a':5, 'b':'x'}, **{'a':5, 'b':'x'})
Rédigez au moins deux autres exemples d'appels de fonction, non énumérés ci-dessus, et prévoyez leur résultat. Sont-ils valides ou non? Vérifiez votre hypothèse.
Souvent, cependant, nous ne voyons pas seulement des arguments de mots-clés de listes de paramètres variadiques dans des situations isolées. La définition de fonction suivante, qui comprend des paramètres de position, des paramètres de mots-clés, des paramètres de position variadiques, des paramètres par défaut de mots-clés uniquement et des paramètres de mots-clés variadiques, est un Python valide.
def all_together(x, y, z=1, *nums, indent=True, spaces=4, **options) :
print("x :", x)
print("y :", y)
print("z :", z)
print("nums :", nums)
print("indent :", indent)
print ("spaces :", spaces)
print("options :", options)
Pour chacun des appels de fonction suivants, prévoyez si l'appel est valide ou non. S'il est valide, quel sera le résultat? S'il n'est pas valide, quelle est la cause de l'erreur?
all_together(2)
all_together(2, 5, 7, 8, indent=False)
all_together(2, 5, 7, 6, indent=None)
all_together()
all_together(indent=True, 3, 4, 5)
all_together(**{"indent" : False}, scope="maximum")
all_together(dict(x=0, y=1), *range(10))
all_together(**dict(x=0, y=1), *range(10))
all_together(*range(10), **dict(x=0, y=1))
all_together([1, 2], {3:4})
all_together(8, 9, 10, *[2, 4, 6], x=7, spaces=0, **{'a':5, 'b':'x'})
all_together(8, 9, 10, *[2, 4, 6], spaces=0, **{'a' :[4,5], 'b':'x'})
all_together(8, 9, *[2, 4, 6], *dict(z=1), spaces=0, **{'a' :[4,5], 'b':'x'})
Rédigez au moins deux autres exemples d'appels de fonction, non énumérés ci-dessus, et prévoyez leur résultat. Sont-ils valides ou non ? Vérifiez votre hypothèse.
parler_enthousiasme
Écrivez une fonction parler_enthousiasme
qui prend un argument positionnel obligatoire (un message) et deux arguments optionnels de mot-clé, le premier étant un entier positif se référant au nombre de points d'exclamation à mettre à la fin du message (par défaut à 1), et le second étant un booléen indiquant s'il faut ou non mettre le message en majuscule (par défaut à False
).
À quoi ressembleraient la signature et l'implémentation de cette fonction?
def parler_enthousiasme( ???):
pass
Comment appelleriez-vous cette fonction pour produire les résultats suivants ?
"J'aime Python !"
"Les arguments des mots-clés sont géniaux !!!!"
"Je suppose que Java est bien..."
"ALLONS À EVRY !!"
average
Écrivez une fonction average
qui accepte un nombre variable d'arguments positionnels entiers et calcule la moyenne. Si aucun argument n'est fourni, la fonction devrait retourner None
.
À quoi ressembleraient la signature et l'implémentation de cette fonction?
def average ( ???):
pass
Il devrait être possible d'appeler la fonction comme suit :
average() # => Aucune
average(5) # => 5,0
average(6, 8, 9, 11) # => 8,5
Supposons que nous ayons une liste l = [ ???]
fournie par l'utilisateur de longueur inconnue. Comment pouvons-nous utiliser la fonction average
que nous venons d'écrire pour calculer la moyenne de cette liste? Pour cette seconde moitié du problème, n'utilisez pas les fonctions intégrées sum
ou len
--- essayez de passer le contenu de l
à average
.
Prévoyez le résultat du code suivant. Ensuite, lancez le code pour vérifier votre hypothèse.
def say_hello() :
print("Bonjour !")
print(say_hello()) # => ?
def echo(arg=None) :
print("arg :", arg)
retour arg
print(echo()) # => ?
print(echo(5)) # => ?
print(echo("Hello")) # => ?
def drive(has_car) :
if not has_car :
# Ne jamais signaler une erreur de ce genre
return "Ah non !
return 100 # km
print(drive(False)) # => ?
print(drive(True)) # => ?
Lecture supplémentaire : Blog de Jeff Knupp
Supposons que nous ayons les deux fonctions suivantes :
def reassign(arr) :
arr = [4, 1]
print("Inside reassign : arr = {}".format(arr))
def append_one(arr) :
arr.append(1)
print("Inside append_one : arr = {}".format(arr))
Prévoyez ce que le code suivant va produire. Quelle est la différence entre deux parties? Quelle est la cause de cette différence?
l = [4]
print("Avant la réaffectation : arr={}".format(l)) # => ?
reassign(l)
print("Après la réaffectation : arr={}".format(l)) # => ?
l = [4]
print("Avant l'append_one : arr={}".format(l)) # => ?
append_one(l)
print("Après append_one : arr={}".format(l)) # => ?
Lecture supplémentaire : Python's Execution Model, en particulier la section 4.2.2.
Prédire la sortie des deux programmes Python suivants, puis les exécuter pour confirmer ou infirmer votre hypothèse.
# Cas 1
x = 10
def foo() :
print("(dedans foo) x :", x)
y = 5
print('valeur:', x * y)
print("(dehors foo) x :", x)
foo()
print("(après foo) x :", x)
et
# Cas 2
x = 10
def foo() :
x = 8 # On a ajouté seulement cette ligne - tout le reste est identique
print("(dedans foo) x :", x)
y = 5
print('valeur:', x * y)
print("(dehors foo) x :", x)
foo()
print("(après foo) x :", x)
Dessinez une image des liaisons variables à chaque portée (portée globale et portée au niveau de la fonction "foo") dans chaque cas.
Si nous échangeons seulement deux lignes de code, quelque chose d'inhabituel se produit. Quelle est l'erreur ? Pourquoi cela pourrait-il se produire ?
x = 10
def foo() :
print("(dedans foo) x :", x) # Nous échangons cette ligne
x = 8 # avec celle-ci
y = 5
print('valeur:', x * y)
print("(dehors foo) x :", x)
foo()
print("(après foo) x :", x)
Dans le même esprit, "foo", tel que défini dans
lst = [1,2,3]
def foo() :
lst.append(4)
foo()
se compilera (c'est-à-dire que l'objet fonction sera compilé en binaire sans problème), mais
lst = [1,2,3]
def foo() :
lst = lst + [4]
foo()
soulèvera une UnboundLocalError. Pourquoi? Cela n'a pas, de manière surprenante, à voir avec le fait que append
est en place et +
ne l'est pas.
C'est un problème si courant que la FAQ Python a une section consacrée à ce type de UnboundLocalError
.
Notez que les mots-clés global
et nonlocal
peuvent être utilisés pour affecter une variable en dehors de la portée de la fonction actuellement active (la plus interne). Si vous êtes intéressé, vous pouvez en savoir plus sur les règles de portée dans la lecture supplémentaire, ou dans cette section de la FAQ.
Les valeurs par défaut d'une fonction sont évaluées au point de définition dans la portée de définition. Par exemple
x = 5
def square(num=x) :
return num * num
x = 6
square() # => 25, et non 36
square(x) # => 36
Attention : Les valeurs par défaut d'une fonction sont évaluées une seule fois, lorsque la définition de la fonction. Ceci est important lorsque la valeur par défaut est un objet mutable (modifiable) tel qu'une liste ou un dictionnaire
Prévoyez ce que fera le code suivant, puis exécutez-le pour tester votre hypothèse :
def append_twice(a, lst=[]) :
lst.append(a)
lst.append(a)
return lst
# Marche bien lorsque le mot-clé est fourni
print(append_twice(1, lst=[4])) # => [4, 1, 1]
print(append_twice(11, lst=[2, 3, 5, 7])) # => [2, 3, 5, 7, 11, 11]
# Mais que se passe-t-il ici ?
print(append_twice(1))
print(append_twice(2))
print(append_twice(3))
Après avoir lancé le code, vous devriez voir apparaître à l'écran ce qui suit :
[1, 1]
[1, 1, 2, 2]
[1, 1, 2, 2, 3, 3]
Pourquoi?
Si vous ne souhaitez pas que la valeur par défaut soit partagée entre les appels suivants, vous pouvez utiliser une valeur sentinelle comme valeur par défaut (pour signaler qu'aucun argument de mot-clé n'a été explicitement fourni). Si c'est le cas, votre fonction peut ressembler à quelque chose comme
def append_twice(a, lst=None) :
if lst is None :
lst = []
lst.append(a)
lst.append(a)
return lst
Parfois, cependant, ce comportement étrange d'initialisation de la valeur des mots-clés peut être souhaitable. Par exemple, il peut être utilisé comme un cache modifiable et accessible par toutes les appels d'une fonction :
def fib(n, cache={0 : 1, 1 : 1}) :
if n in cache : # Note : la valeur par défaut capture nos cas de base
return cache[n]
out = fib(n-1) + fib(n-2)
cache[n] = out
return out
Cool, non? Le cache suit la fonction autour, comme un attribut sur l'objet de la fonction! Malgré tout, il existe de meilleures façons pour capturer ce modèle particulier de conception du cache (voir functools.lru_cache). Néanmoins, c'est une astuce qui pourrait s'avérer utile !
Nous allons explorer les attributs des objets de fonction plus en profondeur ici.
Habituellement, ces informations ne sont pas particulièrement utiles pour les praticiens (vous aurez rarement besoin de connaître en profondeur l'intérieur des fonctions), mais même le fait de voir que vous peut en Python est très cool.
__defaults__
et __kwdefaults__
)Toutes les valeurs par défaut (soit les arguments par défaut, soit les arguments de mot-clé par défaut qui suivent un paramètre d'argument positionnel variadique) sont liées à l'objet de la fonction au moment de sa définition. Considérons la fonction all_together
précédemment :
def all_together(x, y, z=1, *nums, indent=True, espaces=4, **options) : passe
all_together.__defaults__ # => (1, )
all_together.__kwdefaults__ # => {'indent':True, 'spaces':4}
Pourquoi l'attribut __defaults__
serait-il un tuple, alors que l'attribut __kwdefaults__
serait un dictionnaire ?
__doc__
)La première chaîne littérale d'une fonction, si elle précède une expression, est liée à l'attribut __doc__
de la fonction.
def ma_fonction() :
"""Ligne de résumé : ne rien faire, mais le documenter.
Description : Non, vraiment, ça ne fait rien.
"""
passer
print(ma_fonction.__doc__)
# Ligne de résumé : Ne faites rien, mais documentez-le.
#
# Description : Non, vraiment, ça ne fait rien.
Comme indiqué dans le cours, de nombreux outils utilisent ces chaînes de documentation de manière très avantageuse. Par exemple, la fonction help
intégrée affiche les informations de docstring
, et de nombreux outils de génération de documentation API comme Sphynx ou Epydoc utilisent les informations contenues dans docstring
pour former des références et des liens sur les sites web de documentation.
En outre, le module de bibliothèque standard doctest, selon ses propres termes, "recherche les textes qui ressemblent à des sessions Python interactives, puis exécute ces sessions pour vérifier qu'elles fonctionnent exactement comme indiqué". Cool !
__code__
)Dans CPython, l'implémentation de référence de Python utilisée par de nombreuses personnes (dont nous), les fonctions sont compilées dans une sorte de codes binaires lorsqu'elles sont définies. Cet objet de code est lié à l'attribut __code__
, et possède une tonne de propriétés intéressantes, illustrées au mieux par des exemples.
def all_together(x, y, z=1, *nums, indent=True, spaces=4, **options) :
"""Un commentaire inutile""
print(x + y * z)
print(sum(nums))
for k, v in options.items() :
if indentation :
print("{}\t{}".format(k, v))
else :
print("{}{}{}".format(k, " " * spaces, v))
code = all_together.__code__
Attribut | Valeur de l'échantillon | Explication |
---|---|---|
.co_argcount |
3 |
nombre d'arguments de position (y compris les arguments avec des valeurs par défaut) |
.co_cellvars |
() |
tuple contenant les noms des variables locales qui sont référencées par des fonctions imbriquées |
.co_code |
b't\x00\x00...\x00\x00S' |
chaîne représentant la séquence d'instructions du bytecode |
.co_consts |
('A useless comment', '{}\t{}', '{}{}{}', ' ', None) |
tuple contenant les littéraux utilisés par le bytecode - notre 'None' provient du 'return None' implicite à la fin |
.co_filename |
<stdin> |
fichier dans lequel la fonction a été définie |
.co_firstlineno |
1 |
ligne du fichier la première ligne de la fonction apparaît |
.co_flags |
79 |
ET de drapeaux binaires spécifiques au compilateur dont la signification interne est (en grande partie) non spécifiée |
.co_freevars |
() |
tuple contenant les noms des variables libres |
.co_kwonlyargcount |
2 |
nombre d'arguments de mots-clés seulement |
.co_lnotab |
b'\x00\x02\x12\x01\x10\x01\x19\x01\x06\x01\x19\x02' |
chaîne codant la correspondance des décalages de code d'octet aux numéros de ligne |
.co_name |
"all_together" |
le nom de la fonction |
.co_names |
('print', 'sum', 'items', 'format') |
tuple contenant les noms utilisés par le bytecode |
co_nlocals |
9 |
nombre de variables locales utilisées par la fonction (y compris les arguments) |
co_stacksize |
6 |
taille requise de la pile (y compris les variables locales) |
co_varnames |
('x', 'y', 'z', 'indent', 'spaces', 'nums', 'options', 'k', 'v') |
tuple contenant les noms des variables locales (commençant par les noms des arguments) |
Pour plus d'informations à ce sujet, et sur tous les types en Python, voir le site référence du modèle de données. Pour les objets de code, vous devez faire défiler vers le bas jusqu'à Internal Types
.
L'objet de code d'une fonction donnée peut être échangé contre l'objet de code d'une autre fonction (peut-être malveillante) au moment de l'exécution !
def nice() : print("Tu es génial !")
def mean() : print("Tu n'es... pas génial. OOOOH")
# Écrasez l'objet code pour le plaisir
nice.__code__ = moyen.__code__
nice() # prints "Tu n'es... pas génial. OOOOH"
dis
moduleLe module dis
exporte une fonction dis
qui permet de dé-sassembler le code de Python (au moins, pour les distributions Python implémentées dans CPython pour les versions existantes). Le code dé-sassemblé n'est pas exactement du code assembleur normal, mais plutôt une syntaxe Python spécialisée
def gcd(a, b) :
while b :
a, b = b, a % b
return a
import dis
dis.dis(gcd)
"""
2 0 SETUP_LOOP 27 (à 30)
>> 3 LOAD_FAST 1 (b)
6 POP_JUMP_IF_FALSE 29
3 9 LOAD_FAST 1 (b)
12 LOAD_FAST 0 (a)
15 LOAD_FAST 1 (b)
18 BINARY_MODULO
19 ROT_TWO
20 STORE_FAST 0 (a)
23 STORE_FAST 1 (b)
26 JUMP_ABSOLUTE 3
>> 29 POP_BLOCK
4 >> 30 LOAD_FAST 0 (a)
33 RETURN_VALUE
"""
Des détails sur les instructions elles-mêmes peuvent être trouvés ici.
Vous pouvez en savoir plus sur le module dis
ici.
__annotations__
)Comme mentionné en classe, Python nous permet de proposer des annotations de type sur les fonctions
def annotated(a : int, b : str) -> liste :
return [a, b]
print(annotated.__annotations__)
# {'b' : <classe 'str'>, 'a' : <classe 'int'>, 'return' : <classe 'list'>}
Ces informations peuvent être utilisées pour construire des vérificateurs de types dynamiques très soignés pour Python !
Pour plus d'informations, consultez PEP 3107 sur les annotations de fonctions ou Pep 484 sur les indications de type (qui ont été introduites dans Python 3.5)
__call__
)Toutes les fonctions Python ont un attribut __call__
, qui est l'objet réel appelé lorsque vous utilisez des parenthèses pour "appeler" une fonction. C'est-à-dire,
def greet() : print("Hello world !")
greet() # "Hello world !"
# est juste un sucre syntaxique pour
greet.__call__() # "Hello world !"
Cela signifie que tout objet (y compris les instances de classes personnalisées) avec une méthode "call" peut utiliser la syntaxe d'appel de fonction entre parenthèses ! Nous verrons beaucoup plus en détail l'utilisation de ces soi-disant "méthodes magiques" pour exploiter les opérateurs apparents de Python (comme les appels de fonction, +
(__add__
) ou *
(__mul__
), etc).
__module__
, __name__
, et __qualname__
)Les fonctions Python stockent également certaines informations sur le nom d'une fonction.
Le terme module
désigne le module qui était actif au moment où la fonction a été définie. Toute fonction définie dans l'interpréteur interactif aura un __module__ == '__main__'
.
__name__
est le nom de la fonction. Rien de spécial ici.
__qualname__
, qui signifie "nom qualifié", ne diffère de __name__
que lorsqu'il s'agit de fonctions imbriquées, dont nous parlerons plus loin à la semaine suivante.
inspect
En bref, tout ce fouillis avec les internes des fonctions de Python ne peut pas être bon pour notre santé. Heureusement, il existe un module de bibliothèque standard pour cela ! Le module inspect
nous donne beaucoup de bons outils pour interagir non seulement avec les internes des fonctions, mais aussi avec les internes de beaucoup d'autres types. Consultez la documentation pour quelques exemples intéressants.