Fonctions: Acte II

Namespace

On avait vu lorsqu’on a présenté les variables qu’on pouvait les considérer comme des associations entre des noms et des objets python. On avait aussi mentionné le fait que cette association était visualisable grâce aux fonctions globals et locals.

>>> a, b, c = 1, 2, 3
>>> message = "abde"
>>> ls = [1, 2, 3]
>>> ts = (1, 4, 2)
>>> nbr = 12.5
>>> locals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'a': 1, 'b': 2, 'c': 3, 'message': 'abde', 'ls': [1, 2, 3], 'ts': (1, 4, 2), 'nbr': 12.5}
>>> globals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'a': 1, 'b': 2, 'c': 3, 'message': 'abde', 'ls': [1, 2, 3], 'ts': (1, 4, 2), 'nbr': 12.5}

On pouvait, à juste titre, se demander pourquoi on avait besoin de deux fonctions alors qu’elles renvoient la même chose.

En fait il y a à chaque instant dans python non pas une mais plusieurs associations nom -> objets. On les appelle des espaces de noms.

Il y a l’association au niveau de l’interpréteur (dite globale) et il y en a une autre à l’intérieur de chaque fonction (dite locale). Celle-ci n’est visible qu’à l’intérieur de la fonction concernée. Au passage cela permet d’avoir des variables distinctes ayant le même nom à l’intérieur de fonctions différentes ou à l’échelle de l’interpréteur.

Les fonctions locals et globals permettent de visualiser ces différentes associations:

>>> x = 1
>>> def ma_fonction():
...     x = 2
...     print(locals())
...     print(globals())
...
>>> ma_fonction()
{'x': 2}
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'x': 1, 'ma_fonction': <function ma_fonction at 0x7f74b7b941f0>}
>>> def autre_fonction():
...     x = 3
...     print(locals())
...     print(globals())
...
>>> autre_fonction()
{'x': 3}
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'x': 1, 'ma_fonction': <function ma_fonction at 0x7f74b7b941f0>, 'autre_fonction': <function autre_fonction at 0x7f74b7b94280>}

On peut voir ici qu’on a trois variables x distincts

Notez que grâce à globals on peut accéder indirectement aux globales depuis l’intérieur d’une fonction alors que les espaces internes entre fonctions sont totalement cloisonnés.

Les utilisations de variables globales sont à éviter au maximum car elles empêchent l’isolation de la fonction du monde extérieur (à part via les arguments et le retour). Or c’est justement l’objectif des fonctions, et ce afin de diminuer la complexité du code à avoir en tête à chaque instant.

Comme malgré tout ce mécanisme est parfois utilisé on va décrire rapidement son fonctionnement.

  1. S’il n’y a pas de variable du nom recherché dans l’espace local, python va alors chercher dans le global.

    >>> x = 1
    >>> def lecture_globale():
    ...     print(x)
    ...
    >>> lecture_globale()
    1
    >>> x = 2
    >>> lecture_globale()
    2

    REMARQUE on voit déjà que pour savoir ce que va faire la fonction il faut connaître la valeur de x à l’instant de l’appel. Non seulement cela rajoute de la charge mentale mais ce n’est pas visible au niveau de la signature de la fonction (arguments et retour) donc il faut se plonger dans le code de la fonction pour s’en rendre compte.

  2. Notez bien que le mécanisme précédent fonctionne à priori juste en lecture, car l’affectation crée une variable locale. Au passage dès qu’une variable est crée quelque part dans le corps de la fonction python la considérera comme locale même avant l’affectation.

    >>> x = 1
    >>> def probleme():
    ...     print(x)
    ...     x = 2
    ...
    ...
    >>> probleme()
    UnboundLocalError:
    local variable 'x' referenced before assignment

    Ici la présence de la ligne x = 2 fait qu’à l’échelle de toute la fonction, x fait référence à une variable locale. Mais la ligne print(x) précédant la création de cette variable locale, python indique qu’il ne peut pas y accéder.
    Si on veut modifier une variable globale depuis l’intérieur d’une fonction (ce qui rappelons le est généralement une très mauvaise idée) il faut utiliser le mot clef global.

    >>> x = 1
    >>> def plus_de_bogue():
    ...     global x
    ...     print(x)
    ...     x = 2
    ...
    >>> plus_de_bogue()
    1
    >>> x
    2
    c’est une façon de signaler à python que x est toujours à chercher dans l’espace global.
  3. Notez que le comportement est encore plus subtil si on a un objet mutable sous-jacent

    >>> ls = [1, 2, 3]
    >>> def ma_fonction():
    ...     print(ls)
    ...     ls.append(4)
    ...
    >>> ma_fonction()
    [1, 2, 3]
    >>> ls
    [1, 2, 3, 4]

    En effet comme la modification de ls n’est pas une affectation python, et comme il n’y a pas de ls dans l’espace local, python va faire la modification sur l’objet global.

