Leçon 02-2 — Opérateurs logiques : combiner des conditions

1. Objectif

À la fin de cette leçon tu sauras : combiner plusieurs conditions avec and, or et not ; prédire le résultat d'une expression logique grâce aux tables de vérité et aux règles de priorité ; expliquer ce qu'est une valeur « truthy »/« falsy » ; et repérer au premier coup d'œil le piège if x == 1 or 2: — l'erreur de logique la plus célèbre du niveau.

2. Pourquoi c'est important

Les conditions réelles sont presque toujours composées : « majeur ET muni d'un billet », « samedi OU dimanche », « PAS déjà inscrit ». Sans and/or/not, tu empiles des if imbriqués illisibles (tu l'as senti dans l'exercice 9-c de la leçon 02-1). Et c'est dans les combinaisons que naissent les bugs de logique : une condition toujours vraie ne produit aucune erreur, juste un programme qui fait n'importe quoi. Savoir lire une expression logique comme Python la lit, c'est la moitié du débogage du niveau 2.

3. Explication simple

Trois mots-clés, en anglais courant dans le code :

age = 25
income = 30000

print(age >= 21 and income >= 20000)   # True  : les DEUX sont vraies
print(age < 18 or income >= 20000)     # True  : au moins UNE est vraie
print(not age < 18)                    # True  : inverse la condition

Les tables de vérité complètes (à connaître, elles sont courtes) :

aba and ba or b
TrueTrueTrueTrue
TrueFalseFalseTrue
FalseTrueFalseTrue
FalseFalseFalseFalse

Et not : not TrueFalse, not FalseTrue.

Moyens mnémotechniques : and est exigeant (tout doit être vrai) ; or est facile à contenter (une seule suffit — et le or de Python est inclusif : les deux vraies, c'est vrai aussi) ; not retourne la crêpe.

4. Explication approfondie

a) Priorité des opérateurs. Comme * passe avant + en maths, il y a un ordre : not d'abord, puis and, puis or. Et les comparaisons (==, <…) sont évaluées avant tout ça.

print(True or False and False)     # True  : and d'abord → True or False → True
print((True or False) and False)   # False : les parenthèses changent tout
print(not True and False)          # False : not True → False, puis False and False
print(not (True and False))        # True  : d'abord la parenthèse

Règle de vie : dès qu'une expression mélange and et or, mets des parenthèses, même quand elles sont facultatives. Ton lecteur (toi dans un mois) ne connaît pas les priorités par cœur.

b) Truthy / falsy : tout a une valeur de vérité. Dans un contexte de condition, Python accepte n'importe quelle valeur, pas seulement True et False, et la convertit avec bool() :

print(bool(0), bool(1), bool(-1), bool(0.0))    # False True True False
print(bool(""), bool("0"), bool(" "), bool("False"))  # False True True True

Retiens la règle courte : sont falsy le zéro (0, 0.0), la chaîne vide "" et False lui-même (⏩ au niveau 3 s'ajouteront la liste vide et None). Tout le reste est truthy — y compris "0", " " (une espace) et même la chaîne "False" : une chaîne non vide est vraie, peu importe son contenu. D'où l'idiome courant if name: pour dire « si l'utilisateur a tapé quelque chose ».

c) LE piège du niveau : if x == 1 or 2:. En français on dit « si x vaut 1 ou 2 » ; en Python ça se découpe autrement :

x = 5
print(x == 1 or 2)     # affiche : 2   (truthy → le if serait TOUJOURS pris)
print(x == 1 or x == 2)  # False     ← la version correcte

Python lit (x == 1) or (2) : la comparaison vaut False, donc or renvoie son deuxième opérande, 2 — qui est truthy. La condition est vraie pour n'importe quel x, sans le moindre message d'erreur. Chaque valeur a droit à sa comparaison complète : x == 1 or x == 2.

