<jupyter_start><jupyter_text>(Re)prise en main du langage PythonToutes les séances de notre module seront présentées dans des documents au format iPython notebook, associé à l'extension `.ipynb`.  Ce format permet de joindre dans le même document:- du texte dans un format à balises simple, le format [markdown](http://markdowntutorial.com/),- du code à exécuter en [Python](https://docs.python.org/3.8/) (ici la version 3.8),- le résultat des commandes Python à la suite des cellules, que ce soit un résultat textuel ou une image. À noterLes cellules en jaune (sur le modèle suivant) sont des exercices à faire!Exercice: Faire les exercices  Quelques éléments de syntaxe PythonPython est un langage typé dynamiquement, on n’est donc pas obligé de déclarer le type des variables.  On pourra par exemple exécuter le programme suivant en plaçant le curseur dans la cellule et en tapant `Maj+Enter`.<jupyter_code>a: int = 3  # annotation de type (optionnelle, ignorée par le langage)
b: float = 4.0
c = "a * b = "
print(c + str(a * b))
# On pourra préférer cette notation
print("a * b = {}".format(a * b))
# Les f-strings permettent d'évaluer des variables et de les formatter.
print(f"a * b = {a*b}")
# Python 3.8 permet le raccourci suivant
print(f"{a * b = }")<jupyter_output>a * b = 12.0
a * b = 12.0<jupyter_text>Les annotations (placées après les :) sont ignorées par le langage, qui n'effectue aucune vérification avec. Mais ces annotations peuvent être confortables en tant que commentaires. La seule contrainte pour une annotation est qu'elle doit être syntaxiquement valide en Python. On peut écrire un type (courant), une chaîne de caractères ou n'importe quoi.<jupyter_code>angle = float
pi: angle = 3.14
pi, type(pi)<jupyter_output><empty_output><jupyter_text>Le vrai type de la valeur n'est pas impacté. On peut alors par exemple utiliser ces annotations pour ajouter des dimensions à des grandeurs physiques<jupyter_code>c: "m.s^-1" = 3e8
# ou alors
speed = float
c: speed = 3e8<jupyter_output><empty_output><jupyter_text>Les opérateurs habituels sur les entiers et les flottants sont disponibles.  La boucle `for` permet de parcourir des objets itérables comme des listes, des tuples etc.  La fonction `range` permet de générer une séquence d’entiers :<jupyter_code>for x in range(2, 7):  # le 7 est exclu
    print(x)<jupyter_output>2
3
4
5
6<jupyter_text>Important : L’indentation est importante en Python. Elle permet de définir les blocs.  La conditionnelle est définie classiquement :<jupyter_code>if a == 3:
    print("a = 3")
else:
    print("a = something else...")<jupyter_output>a = 3<jupyter_text>Le mot clé pour définir une fonction est `def`.La documentation d'une fonction peut-être écrite au format `__doctest__` en utilisant les triples guillemets.Les exemples d'utilisation indiqués dans la documentation `__doctest__` servent de test unitaire.<jupyter_code>def fact(n: int) -> int:
    """Renvoie la factorielle de n.
    
    >>> fact(6)
    720
    >>> [fact(n) for n in range(6)]
    [1, 1, 2, 6, 24, 120]
    
    Si n est négatif, une exception de type ValueError est levée.
    >>> fact(-1)
    Traceback (most recent call last):
        ...
    ValueError: n doit être un entier positif
    """
    res = 1
    if n < 0:
        raise ValueError("n doit être un entier positif")
    while n > 0:
        res = n * res
        n = n - 1
    return res
fact(6)<jupyter_output><empty_output><jupyter_text>Il est important de bien remplir la documentation, on pourrait en avoir besoin par la suite:<jupyter_code>help(fact)<jupyter_output>Help on function fact in module __main__:

fact(n: int) -> int
    Renvoie la factorielle de n.
    
    >>> fact(6)
    720
    >>> [fact(n) for n in range(6)]
    [1, 1, 2, 6, 24, 120]
    
    Si n est négatif, une exception de type ValueError est levée.
    >>> fact(-1)
    Traceback (most recent call last):
        ...
    ValueError: n doit être un entier positif<jupyter_text>La fonction `doctest.testmod()` permet de tester toutes les fonctions d'un module donné (ici `__main__`):<jupyter_code>import doctest

doctest.testmod()<jupyter_output><empty_output><jupyter_text>Le format iPython notebook permet également de consulter la documentation dans une pop-up à part:<jupyter_code>?fact<jupyter_output><empty_output><jupyter_text>Structures de donnéesPython propose un certain nombres de structures de données de base munies de facilités syntaxiques et algorithmiques.Règle no.1 Tout l'art de la programmation consiste à choisir (et adapter) les bonnes structures de données. Chaînes de caractèresEn Python, le type str représente une suite de caractères Unicode. Tous les caractères (y compris les accentués et ceux utilisés dans la plupart des langues connues) peuvent être concaténés dans une chaîne de caractères valide. Seul le caractère antislash \ doit être doublécar il donne une signification spéciale à certaines séquences de caractères. Le préfixe r"" (pour raw) désactive l’interprétation de l’antislash.<jupyter_code>str()
"bon" + "jour"
a: str = """Bonjour
à tous"""