REMARQUE notez bien que les objets python eux même ne sont pas locaux ou globaux, c’est juste les espaces de noms pointant vers ces objets qui le sont. En particulier on peut très bien avoir à la fois, un nom local et un nom global qui pointent vers le même objet.

>>> gs = [1, 2, 3]
>>> def test():
...     ls = gs
...     print(locals())
...     print(globals())
...     print("ls is gs : ", ls is gs)
...
>>> test()
{'ls': [1, 2, 3]}
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'gs': [1, 2, 3], 'test': <function test at 0x7f6d67c4e1f0>}
ls is gs :  True

REMARQUE les arguments sont des variables locales et permettent justement le passage contrôlé d’objets du global au local.

>>> ls = [1, 2, 3]
>>> def visualisation(valeurs):
...     print(f"local: {locals()}")
...     print(f"global: {globals()}")
...
...
>>> visualisation(valeurs=ls)
local: {'valeurs': [1, 2, 3]}
global: {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'ls': [1, 2, 3], 'visualisation': <function visualisation at 0x7f69bb0891f0>}

REMARQUE les structures permettant de stocker l’association nom -> objet sont en fait des dict que l’on verra dans la prochaine leçon.

REMARQUE la gestion automatique de la mémoire en python fait que les objets sont éliminés lorsque plus aucun nom ne pointe vers eux. (évidemment c’est en fait plus délicat mais l’idée de départ est celle-là)

Fonctions: bonnes pratiques.

Noms et tâches

On a déjà mentionné le fait qu’il faut prendre soin de bien nommer les fonctions et les arguments pour obtenir du code lisible et minimiser les bogues et le cout de maintenance.
Si on éprouve de la difficulté à bien nommer une fonction, cela peut être un signe que celle-ci effectue en fait plusieurs tâches de natures différentes. De ce fait elle devrait être découpée en plusieurs fonctions distinctes.

>>> def va_savoir(valeurs):
...     moyenne = sum(valeurs) / len(valeurs)
...     ok = True
...     for valeur in valeurs:
...             if valeur < 0:
...                     ok = False
...     return moyenne, ok
...

Ici la fonction calcule la moyenne et regarde si tous les éléments sont positifs. Les deux fonctionnalités sont totalement indépendantes et devraient donc être découpées en deux fonctions.

>>> def calcule_moyenne(valeurs):
...     return sum(valeurs) / len(valeurs)
...
>>> def sont_tous_positifs(valeurs):
...     for valeur in valeurs:
...             if valeur < 0:
...                     return False
...     return True
...

On a ainsi la règle : une fonction, une tâche!

Délégation, niveau d’abstraction

On n’hésitera pas à introduire des fonctions auxiliaires que ce soit dans le cadre de compréhensions ou à l’intérieur d’une fonction principale.

Ainsi plutôt que

>>> message = "".join([chr(ord(lettre) + ord("A") - ord("a")) for lettre in "abd efa1ef,qsdf." if ord("a") <= ord(lettre) <= ord("z")])
>>> message
'ABDEFAEFQSDF'

on écrirait

>>> def min_to_maj(lettre):
...     return chr(ord("A") + ord(lettre) - ord("a"))
...
>>> def est_minuscule(lettre):
...     return ord("a") <= ord(lettre) <= ord("z")
...
>>> message = "".join([min_to_maj(lettre) for lettre in "abd efa1ef,qsdf." if est_minuscule(lettre)])
>>> message
'ABDEFAEFQSDF'

De la même façon si on devait coder une fonction reponse prenant en entrée n: int et calculant la somme des carrés des nombres parfaits entre 1 et n. On pourrait commencer par coder

def reponse(n):
    return sum(nbr ** 2 for nbr in genere_nombre_parfaits(n))

