25  Logique booléenne approfondie

Les conditions if/else et les opérateurs and/or/not semblent simples. En apparence seulement. Ce chapitre explore les subtilités que le TOSA teste massivement : court-circuit, idiomes pythoniques (le or de valeur par défaut), clause else des boucles, et rappels de priorité d’opérateurs.

25.1 Différence entre is et == (rappel)

Vu au chapitre précédent, on en fait un résumé pour ce chapitre :

  • == teste si les valeurs sont égales.
  • is teste si les deux noms pointent vers le même objet en mémoire.
a = [1, 2, 3]
b = [1, 2, 3]
c = a

print("a == b :", a == b)    # True : même contenu
print("a is b :", a is b)    # False : objets différents

print("a is c :", a is c)    # True : alias
a == b : True
a is b : False
a is c : True

Règle pratique :

  • is avec None, True, False.
  • == pour comparer des valeurs.

25.2 La logique booléenne avancée

Le court-circuit

Principe : Python évalue les opérateurs logiques de gauche à droite et s’arrête dès qu’il peut conclure.

  • A and B : si A est faux → ne pas évaluer B, retourner A.
  • A or B : si A est vrai → ne pas évaluer B, retourner A.

Démonstration avec une fonction qui affiche quand elle est appelée :

def verifier(x):
    print(f"  → vérification de {x}")
    return x > 0

print("Test `and` avec échec à gauche :")
resultat = verifier(-5) and verifier(10)    # verifier(10) pas exécuté !
print("Résultat :", resultat)
Test `and` avec échec à gauche :
  → vérification de -5
Résultat : False
def verifier(x):
    print(f"  → vérification de {x}")
    return x > 0

print("\nTest `or` avec succès à gauche :")
resultat = verifier(5) or verifier(10)      # verifier(10) pas exécuté !
print("Résultat :", resultat)

Test `or` avec succès à gauche :
  → vérification de 5
Résultat : True

Application concrète : éviter une division par zéro.

def est_valide(x):
    return x != 0 and 10 / x > 2     # la 2e partie NE S'ÉVALUE PAS si x == 0

print(est_valide(0))      # False, pas de ZeroDivisionError
print(est_valide(3))      # True
print(est_valide(10))     # False (10/10 = 1 < 2)
False
True
False
⚠️ and/or renvoient l’opérande, pas un booléen

Détail très testé au TOSA : and et or ne renvoient pas forcément True/False, mais l’une des deux opérandes.

  • A and B : renvoie A si A est falsy, sinon B.
  • A or B : renvoie A si A est truthy, sinon B.
# and : renvoie le premier falsy, sinon le dernier
print(0 and 5)           # 0
print(3 and 5)           # 5
print("" and "oui")      # ""

# or : renvoie le premier truthy, sinon le dernier
print(0 or 5)            # 5
print(3 or 5)            # 3
print("" or "défaut")    # "défaut"
print(None or 0 or "")   # "" (tous falsy, renvoie le dernier)
0
5

5
3
défaut

Idiome courant : valeur par défaut avec or

Une conséquence pratique de ce comportement : on peut utiliser or pour fournir une valeur par défaut quand une variable peut être vide ou None.

def saluer(nom):
    nom = nom or "inconnu"       # si nom est "", None, 0... → "inconnu"
    print(f"Bonjour {nom}")

saluer("Alice")
saluer("")
saluer(None)
Bonjour Alice
Bonjour inconnu
Bonjour inconnu

C’est un idiome classique en Python. Équivalent plus explicite :

if not nom:
    nom = "inconnu"
⚠️ Attention : or traite tout falsy comme défaut

Cela signifie que 0 ou False déclenchent aussi le défaut :

quantite = 0
quantite = quantite or 1    # quantite vaut maintenant 1 !
print(quantite)
1

Si vous voulez seulement remplacer None, utilisez un test explicite :

if quantite is None:
    quantite = 1

Les valeurs falsy et truthy (rappel)

Python évalue n’importe quelle valeur comme booléen. Voici la liste des valeurs falsy :

Type Valeur(s) falsy
Nombres 0, 0.0, 0j
Chaîne "" (vide)
Collection [], (), {}, set()
Spécial None, False

Tout le reste est truthy.

for valeur in [0, "", [], {}, None, False, 1, "a", [0], "False"]:
    print(f"{str(valeur):<12}{bool(valeur)}")
0            → False
             → False
[]           → False
{}           → False
None         → False
False        → False
1            → True
a            → True
[0]          → True
False        → True

25.3 L’opérateur ternaire

Déjà vu. Syntaxe : valeur_si_vrai if condition else valeur_si_faux.

age = 17
statut = "majeur" if age >= 18 else "mineur"
print(statut)
mineur

Équivalent à :

if age >= 18:
    statut = "majeur"
