Physique-Chimie & NSI

Cours complets et originaux de Physique-Chimie & NSI

1-06. Programmation fonctionnelle

Un paradigme de programmation est une façon de concevoir l'organisation d'un programme informatique. Il existe trois principaux paradigmes de programmation : impératif, fonctionnel et objet. Dans ce chapitre, on va s’intéresser principalement à la programmation fonctionnelle.

Paradigmes de programmation

  • Distinguer sur des exemples les paradigmes impératif, fonctionnel et objet.
  • Choisir le paradigme de programmation selon le champ d’application d’un programme.

Un paradigme de programmation, c'est un modèle de pensée qui guide la façon dont on va :

  • Structurer le code
  • Organiser les données
  • Décrire les processus de résolution de problèmes

Impératif

Le paradigme impératif est une approche de programmation qui décrit comment un programme doit s'exécuter à travers une séquence d'instructions qui modifient l'état du programme. Il repose sur quatre concepts fondamentaux :

  • La séquence d'instructions qui s'exécutent l'une après l'autre
  • L'affectation de variables (attribution de valeurs à des emplacements mémoire)
  • Les structures conditionnelles (if/else) pour créer des branchements
  • Les boucles (while/for) pour répéter des instructions

Ce paradigme reflète directement le fonctionnement de l'architecture matérielle des ordinateurs et constitue le fondement sur lequel sont construits d'autres paradigmes comme l'orienté objet ou le fonctionnel. C’est celui que vous utilisez principalement depuis la classe de première.

Le paradigme impératif peut être comparé à une recette de cuisine avec des étapes précises à suivre dans l'ordre.

Objet

Ce paradigme organise le code autour d'objets qui combinent données et comportements :

  • Les données sont regroupées dans des classes avec des attributs
  • Les comportements sont définis par des méthodes
  • On utilise des concepts comme l'encapsulation, l'héritage et le polymorphisme

On a déjà vu ça en détails dans le chapitre 1-03, je ne vais donc pas en dire plus ici.

Un programme écrit en orienté objet peut être comparé une entreprise avec différents services ayant chacun ses responsabilités et collaborant ensemble.

Fonctionnel

Ce paradigme, plus abstrait, s'inspire des mathématiques et se concentre sur les fonctions « pures » :

  • Il évite au maximum les effets de bord (modification de données en dehors de la fonction)
  • Les fonctions ne dépendent que de leurs paramètres d'entrée, pas de l'état global
  • Une même entrée produit toujours la même sortie
  • On privilégie l'immutabilité (on ne modifie pas les données, on crée de nouvelles valeurs)

C’est cette approche que nous allons détailler dans la suite de ce cours.

L’impératif est partout

Le paradigme impératif est omniprésent et fondamental dans la grande majorité des langages et des styles de programmation.

Même au sein d'autres paradigmes, l'impératif reste très présent :

  • Dans les fonctions (même en programmation fonctionnelle), l'implémentation interne de ces fonctions utilise souvent des séquences d'opérations, des affectations temporaires, des boucles ou des conditions. L'objectif est d'isoler ces aspects impératifs à l'intérieur de la fonction pour qu'elle apparaisse « pure » de l'extérieur.
  • Les méthodes d'objets manipulent les attributs de l'objet (ce qui est une forme d'affectation et donc d'impératif), effectuent des calculs séquentiels, des boucles, etc. L'approche objet structure le code et les données, mais les actions réalisées par les objets sont souvent de nature impérative.