On s’est ainsi appuyé sur une fonction genere_nombre_parfaits qui n’existe pas encore. On la code ensuite

def genere_nombre_parfaits(n):
    return [nbr for nbr in range(1, n + 1) if est_parfait(nbr)]

Là encore on s’est appuyé sur une fonction est_parfait qui n’existe pas encore mais qu’on va coder maintenant.

def est_parfait(nbr):
    return sum(genere_diviseurs_stricts(nbr)) == nbr

Le motif apparaît encore et maintenant

def genere_diviseurs_stricts(nbr):
    return [d for d in range(1, nbr) if est_divisible(nbr, d)]

Finalement

def est_divisible(n, d):
    return n % d == 0

Cet exemple est clairement un peu caricatural mais donne une idée de la façon d’aborder une tâche non triviale. Le point clef étant qu’à chaque nouvelle fonction la complexité de la tâche restante a diminué. On reprendra cet exemple lorsqu’on aura parlé des signatures et des tests.

REMARQUE notez que pour les extraits de code ci-dessus l’absence des chevrons (>>>) indique que l’on ne se place pas dans une session interactive mais dans un script (un fichier) qu’on fournira directement à python. Cela rend plus simple les allers retours et les corrections d’erreurs.

Signature, pureté

On a insisté sur le fait qu’il était préférable de manière générale d’éviter d’utiliser des variables globales à l’intérieur d’une fonction. On préfère les passer explicitement comme arguments au moment de l’appel. Cela rend la fonction plus lisible au sens où on devrait pouvoir l’utiliser correctement en connaissant

Tout ceci constitue la signature d’une fonction. Pour être encore plus complet, on peut préciser les types de la façon suivante:

>>> def calcule_moyenne(valeurs: list[int]) -> float:
...     return sum(valeurs) / len(valeurs)
...

(Notez que le code précédent marche sous python3.9, sinon il faudrait remplacer list[int] par List[int] après avoir commencé par faire from typing import List)

C’est la signature qui apparaît automatiquement dans tous les environnement de développement sérieux, ainsi que la documentation dans certains cas (voire plus bas).
On peut même utiliser un typechecker (tel que mypy) qui vérifiera que les types entre les différentes fonctions sont bien compatibles (ce qui permet de fait de trouver un nombre surprenant d’erreurs).

En plus de l’utilisation des variables globales, l’affichage via print et les mutations des arguments sont des choses qui ne sont pas visibles dans la signature et donc qui compliquent l’utilisation de la fonction, voire sont sources de bogues.

Donnons un exemple:

>>> def somme_bizarre(valeurs):
...     somme = 0
...     while valeurs:
...             somme = somme + valeurs.pop()
...     return somme
...
>>> somme_bizarre([1, 2, 3])
6
>>> def compte_bizarre(valeurs):
...     nombre_elements = 0
...     while valeurs:
...             nombre_elements = nombre_elements + 1
...             valeurs.pop()
...     return nombre_elements
...
>>> compte_bizarre([1, 2, 3])
3
>>> def moyenne(valeurs):
...     return somme_bizarre(valeurs) / compte_bizarre(valeurs)
...
>>> moyenne([1, 2, 3])
ZeroDivisionError:
division by zero

Ici le problème vient du fait que les deux premières fonctions mutent la liste passée en argument qui est vide après exécution de la fonction. Dans moyenne, la fonction somme_bizarre est donc bien appelée sur [1, 2, 3] mais compte_bizarre est par contre appelée sur []!

On dira qu’une fonction est pure lorsque la signature résume parfaitement l’activité de la fonction.

Tests

L’avantage lorsqu’on a une fonction pure est qu’on peut facilement rajouter des vérifications automatiques sur des cas particuliers. Ceux-ci permettent d’abord d’avoir plus confiance dans le code fourni. Mais on peut aussi envisager de commencer par écrire les tests avant d’écrire la fonction, ce qui permet d’une certaine façon de préciser le cahier des charges de celle-ci.

On utilise pour faire des tests l’instruction assert. Elle est obligatoirement suivie d’une expression booléenne. Si celle-ci évalue à False python plante, si elle évalue à True par contre python passe silencieusement à la suite.

>>> assert 12343 % 3 == 0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError
>>> assert 12342 % 3 == 0
>>>