a
a[0]
a[2:4]
a[-1]
a[-8:]
a = "heLLo"
(a + " ") * 2
len(a)
a.capitalize()
" hello ".strip()
"hello y’all".split()<jupyter_output><empty_output><jupyter_text>TuplesLe tuple est une structure de base du langage Python qui concatène des variables de nature hétérogène. Il est défini par l’opérateur virgule. Le tuple est toujours affiché avec des parenthèses. Un tuple a un seul élément doit être terminé par une virgule ; un tuple sansélément s’écrit avec des parenthèses, mais on peut préférer le constructeur explicite.<jupyter_code>tuple()
latlon: tuple = 43.6, 1.45
latlon
1,
# tuple unpacking
lat, lon = latlon
lat
lat, _ = latlon  # variable muette<jupyter_output><empty_output><jupyter_text>Le déballage requiert autant d’éléments dans la partie gauche que dans la partie droite du signe égal. Si tous les champs ne sont pas nécessaires à gauche, on utilise généralement la variable muette _ . L’opérateur préfixe * permet de grouper plusieurs variables.<jupyter_code>dix = tuple(range(10))
dix
zero, *autres, huit, neuf = dix
zero
autres
huit, neuf<jupyter_output><empty_output><jupyter_text>ListesLa liste est un conteneur séquentiel de valeurs hétérogènes. C’est un objet mutable : on peut en modifier le contenu à tout moment. Cette structure très intuitive, munie d’une algorithmique riche, notamment pour le tri et la recherche est souvent le choix par défaut des débutants pour tous les problèmes qu’ils doivent résoudre.<jupyter_code>list()
a: list = [1, "deux", 3.0]
a[0]
len(a)
a.append(1)
a
a.count(1)
3 in a
a[1] = 2
a
a.sort()
a
a[1:3]<jupyter_output><empty_output><jupyter_text>On peut utiliser le mécanisme de *compréhension de liste* pour construire une liste facilement.  Par exemple, pourconstruire une liste contenant les carrés des valeurs comprises entre 1 et 5 :<jupyter_code>[i for i in range(5)]
# similar to list(i for i in range(5))
# similar to list(range(5))
[str(i) for i in range(5)]
[i ** 2 for i in range(5)]
[i ** 2 for i in range(5) if i & 1 == 0]  # smarter than i%2 == 0
[x.upper() for x in "hello"]  # even with strings<jupyter_output><empty_output><jupyter_text>Rappelons également le constructeur `sorted` qui crée une liste triée à partir d'une structure itérable ou d'un générateur:<jupyter_code>sorted(i ** 2 for i in range(-5, 5))<jupyter_output><empty_output><jupyter_text>EnsemblesL’ensemble (type set) est un conteneur séquentiel d’éléments uniques. On peut créer un ensemble par énumération de valeurs, à partir d’une structure qui permet l’itération (comme une liste, une chaîne de caractères, etc.) ou par compréhension.<jupyter_code>set()
s = {1}
s
set("coucou")
set(i ** 2 for i in (-2, -1, 0, 1, 2))<jupyter_output><empty_output><jupyter_text>Un set peut-être modifié en ajoutant ou supprimant des valeurs. L’arithmétique des ensembles est accessible à l’aide des opérateurs usuels pour l’union | , l’intersection & et la différence - . L’opérateur + n’est pas défini.<jupyter_code>s
s.add(4)
s
s.remove(4)
s
s.pop(), s
s1 = set(range(3))
s2 = set(range(0, -3, -1))
s1, s2
s1 | s2  # union
s1 & s2  # intersection
s1 - s2  # différence<jupyter_output><empty_output><jupyter_text>DictionnairesLes dictionnaires (type dict ) sont des tables de hash qui fonctionnent sur le modèle clé/valeur. Toutes les valeurs utilisées comme clé doivent être hashable, exactement comme pour les ensembles. Ce sont des structures mutables : on peut librement ajouter de nouvelles clés ou remplacer des valeurs. Comme ils sont utilisés de manière extensive à des emplacements critiques du cœur du langage, les dictionnaires sont particulièrement optimisées en Python.<jupyter_code>tour_eiffel = {
    "latitude": 48.8583,
    "longitude": 2.2945,
    "nom": "<NAME>",
    "ville": "Paris",
}
tour_eiffel["pays"] = "France"
tour_eiffel
point = dict(latitude=43.6, longitude=1.45)  # équivalent
point
"latitude" in point<jupyter_output><empty_output><jupyter_text>On peut utiliser l’opération .get() qui permet de définir une valeur par défaut si une clé n’est pas présente dans le dictionnaire :<jupyter_code>altitude = point.get("altitude", 0)
altitude
point.keys()
point.values()
point.items()
dict((key.upper(), value) for (key, value) in point.items())<jupyter_output><empty_output><jupyter_text>L’opérateur préfixe ** permet de décapsuler les dictionnaires. Il est couramment utilisé pour mettre à jour un dictionnaire ou pour en concaténer deux.<jupyter_code>{**point, **{"pays": "France", "longitude": 1.45}}<jupyter_output><empty_output><jupyter_text>Exercice: Construire et documenter une fonction qui calcule la liste des nombres premiers inférieurs ou égaux à n.<jupyter_code># write your code here!
def prime_until(n: int) -> tuple:
    """Renvoie la la liste des nombres premiers inférieurs ou égaux à n.
    
    >>> prime_until(100)
    {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97}
    
    Si n est négatif, une exception de type AssertionError est levée.
    >>> prime_until(-1)
    AssertionError: n doit être un entier positif
    """
    assert n > 0, "n must be a positive integers"
    numbers = list(range(2,n+1))
    primes = set()
    while len(numbers) > 0:
        cur = numbers[0]
        primes.add(cur)
        numbers = list((num for num in numbers if num % cur != 0))
    return primes
    
