cours/python_albi/programme

Formation Python perfectionnement

Planar, Leyard Group, Albi, décembre 2019

Jour 1

VSCode et Python

Références : Debugging configurations for Python in VSCode, Using Python environments in VSCode, minimal Python system scripting

Activités tutorées :

  • Écrire un programme sigma.py qui calcule la somme des 10 premiers entiers et créer une configuration d’exécution pour le debugger de VSCode. Exécuter le programme en pas à pas

  • Ajouter le nombre N en paramètre de sigma.py et créer une configuration d’exécution avec un argument N=5

  • Créer un environnement virtuel venv pour Python 3, puis créer une configuration d’exécution dans cet environnement pour un programme version.py qui affiche le numéro de version de l’interpréteur Python (voir sys.version_info)

Scripting système en Python

Notions étudiées :

  • Logique de l’importation de modules
  • Librairies os, sys, argparse
  • Écriture de scripts système

Références :

Activités tutorées :

  • Écrire un script Python3 qui affiche tous les éléments de sys.path

  • Écrire un script Python3 brklnk.py qui parcourt tous les liens contenus dans une page web et affiche les liens cassés, avec les options suivantes :

    • –help : affiche l’usage de la commande
    • –depth=n : profondeur de recherche n (1 par défaut)
./brklnk.py [options] <url>

Sous Linux, déployer cette commande globalement sous le nom brklnk.

Tester sur :

Base de données et ORM en Python

Références: minimal Django ORM, csv module

Activités tutorées :

  • Écrire un programme Python3 qui importe dans une base de données sqlite3 les données de nutrition de la table Ciqual 2017 ciqual2017.csv.zip

On créera une base de données dont le schéma permet d’inclure toutes les informations contenues dans Ciqual 2017. Plus précisément, la base de données devra comporter les 4 tables suivantes :

Nutrient
   id: int (autoincrement)
   name string

Grp
   code: string
   name: string
   father_grp: Grp/null

Food
   code string
   name: string
   grp: Grp/null
   ssgrp: Grp/null
   ssssgrp: Grp/null

NutData
   id: int (auto)
   food: Food
   nutrient: Nutrient
   value: string

Les premiers éléments de cette base seront :

Nutrient(1, "Eau (g/100g)")
Nutrient(2, "Protéines (g/100g)")
...
Grp("01", "entrées et plats composés", null)
Grp("02", "fruits, légumes, légumineuses et oléagineux", null)
...
Grp("0101", "salades composées et crudités", "01")
Grp("0102", "soupes", "01")
...
Food("25600", "Céleri rémoulade, préemballé", "01", "0101", "000000")
Food("25601", "Salade de thon et légumes, appertisée, égouttée", "01", "0101", "000000")
...
NutData(1, "25600", 1, "78.5")
NutData(2, "25600", 2, "1.12")
...

HTTP, WSGI, FLASK

Références: Protocole HTTP, Flask WSGI, Gunicorn,

Activités tutorées :

  • créer une page web d’affichage des aliments de la BD avec Flask et le moteur de template jinja2
  • mettre en place la pile Flask - Gunicorn

Jour 2

Techniques avancées en Python

Notions étudiées :

  • Décorateurs, memoization
  • Générateurs

Références : @ Decorators doc, itertools

Activités tutorées :

  • Créer un décorateur pour mesurer le temps d’exécution d’une fonction en microsecondes
  • Créer un décorateur de mémoization pour des fonctions à un argument
  • Créer un générateur fibonacci qui produit la séquence de Fibonacci : 1, 1, 2, 3, 5, 8, 11, etc.
  • Créer un générateur harmonic qui produit la série harmonique divergente : 1, 1+1/2, 1+1/2+1/3, etc.
  • Créer un générateur read_in_chunks(file_object, chunk_size=1024) qui lit les fichiers par morceaux de taille fixe

Packages Python

Notions étudiées :

  • Structure d’un package
  • Création d’un package distribuable

Activités tutorées :

  • Créer un package mymath contenant 2 modules series et sequences
  • Dans sequences, ajouter le générateur fibonacci ; dans series, ajouter harmonic
  • Créer un programme de test qui importe et utilise ces deux générateurs

Python memory management

https://rushter.com/blog/python-garbage-collector/

Performance : Python vs Rest of the World

Références :

Python concurrent programming

multithreading & multiprocessing : CPU-intensive applications

Références : concurrent.futures

Activités tutorées :

  • lancer des tâches en parallèle dans des threads séparés
  • lancer des tâches en parallèle dans des processus séparés