Notez que de manière optionnelle on peut en fait passer en plus une str après l’expression booléenne, pour améliorer le message d’erreur.

>>> assert 12343 % 3 == 0, "12343 n'est pas divisible par 3"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError: 12343 n'est pas divisible par 3
>>> assert 12342 % 3 == 0, "12342 n'est pas divisible par 3"
>>>

Ici comme 12342 est de fait divisible par 3, il n’y a pas d’erreur donc pas de message d’erreur.

On va maintenant reprendre l’exemple des nombres parfaits. Le point de départ est toujours la fonction complexe que l’on va maintenant typer.

def reponse(n: int) -> int:
    return sum(nbr ** 2 for nbr in genere_nombre_parfaits(n))

On précise dans un premier temps la signature et les tests de genere_nombre_parfaits (après avoir calculé manuellement ou via wikipédia que les premiers étaient 6 et 28)

def genere_nombre_parfaits(n: int) -> list[int]:
    ...

assert genere_nombre_parfaits(5) == []
assert genere_nombre_parfaits(10) == [6]
assert genere_nombre_parfaits(20) == [6]
assert genere_nombre_parfaits(30) == [28]

def reponse(n: int) -> int:
    return sum(nbr ** 2 for nbr in genere_nombre_parfaits(n))

On rajoute maintenant le code nécessaire.

def genere_nombre_parfaits(n: int) -> list[int]:
    return [nbr for nbr in range(1, n + 1) if est_parfait(nbr)]

assert genere_nombre_parfaits(5) == []
assert genere_nombre_parfaits(10) == [6]
assert genere_nombre_parfaits(20) == [6]
assert genere_nombre_parfaits(30) == [28]

def reponse(n: int) -> int:
    return sum(nbr ** 2 for nbr in genere_nombre_parfaits(n))

A l’étape d’après on réitère le processus. D’abord

def est_parfait(nbr: int) -> bool:
    ...

assert not est_parfait(5)
assert est_parfait(6)
assert not est_parfait(10)
assert est_parfait(28)

def genere_nombre_parfaits(n: int) -> list[int]:
    return [nbr for nbr in range(1, n + 1) if est_parfait(nbr)]

assert genere_nombre_parfaits(5) == []
assert genere_nombre_parfaits(10) == [6]
assert genere_nombre_parfaits(20) == [6]
assert genere_nombre_parfaits(30) == [28]

def reponse(n: int) -> int:
    return sum(nbr ** 2 for nbr in genere_nombre_parfaits(n))

puis

def est_parfait(nbr: int) -> bool:
    return sum(genere_diviseurs_stricts(nbr)) == nbr

assert not est_parfait(5)
assert est_parfait(6)
assert not est_parfait(10)
assert est_parfait(28)

def genere_nombre_parfaits(n: int) -> list[int]:
    return [nbr for nbr in range(1, n + 1) if est_parfait(nbr)]

assert genere_nombre_parfaits(5) == []
assert genere_nombre_parfaits(10) == [6]
assert genere_nombre_parfaits(20) == [6]
assert genere_nombre_parfaits(30) == [28]

def reponse(n: int) -> int:
    return sum(nbr ** 2 for nbr in genere_nombre_parfaits(n))

En réitérant le procédé on finit par le script

def est_divisible(n: int, d: int) -> bool:
    return n % d == 0

assert est_divisible(10, 5)
assert est_divisible(7, 1)
assert not est_divisible(13, 5)

def genere_diviseurs_stricts(nbr: int) -> list[int]:
    return [d for d in range(1, nbr) if est_divisible(nbr, d)]

assert genere_diviseurs_stricts(5) == [1]
assert genere_diviseurs_stricts(6) == [1, 2, 3]
assert genere_diviseurs_stricts(10) == [1, 2, 5]
assert genere_diviseurs_stricts(28) == [1, 2, 4, 7, 14]

def est_parfait(nbr: int) -> bool:
    return sum(genere_diviseurs_stricts(nbr)) == nbr

assert not est_parfait(5)
assert est_parfait(6)
assert not est_parfait(10)
assert est_parfait(28)

def genere_nombre_parfaits(n: int) -> list[int]:
    return [nbr for nbr in range(1, n + 1) if est_parfait(nbr)]

