"""
Sommaire :
1 - Définition et syntax
2 - Fonction sans paramètre(procédure)
3 - Fonction avec un paramètre
4 - Fonction avec plusieurs paramètres
5 - Fonction renvoyant un résultat
6 - Fonction renvoyant plusieurs résultats
7 - Fonction avec sorties anticipées
8 - BONUS : fonction avec paramètres par défaut

---------------------------------------
1 - Définition
---------------------------------------

Une fonction isole un fragment de programme répondant un objectif et lui associe un nom. 
Le nom peut ensuite être utilisé pour appeler la fonction, c'est-a-dire faire s'exécuter le fragment de programme qu'elle désigne (son corps). 
L’objectif d’une fonction peut être la réalisation d'une action, auquel cas l’appel d’une fonction prend la place d'une instruction, ou le calcul valeur (comparable à la notion de fonction en mathématiques), l’appel étant alors utilisée au sein d'une expression. 
Le corps d'une fonction peut faire référence à des paramètres pour lesquels on fournit des valeurs potentiellement différentes à chaque appel. 

Les fonctions permettent de décomposer un programme complexe en une série de sous-programmes plus simples. 
De plus, les fonctions sont réutilisables : si nous disposons d'une fonction capable de calculer une racine carrée, par exemple, nous pouvons l'utiliser un peu partout dans notre programme sans avoir à la réécrire à chaque fois (on parle de factorisation du code)

Voici la structure d'une fonction en python 

def nom_de_la_fonction (paramètres):
	corps de la fonction

---------------------------------------
2 - Fonction sans paramètre(procédure)
---------------------------------------
"""

def bonjour(): 	# Le nom de la fonction est bonjour, les parenthèses sont vides car la fonction n'a pas de paramètre.
	print("Hello world !") # Le corps de la fonction est indenté par rapport au mot clé def.

# On appelle notre fonction bonjour 3 fois. On met bien des parenthèses vides à la fin.
bonjour() 
bonjour()
bonjour()

"""
output :
Hello world !
Hello world !
Hello world !

---------------------------------------
3 - Fonction avec un paramètre
---------------------------------------
"""

def double(n): 		# On définit un paramètre ayant pour nom n, qui n'existe que dans le corps ma fonction.
	print(n*2) 		# On utilise n pour réaliser une opération.
	
double(2)			# Les parenthèses ne sont pas vides car on passe la valeur de n en paramètre. Pour cet appel, on aura n égal à 2.
double(-5) 		
double(0.25) 	

"""
output :
4
-10
0.5

---------------------------------------
4 - Fonction avec plusieurs paramètres
---------------------------------------
"""

def somme(a, b):	# On a définit 2 paramètres, a et b
	print(a + b)	# On affiche leur somme
	
somme(2, 5)			# Pour cet appel, a est égal 2 et b est égal 5
somme(-5, 5)
somme(0.25, 0.1)

"""
output :
7
0
0.35

---------------------------------------
4 - Fonction renvoyant un résultat
---------------------------------------
On aimerait calculer le double du double de 0.25 avec des appels imbriqués : 
double(double(0.25)) qui donnerait 1.0 
Mais avec nos fonctions précédentes, ce n'est pas possible car le résultat est directement affiché dans la console python.
A la place, on souhaiterait que le résultat soit renvoyé par la fonction afin de pouvoir l'utiliser pour d'autres calculs.
On va donc utiliser le mot clé return à la place de la fonction d'affichage print.
"""

def produit(a, b):
	return a * b 		# On retourne le résultat de a*b. Il n'est pas affiché directement, contrairement au résultat de la fonction somme.

c = produit(2, 3) 		# c sera affecté à 6, la valeur retournée par produit(2, 3)
print(c)				# 6
print(produit(7, -9))	# -63 	Les fonction print et produit sont imbriquées. La fonction produit est exécutée en premier et son résultat est passé en paramètre de la fonction print.

# 3 appels imbriqués, la fonction la plus à l'intérieure est toujours calculée en premier.
# On calcule en premier 2*3 = 6, puis 6*4 = 24 et enfin 24*5 = 120
d = produit(5, produit(4, produit(3, 2))) 
print(d)

"""
output:
6
-63
120

Attention, en python, une fonction renvoie toujours un résultat, même sans la présence d'un return.
S'il n'y a pas de return, la valeur retourné est None.
"""

def test():
	pass # Mot clé python pour ne rien faire. Il n'y a pas de return, donc c'est une procédure.

a = test()
print(a)

"""
output:
None

---------------------------------------
5 - Fonction renvoyant plusieurs résultats
---------------------------------------
Pour renvoier plusieurs résultats, on va devoir créé une structure appelé tuple.
On la créé en mettant nos différents résultats entre parenthèses, séparés par des virgules.
On accède aux différentes valeurs du résultat avec des crochets [] et l'indice de position de la valeur, le premier indice étant toujours le 0.
"""

# attentien, erreur si b == 0 car on ne peut pas diviser par 0
def somme_difference_produit_division_quotient_reste_puissance(a, b):
	s = a + b
	d = a - b
	p = a * b
	div = a / b
	q = a // b
	r = a % b
	puis = a ** b
	return (s, d, p, div, q, r, puis)
	
operation = somme_difference_produit_division_quotient_reste_puissance(3, 6)
print(operation) # affiche tout le tuple
print(operation[0]) # affiche la somme, qui est la première valeur (indice 0)
print(operation[2]) # affiche le produit, qui est la troisième valeur (indice 2)
print(operation[6]) # affiche la puissance, qui est la septième et dernière valeur (indice 6)

"""
output :
(9, -3, 18, 0.5, 0, 3, 729)
9
18
729

---------------------------------------
6 - Fonctions avec sorties anticipées
---------------------------------------
On peut sortir d'une fonction en utilisant le mot clé return sans valeur.
C'est parfois utilse si on ne veut pas avoir à réécrire son code pour gérer un cas particulier.
"""

def division(a, b):
	if b == 0: 		# On ne veut pas diviser par 0
		return
	else:
		print(a/b)
		
division(5, 2) 		# 2.5
division(5, 0) 		# Ne va rien afficher car la condition b == 0 est vrai.
division(15, 3)		# 5.0

"""
output:
2.5
5.0

L'exécution de la fonction s'arrête au premier mot clé return rencontré.
"""

def test():
    return True
    return False		# Cette instruction 
	a = 2				# ainsi que celle-ci ne sont jamais exécutées.

print(test()) # return 1

"""
output:
True

---------------------------------------
8 - BONUS : fonction avec paramètres par défaut
---------------------------------------
On peut définir des valeurs par défaut aux paramètres d'une fonction.
Ces valeurs seront utilisées si l'appel de la fonction est fait avec des paramètres manquants.
L'ordre des paramètres n'est pas important car on ne peut explicitement nommer le paramètre et indiquer sa valeur.
"""

def affiche_n(t = "Hello World !", n=1):
	for i in range(n):
		print(t)

affiche_n() 			# aucun paramètre, on utilise les deux valeurs par défaut.
affiche_n("Bou !")		# t est passé et vaut "Bou !", mais n est manquant donc vaudra 1
affiche_n("BABABA", 4)	# tous les paramètres sont définis, donc t = "BABABA" et n=4
affiche_n(n=2)			# t étant omis il prendra la valeur par défault, et n vaut 2
affiche_n(n=2, t="XA")	# en nommant explicitement les paramètres, on peut changer leur ordre

"""
output:
Hello World !
Bou !
BABABA
BABABA
BABABA
BABABA
Hello World !
Hello World !
XA
XA
"""