(Tu remarques que or a renvoyé 2, pas True : and/or renvoient l'un de leurs opérandes, pas forcément un bool. C'est pour ça que print(0 or "default") affiche default — idiome que tu recroiseras dans du vrai code. Pour l'instant, sache-le sans t'en servir.)

d) L'évaluation court-circuit. Python s'arrête dès que le résultat est connu : dans a and b, si a est faux, b n'est jamais évalué ; dans a or b, si a est vrai, pareil. Ce n'est pas qu'une optimisation — c'est un outil de protection :

n = 0
if n != 0 and 100 / n > 2:    # la division n'est JAMAIS tentée si n vaut 0
    print("grand")
else:
    print("nul ou petit")

Sortie : nul ou petit. Inverse l'ordre (100 / n > 2 and n != 0) et le programme plante sur ZeroDivisionError: division by zero. L'ordre des opérandes d'un and peut donc être vital : le garde-fou d'abord.

Difficulté honnête : la mécanique (and/or/not) s'apprend vite. Ce qui demande de la pratique : penser comme Python découpe (x == 1 or 2), et les valeurs truthy contre-intuitives ("0" est vraie, "False" est vraie). Prévois une session pour les sections 1–7 et une pour les exercices — et accepte de te faire piéger une fois ou deux, c'est le passage obligé.

5. Exemples commentés

Exemple 1 — and : plusieurs exigences à la fois

age = 25
income = 30000
if age >= 21 and income >= 20000:
    print("Pret accorde")
else:
    print("Pret refuse")

Sortie : Pret accorde. Les deux conditions sont vérifiées indépendamment, chacune complète (variable + opérateur + valeur).

Exemple 2 — or correct : chaque option a sa comparaison

day = 6
if day == 6 or day == 7:
    print("Week-end !")
else:
    print("Semaine")

Sortie : Week-end !. Compare avec la version piégée de la section 4-c.

Exemple 3 — not et la lisibilité

is_raining = False
if not is_raining:
    print("On sort !")

Sortie : On sort !. not brille avec des variables bool bien nommées. Mais préfère if x != 5: à if not x == 5: — même sens, plus direct.

Exemple 4 — l'intervalle, trois écritures équivalentes

n = 7
print(1 < n and n < 10)   # True — la version explicite
print(1 < n < 10)         # True — le chaînage (vu en 02-1), le plus lisible

Exemple 5 — truthy en pratique : la saisie vide

name = input("Ton prenom ? ")
if name:                          # truthy : vrai si la chaîne n'est PAS vide
    print(f"Bonjour {name} !")
else:
    print("Tu n'as rien tape.")

Session (l'utilisateur appuie juste sur Entrée) :

Ton prenom ?
Tu n'as rien tape.

Exemple 6 — le court-circuit qui protège une division (celui de la section 4-d — tape-le, puis inverse l'ordre des opérandes pour voir le ZeroDivisionError de tes propres yeux.)

6. Erreurs fréquentes

1) if x == 1 or 2: — la condition toujours vraie

choice = input("Ton choix (1 ou 2) ? ")
if choice == "1" or "2":
    print("Choix valide")     # affiché même si l'utilisateur tape "9" !

Aucune erreur, le menu accepte tout. Python lit (choice == "1") or ("2"), et "2" est truthy. Correction : if choice == "1" or choice == "2":. Relis chaque or/and en vérifiant que chaque côté est une condition complète.

2) Inverser la priorité and/or de tête

# voulu : week-end OU (semaine et férié)
day = 2
holiday = False
if day == 6 or day == 7 and holiday:
    print("Repos")

Ici ça « marche » (pas de repos un mardi normal)… mais par chance : and étant prioritaire, la condition se lit day == 6 or (day == 7 and holiday) — un samedi sans férié passe, ce qui était voulu, alors qu'on aurait juré avoir écrit (day == 6 or day == 7) and holiday. Les deux lectures divergent selon les valeurs : bug intermittent, le pire genre. Correction : parenthèses, systématiquement, dès que les deux opérateurs cohabitent.