asyncio : I/O-bound applications

Notions étudiées :

  • coroutines
  • asyncio, async / await

Références :

Activités tutorées :

  • réaliser une version asynchrone de brklnk

Websockets

Références:

Activités tutorées :

  • chat réseau : https://github.com/sjkingo/websocket-chat

Linting

Références :

Activités tutorées :

  • pip install flake8
  • pip install black
  • créer un git hook pour exécuter flake8 sur les fichiers .py lors du commit

Jour 3

Context managers

Références :

Activités tutorées :

  • créer un contexte manager File
  • créer un contexte manager File avec le décorateur @contextmanager

Travailler avec des expressions régulières

Notions étudiées :

  • notion de regex, syntaxe

Références :

Activités tutorées :

  • Parsing d’un numéro de téléphone
  • Parsing d’une ligne de netlist

PEG parsers

Notions étudiées :

  • Grammaires, notation EBNF

Références:

Activités tutorées :

  • Parsing d’un fichier .INI
  • Parsing d’une netlist Spice complète

Visualisation de graphes

Références:

Activités tutorées :

  • visualisation du graphe de dépendance d’une netlist

Génération de PDF

Références:

Activités tutorées :

  • Génération automatique d’un rapport descriptif de circuit : liste des composants, schéma

Solutions

Scripting

Affiche les liens cassés dans une page web, à une profondeur maximale depth

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import argparse
from urllib.parse import urljoin
import requests
import bs4

def print_broken_links(url, depth, already_visited):
   if depth >= 0 and not url in already_visited:
      already_visited.add(url)
      try:
         response = requests.get(url)
         if response.status_code >= 400:
            print("broken link: {0}".format(url))
         else:
            html_parser = bs4.BeautifulSoup(response.content, 'html.parser')
            # collect links
            links = html_parser.find_all('a')
            for link in links:
               href = link.get('href', None)
               if href:
                  # search recursively
                  absolute_link_url = urljoin(url, href)
                  print_broken_links(absolute_link_url, depth-1, already_visited)
      except:
         print("invalid link: {0}".format(url))


def main():
   # create parser
   parser = argparse.ArgumentParser()
   parser.add_argument("url", type=str, help="url to check")
   parser.add_argument("--depth", type=int, default=1, help="search depth (default: 1)")

   # parse command arguments
   args = parser.parse_args()

   # start search
   print_broken_links(args.url, args.depth, set())

if __name__ == '__main__':
   main()

Django ORM

Lit le fichier csv ciqual2017.csv et créé les aliments et les groupes d’aliments associés

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import csv
import os
import django

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_orm.settings")
django.setup()

# don't import business models before doing `django.setup()`
from foods.models import Food, Grp, Nutrient, NutData

with open('ciqual2017.csv', newline='') as csvfile:
    csv_reader = csv.DictReader(csvfile, delimiter=';')
    for row in csv_reader:
        grp, _ = Grp.objects.get_or_create(code=row['alim_grp_code'], name=row['alim_grp_nom_fr'])
        grp.save()
        ssgrp, _ = Grp.objects.get_or_create(code=row['alim_ssgrp_code'], name=row['alim_ssgrp_nom_fr'])
        ssgrp.save()
        ssssgrp, _ = Grp.objects.get_or_create(code=row['alim_ssssgrp_code'], name=row['alim_ssssgrp_nom_fr'])
        ssssgrp.save()
        ssssgrp.father_grp = ssgrp
        ssssgrp.save()
        ssgrp.father_grp = grp
        ssgrp.save()

        food = Food(
            code = row['alim_code'],
            name = row['alim_nom_fr'],
            grp = grp,
            ssgrp = ssgrp,
            ssssgrp = ssssgrp,
        )
        food.save()
        print(food)

Flask

Serveur web qui affiche la liste les aliments sous forme de liens cliquables. Lorsqu’on clique sur un aliment, le détail de l’aliment est affiché.

from flask import Flask
from flask import render_template

import django
import os

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_orm.settings")
django.setup()

# don't import business models before doing `django.setup()`
from foods.models import Food

# `app` is a wsgi callable
app = Flask(__name__)

@app.route("/")
def food_list():
    return render_template('food_list.html', foods=Food.objects.all())

@app.route("/food/<food_code>")
def food_detail(food_code):
    return render_template('food_details.html', food=Food.objects.get(code=food_code))


if __name__ == '__main__':
    app.run(debug=True)

food_list.html

<!doctype html>
<title>List des aliments</title>

