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 à pasAjouter le nombre N en paramètre de
sigma.py
et créer une configuration d’exécution avec un argument N=5Créer un environnement virtuel
venv
pour Python 3, puis créer une configuration d’exécution dans cet environnement pour un programmeversion.py
qui affiche le numéro de version de l’interpréteur Python (voirsys.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 recherchen
(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 modulesseries
etsequences
- Dans
sequences
, ajouter le générateurfibonacci
; dansseries
, ajouterharmonic
- 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,
)