La plupart des langages modernes (Python, Java, JavaScript, PHP, C#) sont multiparadigmes. Ils permettent d'écrire du code dans un style impératif, orienté objet, et de plus en plus, fonctionnel. Cependant, le socle de l'exécution reste souvent impératif. Les fonctionnalités des paradigmes fonctionnels ou objets sont construites sur cette base.

Programmation fonctionnelle

Prenons point par point les caractéristiques du paradigme fonctionnel et analysons-les :

  • Il évite au maximum les effets de bord (modification de données en dehors de la fonction)
  • Les fonctions ne dépendent que de leurs paramètres d'entrée, pas de l'état global
  • Une même entrée produit toujours la même sortie
  • On privilégie l'immutabilité (on ne modifie pas les données, on crée de nouvelles valeurs)

Les fonctions respectant ces exigences sont appelées des fonctions « pures »

Pas d’effets de bord

Soit le programme ci-dessous :


		def fct():
			global i
			i = i*2
		i = 5
		fct()
	

fct n’est pas une fonction pure, car ce qu’elle modifie une donnée extérieure à celle-ci. i est une varibale global qui se trouvera affectée par fct. C’est donc un effet de bord.

Si notre objectif était de doubler la valeur de i à l’aide d’une fonction, il faudrait écrire :


		def fct(i):
			return i*2
		i = 5
		i = fct(i)
	

Paramètres d’entrée

Soit le programme ci-dessous :


		def fct(): return i>5
		i = 5
		fct()
	

fct n’est pas une fonction pure, car ce qu’elle renvoie (True si i > 5 et False sinon) dépend de i. Et i n’est pas un paramètre d’entrée. C’est une variable global définie dans le scope du programme lui-même et donc exérieur à la fonction.

Pour la transformer en fonction pure, il faudrait écrire :


		def fct(i): return i>5
		i = 5
		fct(i)
	

Mêmes entrées = même sortie

Ce point est une conséquence du point précédent. La fonction ne dépend de rien d’autre que des paramètres d’entrée. Tant qu’on lui donne les mêmes paramètres, elle produira toujours le même résultat, sans dépendre de l’état d’une variable globale.

Immutabilité des données

Une fonction pure ne modifie pas les données d’entrée. Elle renvoie de nouvelles données. Par exemple :


		def fct(list, i):
			list.append(i)

		list = ["a", "b", "c"]
		fct(list, "d") # list a été modifiée
	

fct n’est pas une fonction pure, car elle a modifié l’entrée list. Pour en faire une fonction pure, il faut qu’elle renvoie une nouvelle liste.


		def fct(list, i):
			return list + [i]

		list = ["a", "b", "c"]
		list2 = fct(list, "d") # ["a", "b", "c", "d"]
	

On voit ici que list n’a pas été modifiée. À la place, une nouvelle variable list2 a été créée, avec les éléments de list et "d".

Outils de programmation fonctionnelle

Fonctions d’ordre supérieur

Une fonction d’ordre supérieur est une fonction qui peut prendre, parmi ses paramètres d’entrée, une autre fonction. On va voir dans la suite de cette section deux exemples très utiles en Python : les fonctions filter et map.

Il est bien sûr possible de créer des fonctions d’ordre supérieur. N’oublier pas qu’une fonction n’est après tout qu’un type d’« objet » parmi tant d’autres…


		def sum(a,b): return (a+b)
		def diff(a,b): return (a-b)

		def fos(f, a, b):
			return f(a, b)

		test1 = fos(sum, 2, 5) # test1 = 7
		test2 = fos(diff, 2, 5) # test2 = -3
	

Fonctions ayant un nombre variable d’arguments

Imaginez que vous ayez besoin d’écrire une fonction qui renvoie la somme de tous ses arguments, peu importe leur nombre. La fonction ci-dessous pose problème car elle attend exactement trois arguments.

def sum(a, b, c): return a+b+c

Vous pourriez avoir besoin de lui en passer deux, ou bien dix…. Bien sûr, vous avez la possibilité de passer un unique argument qui est une liste.


		def sum(list):
			total = 0
			for n in list:
				 total += n
			return total
	

Arguments de type *args

La solution précédente marche très bien et est tout à fait correct. Mais il y a une autre possibilité : passer à la fonction un argument précédé d’une astérisque. Cette notation traitera tous les arguments que vous passez à la fonction comme un tableau.


		def sum(*numbers):
			total = 0
			for n in numbers:  # n successivement prend les valeurs dans *numbers
				 total += n
			return total

		print(sum(4,4,8,4)) # affiche 20
	

Attention cependant : des arguments multiples de type *args doivent nécessairement être passés après d’autres arguments éventuels pour des raisons évidentes de logiques.


		def f(arg1, arg2, *args): ... # valide
		def g(arg1, *args, arg2): ... # non valide
	

Arguments de type **kwargs

Lorsqu’on met deux astérisques devant les arguments multiples, Python attend des arguments de type a=…, b=… etc.

On appelle cela des arguments nommés et ils seront traités comme un dictionnaire.


		def f(**kwargs): print(kwargs)

		f(a=1, b=2, nom="Toto") # affiche {'a': 1, 'b': 2, 'nom': 'Toto'}
	

Il est possible d’avoir une fonction prenant à la fois des arguments de type *args et des arguments de type **kwargs. En effet, Python peut faire la différence entre des arguments simples et des arguments nommés.

Fonction filter

Soit le programme ci-dessous :


		a = [ 1, 3, 5, 7 ]
		def fct(x): return x >= 5
		test = filter( fct, a )
		test = list(test) # conversion en list
		print(test) # affiche [5, 7]
	

On a passé à la fonction filter deux arguments : la fonction fct ainsi que a.filter filtre les éléments de a (qui doit être un itérable – liste ou dictionnaire) pour lesquels fct renvoie False et renvoie un filter object qui doit être converti en list.

Fonction map

Soit le programme ci-dessous :


		a = [ 1, 3, 5, 7 ]
		def fct(x): return x**2
		test = map( fct, a )
		test = list(test) # conversion en list
		print(test) # affiche [1, 9, 25, 49]
	

On a passé à la fonction map deux arguments : la fonction fct ainsi que a. map applique la fonction fct aux éléments de a (qui doit être un itérable) et renvoie un map object qui doit être converti en list.

Fonctions anonymes

Il peut arriver (comme dans les exemples ci-dessus) qu’on ait besoin d’une fonction simple et une seule fois. Pour ne pas polluer son code avec la définition de ce genre de fonction, on peut utiliser une fonction anonyme. La syntaxe est la suivante :


		lambda arguments : traitement des arguments (renvoie le résultat automatiquement)
		lambda a,b : a+b # renvoie la somme a+b
	

Ces fonctions sont parfois utilisées comme argument de fonction d’ordre supérieur. Reprenons les deux exemples donnés pour filter et map en utilisant des fonctions anonymes :


		liste = [ 1, 3, 5, 7 ]
		test1 = filter( lambda x : x>=5, liste )
		test2 = map( lambda x : x**2, liste )
	

L’écriture est plus concise.

Tri d’une liste de dictionnaire

En respectant le paradigme fonctionnel et en utilisant la fonction filter et une fonction anonyme, filtrer le tableau ci-dessous de manière à ne garder que les articles dont le prix est inférieur à 500 €.


			articles = [
				{ "name": "Ordinateur portable ZenAir", "prix": 1199, "ref": "48d5c19a1" },
				{ "name": "Écouteurs Airdaube", "prix": 349, "ref": "05ccf48b7" },
				{ "name": "Téléphone Samsonne", "prix": 699, "ref": "fc4804c2" },
				{ "name": "Tablette iPay Pro", "prix": 2499, "ref": "a73e21fc8" },
				{ "name": "Montre connectée iWatchU", "prix": 849, "ref": "2b67f4d3e" },
				{ "name": "Casque audio Bats Studio", "prix": 299, "ref": "9c24e8a5b" },
				{ "name": "Enceinte intelligente Alexoom", "prix": 129, "ref": "6f12d7c9a" },
				{ "name": "Télévision Somy", "prix": 899, "ref": "3e8b5f2d1" },
				{ "name": "Console GameStation Macrohard", "prix": 459, "ref": "7d4c09e8f" },
				{ "name": "Clavier Pov Pomme", "prix": 499, "ref": "5a3b2e7d1" },
				{ "name": "Station d'accueil iDock Pample", "prix": 149, "ref": "1c8e4a7b3" },
				{ "name": "Projecteur portable Booble Cast", "prix": 279, "ref": "8e1d5f3a2" }
			]