<ol> 
   {%for food in foods%} 
       <li><a href={{food.code}}>{{food.name}}</a></li> 
   {%endfor%}      
</ol> 

food_details.html

<!doctype html>
<title>{{food.name}}</title>

<a href="/">Retour à la liste des aliments</a>

<h1>{{food.name}}</h1>

Décorateurs

def timeit(func, *args, **kwargs):
    def inner(*args, **kwargs):
        import time
        start_time = time.clock()
        returned_value = func(*args, **kwargs)
        duration = time.clock() - start_time
        print("duration: {} microseconds".format(int(duration*1000000)))
        return returned_value
    return inner


def memoize(f):
    cache = {}
    def inner(x):
        # print('cache', cache)
        if x not in cache:            
            cache[x] = f(x)
        return cache[x]
    return inner

Générateurs

def fibonacci():
    last = 1
    beforeLast = 1
    yield beforeLast

    while True:
        beforeLast, last = last, last + beforeLast
        yield beforeLast

def harmonic():
    n = 1
    sum = 0
    while True:
        sum += 1/n
        yield sum
        n += 1
def read_in_chunks(file, chunk_size=1024):
    while True:
        data_chunk = file.read(chunk_size)
        if not data_chunk:
            break
        yield data_chunk

Multithreading et multiprocessing

from concurrent.futures import ThreadPoolExecutor, as_completed

def fib(n):
    if n < 2:
        return 1
    else:
        return fib(n-1) + fib(n-2)


with ThreadPoolExecutor(max_workers=3) as executor:
    future1 = executor.submit(fib, 30)
    future2 = executor.submit(fib, 20)
    future3 = executor.submit(fib, 15)
    for future in as_completed([future1, future2, future3]):
        print(future.result())
from concurrent.futures import ProcessPoolExecutor, as_completed

def fib(n):
    if n < 2:
        return 1
    else:
        return fib(n-1) + fib(n-2)


with ProcessPoolExecutor(max_workers=3) as executor:
    future1 = executor.submit(fib, 40)
    future2 = executor.submit(fib, 20)
    future3 = executor.submit(fib, 15)
    for future in as_completed([future1, future2, future3]):
        print(future.result())

asyncio

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import argparse
from urllib.parse import urljoin, urlsplit
import aiohttp
import bs4
import asyncio

# IMPORTANT : le lancer au départ avec https://educ-a-dom.fr/brklnk/index.html et non https://educ-a-dom.fr/brklnk
# sinon problèmes avec urljoin

async def print_broken_links(url, depth, already_visited):
    if depth >= 0 and not url in already_visited:
        try:
            already_visited.add(url)
            async with aiohttp.ClientSession() as session:
                async with session.get(url) as response:
                    if response.status >= 400:
                        print("broken link: {0}".format(url))
                    else:
                        html = await response.text()
                        html_parser = bs4.BeautifulSoup(html, 'html.parser')
                        # collect links
                        links = html_parser.find_all('a')
                        for link in links:
                            href = link.get('href', None)
                            if href:
                                # search recursively
                                link_url = urljoin(url, href)
                                await print_broken_links(link_url, depth-1, already_visited)
        except Exception as ex:
            print(str(ex))
            #print("invalid link: {0}".format(url))


def main():
    # create parser
    parser = argparse.ArgumentParser()
    parser.add_argument("url", type=str, help="url to check")
    parser.add_argument("-d", "--depth", type=int, default=1, help="search depth (default: 1)")

    # parse command arguments
    args = parser.parse_args()

    # start search
    already_visited = set()
    loop = asyncio.get_event_loop()
    loop.run_until_complete(print_broken_links(args.url, args.depth, already_visited))
    # asyncio.run(print_broken_links(args.url, args.depth, already_visited))
    
if __name__ == '__main__':
    main()

Websockets

Serveur

#!/usr/bin/env python

import asyncio
import websockets

async def echo(websocket, path):
    async for message in websocket:
        print('Received:', message)
        await websocket.send("Hello {}!".format(message))

asyncio.get_event_loop().run_until_complete(
    websockets.serve(echo, 'localhost', 8765)
)

asyncio.get_event_loop().run_forever()

Client

#!/usr/bin/env python

import asyncio
import websockets
import sys

async def hello(uri, name):
    async with websockets.connect(uri) as websocket:
        await websocket.send(name)
        message = await websocket.recv()
        print('Received:', message)

asyncio.run(hello('ws://localhost:8765', sys.argv[1]))

flake8 & black