assert genere_nombre_parfaits(5) == []
assert genere_nombre_parfaits(10) == [6]
assert genere_nombre_parfaits(20) == [6]
assert genere_nombre_parfaits(30) == [28]

def reponse(n: int) -> int:
    return sum(nbr ** 2 for nbr in genere_nombre_parfaits(n))

Notez que l’on a au final écrit le script de bas en haut, et que les étapes intermédiaires offrent des garanties (si les assert passent silencieusement).

Documentation

La dernière étape, lorsqu’on veut développer une librairie facilement utilisable, est de rajouter une docstring.
Il s’agit des messages que l’on a jusqu’à présent récupérer en utilisant la fonction help. Pour les inclure dans nos fonctions, il suffit de mettre une chaine de caractères (éventuellement multilignes) comme première instruction du corps principal de l’instruction def.

Ainsi le code

>>> def essai():
...     """Ma docstring à moi
... et sur plusieurs lignes
... """
...     print("ok")
...
>>> essai()
ok
>>> help(essai)

génère

Help on function essai in module __main__:

essai()
    Ma docstring à moi
    et sur plusieurs lignes

Comme premier exemple détaillé voici la docstring de la fonction linspace du module numpy


Help on function linspace in module numpy:

linspace(start, stop, num=50, endpoint=True, retstep=False, dtype=None, axis=0)
    Return evenly spaced numbers over a specified interval.

    Returns `num` evenly spaced samples, calculated over the
    interval [`start`, `stop`].

    The endpoint of the interval can optionally be excluded.

    .. versionchanged:: 1.16.0
        Non-scalar `start` and `stop` are now supported.

    Parameters
    ----------
    start : array_like
        The starting value of the sequence.
    stop : array_like
        The end value of the sequence, unless `endpoint` is set to False.
        In that case, the sequence consists of all but the last of ``num + 1``
        evenly spaced samples, so that `stop` is excluded.  Note that the step
        size changes when `endpoint` is False.
    num : int, optional
        Number of samples to generate. Default is 50. Must be non-negative.
    endpoint : bool, optional
        If True, `stop` is the last sample. Otherwise, it is not included.
        Default is True.
    retstep : bool, optional
        If True, return (`samples`, `step`), where `step` is the spacing
        between samples.
    dtype : dtype, optional
        The type of the output array.  If `dtype` is not given, infer the data
        type from the other input arguments.

        .. versionadded:: 1.9.0

    axis : int, optional
        The axis in the result to store the samples.  Relevant only if start
        or stop are array-like.  By default (0), the samples will be along a
        new axis inserted at the beginning. Use -1 to get an axis at the end.

        .. versionadded:: 1.16.0

    Returns
    -------
    samples : ndarray
        There are `num` equally spaced samples in the closed interval
        ``[start, stop]`` or the half-open interval ``[start, stop)``
        (depending on whether `endpoint` is True or False).
    step : float, optional
        Only returned if `retstep` is True

        Size of spacing between samples.


    See Also
    --------
    arange : Similar to `linspace`, but uses a step size (instead of the
             number of samples).
    geomspace : Similar to `linspace`, but with numbers spaced evenly on a log
                scale (a geometric progression).
    logspace : Similar to `geomspace`, but with the end points specified as
               logarithms.

    Examples
    --------
    >>> np.linspace(2.0, 3.0, num=5)
    array([2.  , 2.25, 2.5 , 2.75, 3.  ])
    >>> np.linspace(2.0, 3.0, num=5, endpoint=False)
    array([2. ,  2.2,  2.4,  2.6,  2.8])
    >>> np.linspace(2.0, 3.0, num=5, retstep=True)
    (array([2.  ,  2.25,  2.5 ,  2.75,  3.  ]), 0.25)

    Graphical illustration:

    >>> import matplotlib.pyplot as plt
    >>> N = 8
    >>> y = np.zeros(N)
    >>> x1 = np.linspace(0, 10, N, endpoint=True)
    >>> x2 = np.linspace(0, 10, N, endpoint=False)
    >>> plt.plot(x1, y, 'o')
    [<matplotlib.lines.Line2D object at 0x...>]
    >>> plt.plot(x2, y + 0.5, 'o')
    [<matplotlib.lines.Line2D object at 0x...>]
    >>> plt.ylim([-0.5, 1])
    (-0.5, 1)
    >>> plt.show()