print(prime_until(100))
# print(prime_until(-1))
# %load solutions/prime.py
import math


def prime(n: int) -> int:
    """Computes all prime numbers below n.
    Computes the sieve of Eratosthenes
    >>> prime(20)
    {1, 2, 3, 5, 7, 11, 13, 17, 19}
    """
    assert n > 0, "Positive integers only"
    p = set(range(1, n))
    for i in range(2, int(math.sqrt(n)) + 1):
        p = p - set(x*i for x in range(2, n//i + 1))
    return p


prime(20)
", ".join(str(i) for i in prime(100))<jupyter_output><empty_output><jupyter_text>Exercice: Écrire un programme qui lit le fichier 00-lorem-ipsum.txt et y compte le nombre d’occurrences de chaque mot.On pourra partir du modèle suivant :```pythonwith open("data/00-lorem-ipsum.txt") as f:    print(f.readlines())```<jupyter_code># write your code here!
import string
from collections import defaultdict

with open("data/00-lorem-ipsum.txt") as f:
#     print(f.readlines())
    d = defaultdict(int)
    for line in f.readlines():
        for char in string.punctuation:
            line = line.replace(char, " ") 
        for word in line.split():
            d[word] += 1
    
    for key in sorted(d.keys()):
        if d[key] > 5:  # make the output a bit lighter...
            print(f"{key}: {d[key]}")
# %load solutions/lorem_ipsum.py
import string

with open("data/00-lorem-ipsum.txt") as f:
    d = {}
    for x in f.readlines():
        for s in string.punctuation:  # !"#$%&\'()*+,-./:;<=>?@[\\]^_‘{|}~
            x = x.replace(s, " ")
        for p in x.split():
            if p in d.keys():
                d[p] += 1
            else:
                d[p] = 1

    for key in sorted(d.keys()):
        if d[key] > 5:  # make the output a bit lighter...
            print("{}: {}".format(key, d[key]))

# %load solutions/lorem_ipsum_alt.py
import string
from collections import defaultdict

with open("data/00-lorem-ipsum.txt") as f:
    d = defaultdict(int)
    for x in f.readlines():
        for s in string.punctuation:  # !"#$%&\'()*+,-./:;<=>?@[\\]^_‘{|}~
            x = x.replace(s, " ")
        for p in x.split():
            d[p] += 1

    for key in sorted(d.keys()):
        if d[key] > 5:  # make the output a bit lighter...
            print(f"{key}: {d[key]}")

# %load solutions/lorem_ipsum_alt2.py
import string
from collections import Counter

lw=[]
with open("data/00-lorem-ipsum.txt") as f:
    for l in f.readlines():
        for s in string.punctuation:  # !"#$%&\'()*+,-./:;<=>?@[\\]^_‘{|}~
            l = l.replace(s, " ")
        lw+=[w for w in l.split()]
        
c=Counter(lw)
print(c.most_common(5))<jupyter_output>[('non', 15), ('et', 14), ('ac', 12), ('sit', 11), ('amet', 11)]
{'fraction_non_alphanumeric': 0.11448043494907727, 'fraction_numerical': 0.020981698445516502, 'mean_word_length': 3.9301623254058136, 'pattern_counts': {'":': 6, '<': 89, '<?xml version=': 0, '>': 126, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '22', 'dir': 'jupyter-structured-clean-dedup', 'id': '397946', 'license': 'MIT', 'n_tokens_mistral': 4814, 'n_tokens_neox': 4636, 'n_words': 1585, 'path': '00-types-arithmetiques.ipynb', 'repo_name': 'LudovicSterlin/back2python', 'size': '13064'}