else:
    statut = "mineur"

Enchaîner des ternaires (à utiliser avec modération)

On peut imbriquer des ternaires, mais la lisibilité en souffre :

note = 14

# Imbriqué (lisible ici)
mention = "Bien" if note >= 14 else "Passable" if note >= 10 else "Insuffisant"
print(mention)
Bien

Version équivalente avec if/elifplus lisible pour plus de 2 cas :

note = 14

if note >= 14:
    mention = "Bien"
elif note >= 10:
    mention = "Passable"
else:
    mention = "Insuffisant"

print(mention)
Bien
Règle pratique
  • 2 cas : ternaire OK.
  • 3 cas ou plus : préférez if/elif/else.

25.4 Les instructions break et continue (rappel approfondi)

Déjà vues en Partie 1. Approfondissement sur des cas subtils.

break dans une boucle imbriquée

break sort uniquement de la boucle la plus interne.

for i in range(3):
    for j in range(3):
        if j == 1:
            break           # sort seulement du for interne
        print(f"({i}, {j})")
(0, 0)
(1, 0)
(2, 0)

Pour sortir de toutes les boucles, il faut utiliser une variable témoin, une fonction avec return, ou lever une exception (vu en Partie 3).

continue pour filtrer proprement

# Afficher les nombres de 1 à 10, sauf les multiples de 3
for n in range(1, 11):
    if n % 3 == 0:
        continue
    print(n, end=" ")
1 2 4 5 7 8 10 

25.5 La clause else sur les boucles

Spécificité Python : for et while peuvent avoir une clause else, qui s’exécute si la boucle se termine normalement (sans break).

Exemple typique : recherche dans une liste

nombres = [3, 7, 12, 25, 8, 14]
cible = 25

for n in nombres:
    if n == cible:
        print("Trouvé !")
        break
else:
    print("Pas trouvé")
Trouvé !
nombres = [3, 7, 12, 25, 8, 14]
cible = 99

for n in nombres:
    if n == cible:
        print("Trouvé !")
        break
else:
    print("Pas trouvé")
Pas trouvé

Application : nombre premier

n = 17

for i in range(2, n):
    if n % i == 0:
        print(f"{n} n'est pas premier (divisible par {i})")
        break
else:
    print(f"{n} est premier")
17 est premier
Un else mal nommé

La clause else des boucles est souvent considérée comme un des choix syntaxiques les plus malheureux de Python : son nom suggère « sinon », mais son sens est « si la boucle s’est terminée sans break ». Mieux vaudrait la lire comme no_break:.

Le TOSA teste son existence et son comportement. En pratique, beaucoup de développeurs Python l’évitent au profit de fonctions avec return plus explicites.

25.6 Versions de Python

Petit rappel utile pour le TOSA : les versions de Python apportent régulièrement de nouvelles fonctionnalités. Les plus importantes pour le niveau Opérationnel :

Version Nouveautés clés
Python 3.6 (2016) f-strings
Python 3.7 (2018) dict conserve l’ordre d’insertion
Python 3.8 (2019) opérateur walrus :=, f"{x=}"
Python 3.10 (2021) match/case, int \| str, zip(strict=True)
Python 3.12 (2023) messages d’erreur encore plus précis

Le TOSA utilise Python 3.10 — c’est la version de référence du référentiel Isograd 2024-2025.

Pour connaître votre version :

import sys
print(sys.version)

25.7 Rappel : priorité des opérateurs

Quand on combine plusieurs opérateurs sans parenthèses, Python suit un ordre strict. Voici les règles les plus importantes du plus prioritaire au moins prioritaire :

Niveau Opérateurs
1 ** (puissance)
2 +x, -x (unaires), ~x
3 *, /, //, %
4 +, - (binaires)
5 Comparaisons : <, <=, >, >=, ==, !=, in, is
6 not
7 and
8 or

Points à retenir

1. Les comparaisons sont prioritaires sur les booléens.

x, y = 5, 10
# age >= 18 and age < 65 se lit (age >= 18) and (age < 65), OK
print(x > 0 and y > 0)        # True and True = True
True

2. not est prioritaire sur and et or.

a = False
b = True
print(not a and b)            # (not a) and b = True and True = True
True

3. and est prioritaire sur or.

# True or False and False se lit True or (False and False) = True or False = True
print(True or False and False)
True
En cas de doute : mettez des parenthèses

Elles ne coûtent rien et rendent le code lisible pour vous et vos relecteurs :

# ❌ Lecture difficile
if a and b or c and not d:
    ...

# ✅ Intention claire
if (a and b) or (c and not d):
    ...

🧩 Quiz 6.1 — Logique avancée

Question 1

Que renvoie 0 or "défaut" ?

  1. 0
  2. True
  3. "défaut"
  4. Une erreur