On peut se contenter d’un effort plus modeste, d’autant plus si on a bien soigné la signature. Donnons comme exemple le code des nombres parfaits. REMARQUE notez qu’on a complété est_divisible en générant une exception pour donner un exemple, on aura une leçon entière sur la gestion des erreurs.

def est_divisible(n: int, d: int) -> bool:
    """Détermine si d divise n.

    Génère une exception si d est nul mais pas n.

    Exemple:
>>> est_divisible(10, 5)
True
>>> est_divisible(7, 1)
True
>>> est_divisible(13, 5)
False
>>> est_divisible(0, 0)
True
>>> est_divisible(10, 0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 17, in est_divisible
ValueError: Seul 0 est divisible par 0
    """
    if d == 0:
        if n == 0:
            return True
        else:
            raise ValueError("Seul 0 est divisible par 0")
    return n % d == 0

assert est_divisible(10, 5)
assert est_divisible(7, 1)
assert not est_divisible(13, 5)

def genere_diviseurs_stricts(nbr: int) -> list[int]:
    """Renvoit la liste des diviseurs de nbr différents de nbr.

    Exemples:
>>> genere_diviseurs_stricts(5)
[1]
>>> genere_diviseurs_stricts(6)
[1, 2, 3]
>>> genere_diviseurs_stricts(10)
[1, 2, 5]
>>> genere_diviseurs_stricts(28)
[1, 2, 4, 7, 14]
    """
    return [d for d in range(1, nbr) if est_divisible(nbr, d)]

assert genere_diviseurs_stricts(5) == [1]
assert genere_diviseurs_stricts(6) == [1, 2, 3]
assert genere_diviseurs_stricts(10) == [1, 2, 5]
assert genere_diviseurs_stricts(28) == [1, 2, 4, 7, 14]

def est_parfait(nbr: int) -> bool:
    """Détermine si nbr est parfait.

    Exemples:
>>> est_parfait(5)
False
>>> est_parfait(6)
True
>>> est_parfait(10)
False
>>> est_parfait(28)
True
    """
    return sum(genere_diviseurs_stricts(nbr)) == nbr

assert not est_parfait(5)
assert est_parfait(6)
assert not est_parfait(10)
assert est_parfait(28)

def genere_nombre_parfaits(n: int) -> list[int]:
    """Renvoit la liste des nombres parfaits de 1 à n compris.

    Exemples
>>> genere_nombre_parfaits(5)
[]
>>> genere_nombre_parfaits(10)
[6]
>>> genere_nombre_parfaits(20)
[6]
>>> genere_nombre_parfaits(30)
[6, 28]
    """
    return [nbr for nbr in range(1, n + 1) if est_parfait(nbr)]

assert genere_nombre_parfaits(5) == []
assert genere_nombre_parfaits(10) == [6]
assert genere_nombre_parfaits(20) == [6]
assert genere_nombre_parfaits(30) == [28]

def reponse(n: int) -> int:
    return sum(nbr ** 2 for nbr in genere_nombre_parfaits(n))

REMARQUE on n’hésitera pas à faire un copier/coller d’une véritable session dans l’interpréteur (c’est le cas ici).

Références

On pourra consulter le livre de Robert Martin : Coder proprement pour plus de recommandations/explications sur les bonnes pratiques.

Pour une vision plus pythonesque on consultera le livre de Brett Slatkin : Effective python.

Exercices

Pour les questions suivantes, on testera et on documentera les fonctions utilisées.

  1. Coder une fonction renvoie_nombre_premier prenant en argument n: int et renvoyant le n-ième nombre premier.
  2. Coder une fonction decoupe prenant en entrée nombres: list[int] et renvoyant deux listes petits et grands
  1. Coder une fonction formate prenant en entrée table: list[list[int]] et renvoyant une chaine de caractères représentant la table de la façon suivante: si table = [[1, 2, 3], [4, 5, 6]] la sortie est

    """
    -------------
    | 1 | 2 | 3 |
    -------------
    | 4 | 5 | 6 |
    -------------
    """

    On pourra supposer que la table a des listes internes de même taille.

Pour aller plus loin

>>> help("NAMESPACES")
>>> help("global")
>>> help("SCOPING")
>>> help("ASSERTION")
>>> help("NONE")
>>> help("assert")

Corrections

Voir le fichier associé pour les corrections et une idée de la présentation attendue.