3) Croire que "0" ou "False" sont faux

answer = input("Continuer ? (tape 0 pour arreter) ")
if answer:
    print("On continue !")    # affiché MÊME si l'utilisateur tape 0

answer vaut "0" — une chaîne non vide, donc truthy. Le test truthy vérifie « vide ou pas », rien d'autre. Pour tester le contenu : if answer != "0":.

4) Écrire && ou || comme dans d'autres langages

x = 5
y = 10
if x > 0 && y > 0:
    print("both positive")
# SyntaxError: invalid syntax

En Python c'est and, or, not — des mots, pas des symboles. (Le message ne suggère rien ici : à toi de reconnaître le réflexe venu de JavaScript/C.)

5) Mettre le garde-fou du mauvais côté du and

n = 0
if 100 / n > 2 and n != 0:
    print("grand")
# ZeroDivisionError: division by zero

Le court-circuit évalue de gauche à droite : la division explose avant que n != 0 ne soit consulté. Correction : if n != 0 and 100 / n > 2:.

7. Lire les messages d'erreur

La spécialité de cette leçon est justement l'absence de message : une condition toujours vraie ou toujours fausse s'exécute sans broncher (erreurs 1 à 3 ci-dessus). Quand un if prend systématiquement la même branche, le réflexe est print() sur la condition — méthode complète en leçon 02-5.

Les deux erreurs bruyantes possibles :

SyntaxError — le && importé d'un autre langage

  File "c:\Users\jtron\Claude\learnpython\logic.py", line 3
    if x > 0 && y > 0:
              ^
SyntaxError: invalid syntax

Pas de Traceback : rien n'a été exécuté. Le ^ pointe le premier & — quand un SyntaxError ne suggère rien, regarde le caractère pointé et demande-toi « est-ce du Python, ça ? ».

ZeroDivisionError — le court-circuit dans le mauvais sens

Traceback (most recent call last):
  File "c:\Users\jtron\Claude\learnpython\lessons\level-02-conditions-boucles\exercices\mes-reponses\ex-02-2-e.py", line 2, in <module>
    if 100 / n > 2 and n != 0:
       ~~~~^~~
ZeroDivisionError: division by zero
  1. Dernière ligne : diviser par zéro est mathématiquement indéfini, Python refuse. Le ~~~~^~~ souligne précisément 100 / n.
  2. La présence du and n != 0 DANS la ligne fautive est l'indice-clé : le garde-fou existe, mais à droite, donc trop tard. Déplace-le à gauche.

8. Exercices — faciles

Règles : nouveau fichier par exercice dans lessons/level-02-conditions-boucles/exercices/mes-reponses/, nommé ex-02-2-a.py, etc. Tape tout à la main. Prédis la sortie AVANT d'exécuter.

a) Sans exécuter, prédis les quatre sorties (attention aux priorités), puis vérifie :

print(True and False)
print(not False)
print(True or False and False)
print(not True or True)

b) Sans exécuter, prédis les six sorties, puis vérifie et note dans ton journal celles qui t'ont surpris :

print(bool(""))
print(bool("False"))
print(bool(0))
print(bool(0.0))
print(bool(-5))
print(bool(" "))

c) Demande une température (en °C, float) et affiche Baignade ! si elle est entre 24 et 32 inclus, sinon Trop froid en dessous et Trop chaud au-dessus. Écris la condition de baignade UNE fois avec and, une fois en chaînage 24 <= t <= 32 — vérifie que les deux versions réagissent pareil aux bornes exactes (24, 32).

9. Exercices — moyens

a) Ce menu accepte n'importe quoi. Explique le bug sans exécuter, prédis ce qu'affiche une saisie 9, puis corrige :

choice = input("Ton choix (1 ou 2) ? ")
if choice == "1" or "2":
    print("Choix valide")
else:
    print("Choix invalide")