c) "défaut"or renvoie le premier opérande truthy (ou le dernier si tous falsy). Ici 0 est falsy, donc on passe au suivant.

print(0 or "défaut")
défaut

Question 2

Que renvoie 3 and 5 ?

  1. True
  2. 3
  3. 5
  4. 8

c) 5and renvoie le premier opérande falsy (ou le dernier si tous truthy). Ici 3 est truthy, on passe au suivant (5) qui est aussi truthy → c’est le dernier, on le renvoie.

print(3 and 5)        # 5
print(0 and 5)        # 0 (premier falsy)
5
0

Question 3

Que s’affiche-t-il ?

for n in [1, 2, 3]:
    if n == 10:
        break
else:
    print("terminé")
  1. Rien
  2. terminé
  3. break
  4. Une erreur

b) terminé — la boucle se termine sans rencontrer de break, donc le else s’exécute.

for n in [1, 2, 3]:
    if n == 10:
        break
else:
    print("terminé")
terminé

Question 4

Que renvoie True or False and False ?

  1. True
  2. False
  3. Une erreur

a) Trueand est prioritaire sur or. L’expression s’évalue comme True or (False and False) = True or False = True.

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

Question 5

Que s’affiche-t-il ?

nom = ""
nom = nom or "inconnu"
print(nom)
  1. ""
  2. inconnu
  3. None
  4. Une erreur

b) inconnu — idiome Python classique : nom or "défaut" renvoie "défaut" si nom est falsy (ici, chaîne vide).

nom = ""
nom = nom or "inconnu"
print(nom)
inconnu

Question 6

Que renvoie ce code ?

def f(x):
    print(f"f({x}) appelé")
    return x > 0

result = f(5) or f(10)
  1. f(5) appelé puis f(10) appelé, résultat True
  2. f(5) appelé seulement, résultat True
  3. f(10) appelé seulement, résultat True
  4. Une erreur

b)or est court-circuité : comme f(5) renvoie True, f(10) n’est pas appelé.

def f(x):
    print(f"f({x}) appelé")
    return x > 0

result = f(5) or f(10)
print("Résultat :", result)
f(5) appelé
Résultat : True

Question 7

Que renvoie not True == False ?

  1. True
  2. False
  3. Une erreur

a) True — piège de priorité ! == est plus prioritaire que not. L’expression s’évalue comme not (True == False) = not False = True.

print(not True == False)
print(not (True == False))
print((not True) == False)
True
True
True

Les trois valent True ici, mais la première suit la règle not(True==False).


✏️ Exercice 6.1 — Implémenter all() à la main

Sans utiliser all(), écrivez une fonction tous_positifs(liste) qui renvoie True si tous les éléments de la liste sont > 0, False sinon.

Indice : utilisez break pour sortir dès qu’on trouve un contre-exemple. Bonus : utilisez la clause else de boucle.

def tous_positifs(liste):
    ...

print(tous_positifs([1, 2, 3, 4]))
print(tous_positifs([1, -2, 3]))
print(tous_positifs([]))
def tous_positifs(liste):
    for n in liste:
        if n <= 0:
            return False
    return True

print(tous_positifs([1, 2, 3, 4]))      # True
print(tous_positifs([1, -2, 3]))        # False
print(tous_positifs([]))                # True (liste vide)
True
False
True
def tous_positifs(liste):
    for n in liste:
        if n <= 0:
            break
    else:
        return True
    return False

print(tous_positifs([1, 2, 3]))
print(tous_positifs([1, -2, 3]))
print(tous_positifs([]))
True
False
True
def tous_positifs_natif(liste):
    return all(n > 0 for n in liste)

print(tous_positifs_natif([1, 2, 3]))
print(tous_positifs_natif([1, -2, 3]))
print(tous_positifs_natif([]))
True
False
True

✏️ Exercice 6.2 — Test d’appartenance sensible aux types

Écrivez une fonction contient_strict(liste, valeur) qui renvoie True uniquement si la liste contient exactement la même valeur et du même type.

Par exemple : contient_strict([1, 2, 3], 2)True, mais contient_strict([1, 2, 3], 2.0)False (bien que 2 == 2.0).

Indice : utilisez à la fois == et type() ou is avec un test explicite.

def contient_strict(liste, valeur):
    ...

print(contient_strict([1, 2, 3], 2))         # True
print(contient_strict([1, 2, 3], 2.0))       # False
print(contient_strict([1, 2.0, 3], 2.0))     # True
print(contient_strict([1, 2, 3], "2"))       # False
def contient_strict(liste, valeur):
    for x in liste:
        if x == valeur and type(x) == type(valeur):
            return True
    return False

print(contient_strict([1, 2, 3], 2))         # True
print(contient_strict([1, 2, 3], 2.0))       # False
print(contient_strict([1, 2.0, 3], 2.0))     # True
print(contient_strict([1, 2, 3], "2"))       # False
True
False
True
False