Pour une installation globale de flake8

~/.config/flake8

[flake8]
ignore = E226,E302,E41
max-line-length = 160
exclude = tests/*
max-complexity = 10

.git/hooks/pre-commit

#!/bin/sh
  
echo "running flake8"
flake8 . --exclude=venv/

Context manager

import json
import os

class JSONFile(object):

    def __init__(self, file_name):
        self.file_obj = open(file_name, 'r')
        self.json_dict = json.loads(self.file_obj.read())

    def __enter__(self):
        return self.json_dict

    def __exit__(self, type, value, traceback):
        self.file_obj.close()

file_name = os.path.join(os.path.dirname(__file__), 'test.json')
with JSONFile(file_name) as dict:
    print(dict)

Regex

import re

regex = r'^[ \t]*([rcl]\w+)[ \t]+(\w+)[ \t]+(\w+)(?:[ \t]+(\w+))?(?:[ \t]+(\w+))?[ \t]*(;.*)?$'
compiled_element_regex = re.compile(regex, re.IGNORECASE | re.ASCII)

text = "R11 xx yy opt1 opt2 ; comment"

match = compiled_element_regex.match(text)

if match:
    print('Match found')
    for group in match.groups():
        print(group)
else:
    print('No match')

PEG parsing & Graphviz

NumExpr, version 1


from parsimonious.grammar import Grammar

from parsimonious.nodes   import NodeVisitor



grammar = Grammar(

    r"""

        expr            = term COMMENT ?

        term            = factor add_term ?

        add_term        = (PLUS term) / (MINUS term)

        factor          = terminal product_factor ?

        product_factor  = (MULTIPLY factor) / (DIVIDE factor)

        terminal        = INTEGER / parent

        parent          = OPEN expr CLOSE

        INTEGER         = ~"\s*\d+\s*"

        OPEN            = ~"\s*[(]\s*"

        CLOSE           = ~"\s*[)]\s*"

        PLUS            = ~"\s*\+\s*"

        MINUS           = ~"\s*\-\s*"

        MULTIPLY        = ~"\s*\*\s*"

        DIVIDE          = ~"\s*\/\s*"

        COMMENT         = ~"\s*//.*$"

    """

)



more = r"""

    """



class ExprVisitor(NodeVisitor):



    def visit_expr(self, node, children):

        if(children[1]):

            print('comment = "{}"'.format(children[1][0]))

        return children[0]



    def visit_term(self, node, children):

        if children[1]:

            return children[0] + children[1][0]

        else:

            return children[0]



    def visit_add_term(self, node, children):

        operation = children[0][0]

        if operation == '+':

            return children[0][1]

        else:

            return -children[0][1]



    def visit_factor(self, node, children):

        if children[1]:

            return children[0] * children[1][0]

        else:

            return children[0]



    def visit_product_factor(self, node, children):

        operation = children[0][0]

        if operation == '*':

            return children[0][1]

        else:

            return 1/children[0][1]



    def visit_terminal(self, node, children):

        return children[0]



    def visit_parent(self, node, children):

        return children[1]

    #

    # tokens

    #

    def visit_OPEN(self, node, children):

        return '('



    def visit_CLOSE(self, node, children):

        return ')'



    def visit_PLUS(self, node, children):

        return '+'



    def visit_MINUS(self, node, children):

        return '-'



    def visit_MULTIPLY(self, node, children):

        return '*'



    def visit_DIVIDE(self, node, children):

        return '/'



    def visit_INTEGER(self, node, children):

        return int(node.text)



    def visit_COMMENT(self, node, children):

        return node.text



    #

    # required visitor

    #

    def generic_visit(self, node, children):

        return children



tree = grammar.parse("(1+(2))*3 // this is a comment...")

visitor = ExprVisitor()

output = visitor.visit(tree)

print('expr = ', output)

NumExpr, version 2

Expression
  = Term OtherTerms

OtherTerms
  = (_ ("+" / "-") _ Term)*

Term
  = Factor OtherFactors

OtherFactors
  = (_ ("*" / "/") _ Factor)*

Factor
  = Integer / ("(" _ Expression _ ")")

Integer
  = ~"\s*\d+\s*"
_
  = ~"\s*"


import os
from functools import reduce 

from parsimonious.grammar import Grammar
from parsimonious.nodes import NodeVisitor

class NumExprVisitor(NodeVisitor):
    def visit_Expression(self, node, visited_children):
        return visited_children

    def visit_Term(self, node, visited_children):
        return visited_children

    def generic_OtherTerms(self, node, visited_children):
        sum = visited_children[0]
        for i in visited_children[1]:
            sum += i
        return sum

    def visit_Factor(self, node, visited_children):
        return visited_children[0]

    def generic_OtherFactors(self, node, visited_children):
        prod = visited_children[0]
        for i in visited_children[1]:
            prod *= i
        return prod

    def visit_Integer(self, node, visited_children):
        return int(node.text)

    def generic_visit(self, node, visited_children):
        """ The generic visit method. """
        return visited_children[0] if visited_children else visited_children


grammar_file_path = os.path.join(os.path.dirname(__file__), 'numexpr.peg')
with open(grammar_file_path) as grammar_file:

    try:
        grammar = Grammar(grammar_file.read())
        tree = grammar.parse("1+2*3")

        visitor = NumExprVisitor()
        output = visitor.visit(tree)
        print(output)

    except Exception as ex:
        print(ex)

Avec Graphviz

from parsimonious.grammar import Grammar
from parsimonious.nodes import NodeVisitor

from graphviz import Digraph

import os

def main():
    try:
        grammar_file_path = os.path.join(os.path.dirname(__file__), 'netlist_grammar.peg')
        with open(grammar_file_path, 'r') as grammar_file:
            grammar_text = grammar_file.read()
            parser = Grammar(grammar_text)

            data_file_path = os.path.join(os.path.dirname(__file__), 'rc_circuit.cir')
            with open(data_file_path, 'r') as data_file:
                data_text = data_file.read()

                tree = parser.parse(data_text)

                dot = Digraph(comment='RC circuit', format='pdf')

                visitor = NetlistVisitor(dot)
                output = visitor.visit(tree)
                print(output)

                dot.render('circuit.gv', view=True)
    except Exception as ex:
        print(ex)


class NetlistVisitor(NodeVisitor):
    def __init__(self, dot):
        self.dot = dot

    def visit_netlist(self, node, visited_children):
        return {
            "title": visited_children[0],
            "elements" : visited_children[1],
        }
    def visit_title(self, node, visited_children):
        return node.text.lstrip()

    def visit_header(self, node, visited_children):
        return visited_children[1]

    def visit_commentline(self, node, visited_children):
        return None

    def visit_elements(self, node, visited_children):
        return [ elt[1] for elt in visited_children]

    def visit_lettre(self, node, visited_children):
        return node.text

    def visit_name(self, node, visited_children):
        return visited_children[0]

    def visit_node(self, node, visited_children):
        self.dot.node(visited_children[0], visited_children[0])
        return visited_children[0]

    def visit_word(self, node, visited_children):
        return node.text

    def visit_blanks(self, node, visited_children):
        return None

    def visit_element(self, node, visited_children):
        componentName = visited_children[1] + visited_children[2]
        n1 = visited_children[4]
        n2 = visited_children[6]
        mname = visited_children[8]
        self.dot.node(n1, n1, shape='point')
        self.dot.node(n2, n2, shape='point')
        self.dot.node(componentName, componentName, shape='box')
        self.dot.edge(componentName, n1)
        self.dot.edge(componentName, n2)
        return {
            'letter': visited_children[1],
            'name': visited_children[2],
            'n1': n1,
            'n2': n2,
            'mname': mname,
        }

    def visit_end(self, node, visited_children):
        return None

    def generic_visit(self, node, visited_children):
        return visited_children or node

if __name__ == '__main__':
    main()

netlist_grammar.peg


netlist
  = header elements end

header
  = commentline* title

title
  = ~r"\n*[^*.].*"

commentline
  = ~r"\n*\*.*"

elements
  = (commentline* element)*

element
  = "\n" lettre name blanks node blanks node blanks word

name
  = word

node
  = word

lettre
  = ~r"[rRcClL]"

word
  = ~r"\w+"

blanks
  = ~r" +"

end
  = ~"\n*\.end\s*"

rc_circuit.cir

* comment1
* comment2
RC circuit
r1 0 1 10k
c1 1 2 100pF
* comment2
L4 3 0 4mH
.end

Weasyprint

from weasyprint import HTML, CSS
from weasyprint.fonts import FontConfiguration

font_config = FontConfiguration()

html = HTML(string='<h1>The title</h1>')

css = CSS(
    string='''
        @page { size: A4; margin: 1cm }
        h1 { font-family: Courier }
    ''',
    font_config=font_config,
)

html.write_pdf(
    './example.pdf',
    stylesheets=[css],
    font_config=font_config,
)