b) Année bissextile : demande une année et affiche si elle est bissextile. Règle exacte : divisible par 4 et pas par 100 — ou divisible par 400. Contraintes : % pour la divisibilité, parenthèses obligatoires, et vérifie tes quatre cas de contrôle : 2024 (oui), 2023 (non), 1900 (non !), 2000 (oui).

c) Connexion : demande un identifiant puis un mot de passe. Accès accordé si (user == "admin" et password == "s3cret") ou si user == "invite" (peu importe le mot de passe). Une seule chaîne if/else, parenthèses explicites. Teste les quatre combinaisons : admin/bon, admin/mauvais, invite/n'importe quoi, inconnu/n'importe quoi.

10. Exercices — difficiles

a) Lois de De Morgan. Le programme ci-dessous doit afficher quatre lignes True (une par combinaison de a et b) prouvant que not (a and b) équivaut à not a or not b :

# pour chaque combinaison, afficher : not (a and b) == (not a or not b)

Écris-le (quatre blocs de deux assignations + un print, ou mieux si tu vois comment ⏩ les boucles arrivent en 02-3/02-4). Puis fais pareil pour la deuxième loi : not (a or b) équivaut à not a and not b. Enfin, utilise-la pour réécrire SANS not global : not (age >= 18 and city == "Nice").

11. Mini-projet lié — « contrôle d'accès du club »

Écris door_policy.py : le programme pose trois questions —

Age ? 17
Membre du club ? (oui/non) non
Accompagne d'un adulte ? (oui/non) oui

— puis applique la politique d'entrée : entrée accordée si la personne a 18 ans ou plus ou si elle est accompagnée d'un adulte ; MAIS un membre du club entre dans tous les cas ; ET en dessous de 12 ans, refus absolu, membre ou pas. Affiche ACCES ACCORDE ou ACCES REFUSE, plus une ligne expliquant la raison.

Contraintes : les réponses oui/non sont comparées à "oui" (is_member = answer == "oui" te donne un vrai bool) ; la décision finale tient dans UNE expression logique parenthésée, stockée dans une variable allowed avant le if. Écris d'abord la politique en français sur papier, traduis ensuite. Teste au minimum : 17/non/oui, 17/oui/non, 11/oui/oui, 25/non/non. Durée cible : 30–40 min.

12. Correction / méthode de correction

Les solutions détaillées sont dans solutions/serie-02-2-solutions.md.

Méthode : (1) finis TOUS les exercices avant d'ouvrir les solutions ; (2) compare le raisonnement, pas seulement le code ; (3) pour chaque écart, une ligne de journal : « je pensais X, en fait Y » ; (4) refais de tête l'exercice raté, le lendemain.

13. À retenir

14. Questions de révision

  1. Récite les tables de vérité de and et or. Le or de Python est-il exclusif ou inclusif ?
  2. Que vaut True or False and False, et pourquoi ? Et avec des parenthèses autour du or ?
  3. Pourquoi if x == 1 or 2: est-il toujours vrai ? Comment Python découpe-t-il exactement cette expression ?
  4. Cite les quatre valeurs falsy vues dans cette leçon. "0" est-il truthy ou falsy ? Et " " ?
  5. Qu'est-ce que l'évaluation court-circuit ? Donne un cas où l'ordre des opérandes d'un and évite un plantage.
  6. Pourquoi bool("False") vaut-il True ?
  7. Comment réécrire not (a or b) sans parenthèses (loi de De Morgan) ?
  8. Les bugs typiques de cette leçon produisent-ils des tracebacks ? Comment les détecte-t-on alors ?

15. Checklist de compréhension

Si une case reste vide : refais les exercices de la section correspondante demain, AVANT d'ouvrir la leçon suivante.

16. Commit conseillé

git add lessons/level-02-conditions-boucles/exercices/mes-reponses/
git commit -m "exercises: complete lesson 02-2 logical operators"

Puis, si c'est ta fin de session : entrée de journal + git push.