L’opérateur in utilise ==, donc il trouverait 2.0 dans [1, 2, 3] car 2 == 2.0 est True :

print(2.0 in [1, 2, 3])       # True (via ==)
print(True in [1, 2, 3])      # True aussi ! (True == 1)
True
True

La fonction contient_strict ajoute le test de type pour différencier.


✏️ Exercice 6.3 — Validation avec court-circuit

Écrivez une fonction acces_autorise(utilisateur, role_requis, actif) qui renvoie True si :

  1. utilisateur n’est pas None.
  2. utilisateur a le role_requis.
  3. actif est True.

Tirez parti du court-circuit de and pour ne pas accéder à utilisateur.get(...) si utilisateur est None.

def acces_autorise(utilisateur, role_requis, actif):
    ...

# Exemples d'appel
u1 = {"nom": "Alice", "role": "admin"}
u2 = {"nom": "Bob",   "role": "user"}

print(acces_autorise(u1, "admin", True))
print(acces_autorise(u2, "admin", True))
print(acces_autorise(None, "admin", True))
def acces_autorise(utilisateur, role_requis, actif):
    return utilisateur is not None and utilisateur.get("role") == role_requis and actif

u1 = {"nom": "Alice", "role": "admin"}
u2 = {"nom": "Bob",   "role": "user"}

print(acces_autorise(u1, "admin", True))      # True
print(acces_autorise(u2, "admin", True))      # False
print(acces_autorise(None, "admin", True))    # False (pas d'erreur !)
print(acces_autorise(u1, "admin", False))     # False
True
False
False
False

Si on avait inversé l’ordre :

def acces_autorise_bugué(utilisateur, role_requis, actif):
    return utilisateur.get("role") == role_requis and utilisateur is not None

acces_autorise_bugué(None, "admin", True)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[33], line 4
      1 def acces_autorise_bugué(utilisateur, role_requis, actif):
      2     return utilisateur.get("role") == role_requis and utilisateur is not None
      3 
----> 4 acces_autorise_bugué(None, "admin", True)

Cell In[33], line 2, in acces_autorise_bugué(utilisateur, role_requis, actif)
      1 def acces_autorise_bugué(utilisateur, role_requis, actif):
----> 2     return utilisateur.get("role") == role_requis and utilisateur is not None

AttributeError: 'NoneType' object has no attribute 'get'

On aurait une AttributeError car on tente d’appeler .get() sur None. L’ordre des conditions dans and compte !


✏️ Exercice 6.4 — Détection du plus petit non nul

Écrivez une fonction qui renvoie le plus petit élément non nul d’une liste. Si tous sont nuls ou si la liste est vide, renvoyez None.

Contrainte : utilisez la clause else de boucle.

def min_non_nul(liste):
    ...

print(min_non_nul([0, 3, 0, 7, 2, 0]))   # 2
print(min_non_nul([0, 0, 0]))             # None
print(min_non_nul([]))                     # None
def min_non_nul(liste):
    # Filtrer les non nuls
    non_nuls = []
    for x in liste:
        if x != 0:
            non_nuls.append(x)

    if non_nuls:
        return min(non_nuls)
    return None

print(min_non_nul([0, 3, 0, 7, 2, 0]))
print(min_non_nul([0, 0, 0]))
print(min_non_nul([]))
2
None
None
def min_non_nul_v2(liste):
    non_nuls = [x for x in liste if x != 0]
    return min(non_nuls) if non_nuls else None

print(min_non_nul_v2([0, 3, 0, 7, 2, 0]))
print(min_non_nul_v2([0, 0, 0]))
print(min_non_nul_v2([]))
2
None
None

La compréhension de liste [x for x in ... if ...] sera vue en Partie 3, mais elle est si fréquente qu’il est utile de la reconnaître dès maintenant.


À retenir

Points clés du chapitre
  1. == vs is : valeur vs identité. Utilisez is avec None, True, False uniquement.
  2. Court-circuit : and s’arrête au premier falsy, or au premier truthy. Permet d’éviter des erreurs (division par zéro, appel sur None…).
  3. A and B et A or B renvoient l’un des opérandes, pas forcément True/False.
  4. Idiome x = x or defaut pour valeur par défaut (attention aux falsy légitimes comme 0).
  5. Ternaire a if cond else b — OK pour 2 cas, préférez if/elif sinon.
  6. Clause else de boucle : s’exécute si la boucle termine sans break. À connaître mais utilisez avec parcimonie.
  7. Priorité : ** > unaires > */% > +- > comparaisons > not > and > or. En cas de doute, parenthésez.

← Chapitre précédent : Références et mutabilitéChapitre suivant : Modules →