Dans l'exercice P2C1 du cours "Apprenez les bases du langage Python", j'ai semble t-il un problème de syntaxe avec la déclaration match-case. Je précise que je suis sur Python 3.12.2.
Voici le snippet en question :
match resultat:
case operation == "+":
resultat = nombre_1 + nombre_2
case operation == "-":
resultat = nombre_1 - nombre_2
case operation == "*":
resultat = nombre_1 * nombre_2
case operation == "/":
if nombre_2 != 0:
resultat = nombre_1 / nombre_2
else:
print("Erreur : division par zéro = Impossible")
exit()
case _:
print("Erreur : opérateur non reconnu")
exit()
Et voici le message d'erreur que le terminal me retourne :
case operation == "+":
SyntaxError: invalid syntax
Il semble que cela ait cassé Replit qui depuis tourne en boucle (coincé sur working) dès que je "Run".
Je précise enfin qu'en me servant des déclarations if/elif/else, je n'ai aucun problème mais je ne trouve pas la solution pour y parvenir avec match-case. Je ne trouve rien non plus pour vérifier si la syntaxe, dans ce cas précis, est bonne ou non (il semblerait que non de toute manière).
Si l'un de vous n'a serait-ce qu'un début de piste, n'hésitez pas !
Une autre solution serait d'utiliser le dictionnaire d'opérations (les clés seraient les opérateurs et les valeurs, les fonctions lambdas avec l'opération à calculer), par exemple
operations = {
"+": lambda x, y: x + y,
"-": lambda x, y: x - y,
"*": lambda x, y: x * y,
"/": lambda x, y: x / y if y != 0 else "Erreur : division par zéro = Impossible"
}
Celui qui trouve sans chercher est celui qui a longtemps cherché sans trouver.(Bachelard) La connaissance s'acquiert par l'expérience, tout le reste n'est que de l'information.(Einstein)
ok, ça lui permettra tout de même de voir une pratique équivalente en terme de fonctionnalité et meilleure en terme d'efficacité...
Je n'ai jamais utilisé match case sans doute parce-que je n'en vois pas l'intérêt !
- Edité par fred1599 3 mars 2024 à 2:48:17
Celui qui trouve sans chercher est celui qui a longtemps cherché sans trouver.(Bachelard) La connaissance s'acquiert par l'expérience, tout le reste n'est que de l'information.(Einstein)
L'utilisation d'un dictionnaire + lambda à son charme (notamment quand le dictionnaire est destiné à évoluer en cours d'exécution).
Mais en terme d'efficacité, ça reste à prouver. Dans le cas de cet exemple, avec un match-case, au pire il va y avoir simplement une cascade de 4 comparaisons (en moyenne 2). La recherche dans un dictionnaire, ca passe par un appel au hachage, au moins une comparaison, et derrière il y aura l'application d'une lambda, ce qui n'est pas gratuit. Un petit benchmark pour avoir des chiffres qui nous éclaireraient ?
Pour ce qui est de l' expressivité du code, avec le match on voit tout de suite l'intention, tester 4 valeurs + traitement anomalie, qu'il faut ajouter à coté si on utilise un dictionnaire.
Ps : match-case a été introduite dans python 3.10. Il est donc normal que les gens qui utilisent python depuis longtemps déclarent qu'on peut trés bien s'en passer (par un hack dont ils sont fiers et coutumiers), c'est ce qu'ils ont fait jusque-là. D'autres y verront un truc très important qui est la solution à tous les problèmes, surtout que ça leur permet de montrer aux vieux un truc qu'ils ne connaissent pas
Faut avouer que je m'attendais à mieux niveau efficacité, mais avec les dictionnaire on peut encore améliorer les temps...
La clarté, la maintenabilité et la flexibilité du code sont souvent des considérations plus importantes, surtout lorsque les différences de performance sont minimes. La méthode du dictionnaire offre une grande flexibilité et peut rendre le code plus modulaire et facile à étendre, ce qui peut être un avantage significatif dans de nombreux scénarios.
import timeit
def calculatrice_match(nombre_1, nombre_2, operation):
match operation:
case "+":
return nombre_1 + nombre_2
case "-":
return nombre_1 - nombre_2
case "*":
return nombre_1 * nombre_2
case "/":
if nombre_2 != 0:
return nombre_1 / nombre_2
else:
print("Erreur : division par zéro = Impossible")
exit()
case _:
print("Erreur : opérateur non reconnu")
exit()
def calculatrice_if_else(nombre_1, nombre_2, operation):
if operation == "+":
return nombre_1 + nombre_2
elif operation == "-":
return nombre_1 - nombre_2
elif operation == "*":
return nombre_1 * nombre_2
elif operation == "/":
if nombre_2 != 0:
return nombre_1 / nombre_2
else:
print("Erreur : division par zéro = Impossible")
exit()
else:
print("Erreur : opérateur non reconnu")
exit()
def calculatrice_dict(nombre_1, nombre_2, operation):
operations = {
"+": lambda x, y: x + y,
"-": lambda x, y: x - y,
"*": lambda x, y: x * y,
"/": lambda x, y: x / y if y != 0 else print("Erreur : division par zéro = Impossible")
}
func = operations.get(operation)
if func:
return func(nombre_1, nombre_2)
else:
print("Erreur : opérateur non reconnu")
exit()
# Utilisation directe des opérateurs pour les opérations courantes
def add(x, y): return x + y
def sub(x, y): return x - y
def mul(x, y): return x * y
def div(x, y): return x / y if y != 0 else "Erreur : division par zéro = Impossible"
def calculatrice_optimisee(nombre_1, nombre_2, operation):
func = operations.get(operation)
if func:
return func(nombre_1, nombre_2)
else:
print("Erreur : opérateur non reconnu")
exit()
operations = {
"+": add,
"-": sub,
"*": mul,
"/": div
}
nombre_1 = 10
nombre_2 = 5
operation = "+"
# Benchmark pour calculatrice_match
temps_match = timeit.timeit('calculatrice_match(nombre_1, nombre_2, operation)', globals=globals(), number=1000000)
# Benchmark pour calculatrice_if_else
temps_if_else = timeit.timeit('calculatrice_if_else(nombre_1, nombre_2, operation)', globals=globals(), number=1000000)
# Benchmark pour calculatrice_dict
temps_dict = timeit.timeit('calculatrice_dict(nombre_1, nombre_2, operation)', globals=globals(), number=1000000)
# Benchmark pour calculatrice_dict_optimise
temps_dict_optimise = timeit.timeit('calculatrice_optimisee(nombre_1, nombre_2, operation)', globals=globals(), number=1000000)
print(f"Temps d'exécution avec match: {temps_match} secondes")
print(f"Temps d'exécution avec if-else: {temps_if_else} secondes")
print(f"Temps d'exécution avec dictionnaire: {temps_dict} secondes")
print(f"Temps d'exécution avec dictionnaire optimise: {temps_dict_optimise} secondes")
Les résultats
Temps d'exécution avec match: 0.07410755900491495 secondes
Temps d'exécution avec if-else: 0.06876738999562804 secondes
Temps d'exécution avec dictionnaire: 0.3240111149934819 secondes
Temps d'exécution avec dictionnaire optimise: 0.12330839000060223 secondes
On remarque que match et if-else sont équivalents et mieux que mon dictionnaire pour 1000000 d'itérations, qui n'est évidemment pas représentatif du nombre d'itérations dont on aura besoin pour ce type de problème.
J'ai ajouté la solution d'un dictionnaire optimisé qui se rapproche de très près des deux solutions (match et if-else).
J'ai fais aussi un autre test, avec pypy qui malheureusement ne supporte pas match (version 3.8 pypy), mais comme if-else est équivalent à match, on peut comparer match de manière indirecte.
Temps d'exécution avec if-else: 0.0036038779944647104 secondes
Temps d'exécution avec dictionnaire: 0.06391517700103577 secondes
Temps d'exécution avec dictionnaire optimise: 0.007861538993893191 secondes
Là on voit que la différence est moindre entre dictionnaire optimise et if-else.
L'accès à un élément dans un dictionnaire nécessite de calculer le hash de la clé et de vérifier l'égalité, ce qui peut être plus coûteux en termes de performance par rapport à une suite de tests if-elif ou à l'utilisation de match qui sont optimisés pour des comparaisons directes de valeurs.
Celui qui trouve sans chercher est celui qui a longtemps cherché sans trouver.(Bachelard) La connaissance s'acquiert par l'expérience, tout le reste n'est que de l'information.(Einstein)
[Peux pas vous montrer le code et les résultats parce que le site est encore pété (*) pour firefox/linux sur pc, mais faut aussi voir ce que ça donne avec les 4 opérations. Edit: remarche.]
Pour if-then-else et match, les opérations sont comparées l'une après l'autre donc ça prend plus de temps pour - que pour + qui vient en premier, plus pour *, et encore plus pour /, à cause de la comparaison avec 0 qui s'y ajoute.
Bilan sans surprise : le temps ne varie guère avec les dictionnaires. Une collision sur 4 chaînes de longueur 1, ça serait quand même pas de bol, mais ça aurait pu arriver.
(*) ça va mieux après effacement de toutes les données en cache du navigateur. Une modif sur le site a dû l'empoisonner...
# operation +
avec match : 0.093 secondes
avec if-else : 0.077 secondes
avec dictionnaire : 0.527 secondes
avec dictionnaire optimise: 0.144 secondes
# operation -
avec match : 0.095 secondes
avec if-else : 0.088 secondes
avec dictionnaire : 0.520 secondes
avec dictionnaire optimise: 0.141 secondes
# operation *
avec match : 0.107 secondes
avec if-else : 0.104 secondes
avec dictionnaire : 0.516 secondes
avec dictionnaire optimise: 0.141 secondes
# operation /
avec match : 0.157 secondes
avec if-else : 0.135 secondes
avec dictionnaire : 0.538 secondes
avec dictionnaire optimise: 0.170 secondes
>>>
si j'étais courageux, j'aurais généré un CSV et fait des courbes avec gnuplot...
Donc on en déduis la même chose, merci pour la confirmation...
Celui qui trouve sans chercher est celui qui a longtemps cherché sans trouver.(Bachelard) La connaissance s'acquiert par l'expérience, tout le reste n'est que de l'information.(Einstein)
Ce qui me paraissait utile, c'est pas d'être en désaccord (et d'avoir raison, évidemment), c'est de chiffrer un peu pour ne pas rester sur du "on dit". Par exemple, on a maintenant des billes pour dire que l'application d'une lambda, ça coûte cher par rapport à l'utilisation d'une fonction définie (avec l'implémentation de l'interprète python qu'on a, précautions d'usage).
EDIT << en fait non (voir messages suivants). Ce qui coûte, c'est de construire une lambda chaque fois qu'on veut l'utiliser. Si on la met dans une variable qu'on réutilise, c'est pas pire que d'appeler une fonction. >>
Ma conclusion, c'est que match n'est pas spécialement intéressant (point de vue rapidité d'exécution) pour faire un bête "switch", pour lequel un gros if elif elif elif fait le boulot. Par contre ça indique mieux l'intention du code.
OK, c'est l'utilisation basique qui est montrée en général pour le présenter, mais match est destiné à des usages beaucoup plus sérieux que faire un aiguillage. Le pattern matching avec destructuration d'un objet est beaucoup plus utile. voir la doc https://docs.python.org/3/tutorial/controlflow.html
La sélection de cas par pattern matching, c'est pas jeune (fin des années 70 : Prolog, Miranda, programmation équationnelle etc) mais ça a fini par percoler dans les langages modernes, quasi un demi-siècle plus tard... Comme quoi faut pas désespérer.
Merci à tous pour vos réponses, c'était pas évident de tout comprendre mais j'ai saisi le principal.
Il y a donc plusieurs façons de faire, certaines étant plus optimisées que d'autres mais en effet, dans l'exercice, il m'était demander d'utiliser match-case.
Très intéressant en tout cas les benchs. Faut encore que je vois ce qu'est un dictionnaire optimisé...
En fait, c'est pas le dictionnaire qui est optimisé, mais ce qu'on met dedans (associé à +, par exemple) et qui sera appelé ensuite
soit une fonction définie par def
soit une lambda
Edit: en fait c'est pas ça, voir seconde moitié du message.
pour essayer de voir, j'ai écrit plusieurs manières de faire, plus ou moins directes
import timeit
def appel_direct(a, b):
return a + b
lambda_addition = lambda a, b : a + b
fonction_addition = appel_direct
def appel_lambda(a, b):
return lambda_addition(a, b)
def appel_fonction(a,b):
return fonction_addition(a, b)
def lambda_locale(a, b):
f = lambda a, b : a + b
return f(a, b)
def tester(noms_fonctions):
for fun in noms_fonctions:
temps = timeit.timeit(fun + "(123, 45)", globals = globals(), number = 10000000)
print(f"{fun:20s}: {temps:.3f}")
tester( ['appel_direct', 'lambda_addition', 'fonction_addition',
'appel_fonction', 'appel_lambda', 'lambda_locale'] )
Surprise, pas de différence notable entre l'appel d'une fonction ou d'une l'exécution directe. (3 premiers)
Les deux suivants montrent le délai induit par l'accès à une variable globale contenant la fonction ou la lambda.
La dernière (un peu hors sujet), c'est le temps de construction d'une lambda, si on le fait à chaque fois.
EDIT: pendant la sieste, il m'est venu à l'idée que la différence de performances dans le bench de @fred1599 venait précisément de là
sa version "optimisée" (avec fonctions) utilise un dictionnaire construit à l'avance dans une variable globale
par contre, et en revanche, la fonction calculatrice_dictconstruit un dictionnaire à chaque appel. Ce qui pénalise, forcément.
En mettant les deux sur un pied d'égalité
dict_lambda = {
"+": lambda x, y: x + y,
"-": lambda x, y: x - y,
"*": lambda x, y: x * y,
"/": lambda x, y: x / y if y != 0 else print("Erreur : division par zéro = Impossible")
}
def calculatrice_dict(nombre_1, nombre_2, operation):
func = dict_lambda.get(operation)
if func:
return func(nombre_1, nombre_2)
else:
print("Erreur : opérateur non reconnu")
exit()
# Utilisation directe des opérateurs pour les opérations courantes
def add(x, y): return x + y
def sub(x, y): return x - y
def mul(x, y): return x * y
def div(x, y): return x / y if y != 0 else "Erreur : division par zéro = Impossible"
dict_functions = {
"+": add,
"-": sub,
"*": mul,
"/": div
}
def calculatrice_optimisee(nombre_1, nombre_2, operation):
func = dict_functions.get(operation)
if func:
return func(nombre_1, nombre_2)
else:
print("Erreur : opérateur non reconnu")
exit()
les temps d'exécutions c'est kif-kif bourricot
# operation +
avec match : 0.113 secondes
avec if-else : 0.076 secondes
avec dict de lambdas : 0.147 secondes
avec dict de fonctions : 0.154 secondes
# operation -
avec match : 0.096 secondes
avec if-else : 0.091 secondes
avec dict de lambdas : 0.145 secondes
avec dict de fonctions : 0.150 secondes
# operation *
avec match : 0.114 secondes
avec if-else : 0.108 secondes
avec dict de lambdas : 0.144 secondes
avec dict de fonctions : 0.150 secondes
# operation /
avec match : 0.164 secondes
avec if-else : 0.140 secondes
avec dict de lambdas : 0.171 secondes
avec dict de fonctions : 0.178 secondes
>>>
Conclusion : on devrait faire la sieste plus souvent. Ce qui optimise, c'est de constituer le dictionnaire une fois pour toute dans une variable globale. Pas à chaque appel. Qu'on y mette des lambda ou des fonctions, ca change pas grand chose.
Une idée qui m'est venue au réveil : pour mieux voir la différence entre lambda et fonction (quand j'ai une idée quelque part...) j'ai écrit le petit code suivant
def my_function(a,b):
return a + b
my_lambda = lambda a, b : a + b
# def test(name, fun, a, b):
# r = fun(a,b)
# print(f'{name}({a},{b}) = {r}')
# test("fonction", my_function, 2, 3)
# test("lambda ", my_lambda , 4, 5)
pour voir ce que python3 générait comme bytecode pour les deux, maintenant que je sais faire.
python3 -m dis python-lambda.py
On ne peut pas dire que ça soit très différent !
0 0 RESUME 0
#
# fabrication de la fonction avec le code qui est plus loin, et rangement dans la variable my_function
1 2 LOAD_CONST 0 (<code object my_function at 0x7f49b0365960, file "python-lambda.py", line 1>)
4 MAKE_FUNCTION 0
6 STORE_NAME 0 (my_function)
#
# même chose pour la variable my_lambda
4 8 LOAD_CONST 1 (<code object <lambda> at 0x7f49b0365a30, file "python-lambda.py", line 4>)
10 MAKE_FUNCTION 0
12 STORE_NAME 1 (my_lambda)
#
# l'évaluation du module retourne None je présume
14 LOAD_CONST 2 (None)
16 RETURN_VALUE
# ----------------------------------------------------------
# le code de la fonction
Disassembly of <code object my_function at 0x7f49b0365960, file "python-lambda.py", line 1>:
1 0 RESUME 0
2 2 LOAD_FAST 0 (a)
4 LOAD_FAST 1 (b)
6 BINARY_OP 0 (+)
10 RETURN_VALUE
# le code de la lambda
Disassembly of <code object <lambda> at 0x7f49b0365a30, file "python-lambda.py", line 4>:
4 0 RESUME 0
2 LOAD_FAST 0 (a)
4 LOAD_FAST 1 (b)
6 BINARY_OP 0 (+)
10 RETURN_VALUE
donc a priori aucune raison que l'un aille plus vite que l'autre.
- Edité par michelbillaud 7 mars 2024 à 10:06:05
Erreur de syntaxe match-case Exercice P2C1
× Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
Le Tout est souvent plus grand que la somme de ses parties.
Celui qui trouve sans chercher est celui qui a longtemps cherché sans trouver.(Bachelard)
La connaissance s'acquiert par l'expérience, tout le reste n'est que de l'information.(Einstein)
Le Tout est souvent plus grand que la somme de ses parties.
Celui qui trouve sans chercher est celui qui a longtemps cherché sans trouver.(Bachelard)
La connaissance s'acquiert par l'expérience, tout le reste n'est que de l'information.(Einstein)
Celui qui trouve sans chercher est celui qui a longtemps cherché sans trouver.(Bachelard)
La connaissance s'acquiert par l'expérience, tout le reste n'est que de l'information.(Einstein)
Celui qui trouve sans chercher est celui qui a longtemps cherché sans trouver.(Bachelard)
La connaissance s'acquiert par l'expérience, tout le reste n'est que de l'information.(Einstein)