#!/usr/bin/env python
# coding: utf-8
# # Les Finesses de Python
#
# ## Microclub 1er juin 2018
#
#
#
#
# ## Philippe Guglielmetti
#
#
#
# ---
# ## Encore Python ???
#
# > Il ne vaut pas la peine de connaître un langage qui ne modifie pas votre façon de penser la programmation.
# > ([Alan Perlis](https://www.drgoulu.com/2008/01/21/perlisismes-les-dictons-informatiques-dalan-perlis/))
#
# * #4 au [TIOBE](https://www.tiobe.com/tiobe-index/) (nombre de développeurs)
# * #2 au [Madnight] (https://madnight.github.io/githut/#/issues/2018/1) (activité sur GitHub)
# * #1 au [PYPL](http://pypl.github.io/PYPL.html) PopularitY of Programming Language (nombre de tutoriels suivis)
#
#
#
# >Python est un langage de programmation objet, multi-paradigme ([wikipedia](https://fr.wikipedia.org/wiki/Python_(langage)))
#
# = on peut programmer :
# 1. [comme une patate](http://entrian.com/goto/)
# 2. comme on en a l’habitude (classes, objets, ...)
# 3. comme on en a pas l’habitude
# 4. comme ~~un dieu~~ [Guido van Rossum](https://fr.wikipedia.org/wiki/Guido_van_Rossum)
#
# Notamment, Python intègre ou facilite certains [patron de conception](https://fr.wikipedia.org/wiki/Patron_de_conception)
#
# "Gang of four" (GOF) Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides
# “Design Patterns - Catalogue de modèles de conceptions réutilisables”
# Vuibert, 1999, 490 p
#
# ----
# ## Au menu ce soir :
# * [Objets et Types (Classes)¶](#objs)
# * [Iterateurs](#iter)
# * [Introspection](#intro)
# * [Les Décorateurs](#deco)
# * [La Memoïsation](#memo)
# * [Timeout](#stop)
# * [Intermède OEIS](#OEIS)
#
# ----
# mais commençons par importer quelques [librairies standard](https://docs.python.org/3/library/) et ["maison"](https://github.com/goulu/Goulib) utilisées dans la suite
# In[1]:
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
import itertools
import functools
import operator
from Goulib import itertools2, decorators
#
# ## Types et Objets (Classes)
#
# Python utilise un typage:
# * FORT : chaque variable désigne un objet dont le type est connu
# * DYNAMIQUE : mais une variable peut changer pour désigner un autre objet d'un autre type
# * PAS vérifié à la compilation (à moins que...)
#
# In[2]:
a=256
print(a,type(a)) # a désigne un OBJET de type (classe...) int
# In[3]:
print(255+1 == a, 255+1 is a) # == compare les valeurs, is compare les objets
print(a+1 == 257, a+1 is 257) # une idée de ce qui se passe ???
# In[4]:
langue_au_chat='! nohtyP ne sinifédérp stejbo sed tnos 652 à 5- ed sreitne sel'
''.join(reversed(langue_au_chat))
# In[5]:
s="a = "+str(a) # le str est obligatoire : pas de conversion implicite
print(s,type(s))
# In[6]:
a=str(a) # a ne change pas de type, il désigne un nouvel objet (nuance...)
print(a,type(a))
# In[7]:
def fib(n):
"""une très mauvaise implémentation de la suite de Fibonacci"""
if n < 1: return 0
if n < 2: return 1
return fib(n-1) + fib(n-2)
print(fib,type(fib)) # les fonctions sont des objets aussi
# In[8]:
class LateX (str): # on peut dériver une classe de n'importe quel type
def __init__(self, s=''): # constructeur
self = str(s) # utilise la méthode d'assignation de la classe str
def _repr_latex_(self): # affichage dans Jupyter Notebook
return r'$%s$'%self
# une méthode est juste un champ de type fonction
# les "magic methods" permettent de redéfinir les opérateurs
__add__=lambda self,right:LateX('{%s}+{%s}'%(self, right))
__sub__=lambda self,right:LateX('{%s}-{%s}'%(self, right))
__mul__=lambda self,right:LateX('{%s}{%s}'%(self, right))
__div__=lambda self,right:LateX('\\frac{%s}{%s}'%(self, right))
__truediv__= __div__ # nécessaire pour / au lieu de //
(LateX(2)*'x'+1)/2
# In[9]:
# on peut ajouter/modifier dynamiquement des méthodes à une classe
# puisque les méthodes sont aussi des objets !
def puissance_en_latex(self,right): # self n'est pas un mot réservé
return LateX('{%s}^{%s}'%(self, right))
LateX.__pow__=puissance_en_latex
LateX.version=1.1
LateX('\\pi')*LateX('r')**2
# In[10]:
LateX().version
# Oui c'est TRES dangereux. Mais un principe de Python est
# >"We are all consenting adults here."
# ----
#
# ## Iterateurs, générateurs, et programmation fonctionnelle
# L'itérateur est un patron de conception qui permet de parcourir tous les éléments contenus dans un autre objet, le plus souvent un conteneur (liste, arbre, etc). Un synonyme d'itérateur est curseur, notamment dans le contexte des bases de données.
# In[11]:
for i in range(10):
print(fib(i), end=" ")
# In[12]:
r=range(10)
print(r,type(r))
# In[13]:
[fib(i) for i in r] # construction de liste par "compréhension"
# In[14]:
res=map(fib,r) # map applique une fonction à un itérable
print(res,type(res)) # le résultat est un itérable
# In[15]:
functools.reduce(operator.add,res) # reduce applique répétitivement une fonction à 2 paramètres (opérateur) à un itérable
# sum(res) # aurait fait la même chose ...
# In[16]:
for obj in [1548, [1548], res, fib]:
print(obj, itertools2.isiterable(obj))
# In[17]:
def fibogen():
logging.info('fibogen démarre...')
n2,n1=0,1
yield n2
yield n1
while True: # oui, une boucle infinie dans une fonction ...
n1,n2=n1+n2,n1 # quand même pratique...
yield n1
print(fibogen(), itertools2.isiterable(fibogen()))
# In[18]:
e=enumerate(fibogen())
print(e, type(e), itertools2.isiterable(e))
# In[19]:
for i,n in e:
print(n, end=" ")
if i>=9: break
# In[20]:
res=itertools.islice(fibogen(),10)
print(res,type(res)) # le résultat est encore un itérable
# In[21]:
list(res)
# In[22]:
list(res) # mais attention ! un iterateur ne peut être "consommé" qu'une fois
# In[23]:
list(itertools.islice(fibogen(),10,20))
# In[24]:
class Fib:
'''Fibonacci avec classe !'''
def __iter__(self):
logging.info('Fib.__iter__')
self.n2, self.n1 = 0,1
return self
def __next__(self):
logging.info('Fib.__next__')
fib = self.n2
self.n2, self.n1 = self.n1, self.n1 + self.n2
return fib
f=Fib()
print(f, itertools2.isiterable(f)) # une classe contenant __iter__ (et __next__) est itérable !
# In[25]:
list(itertools.islice(f,10))
# ----
#
# ## Introspection
# Les champs+méthodes des objets peuvent être examinés.
# Et comme tout est objet ...
# In[26]:
print(dir(LateX)) # tous les membres de la classe (beaucoup sont hérités de str)
# In[27]:
print(dir(None)) # membres de l'objet None ?
# In[28]:
print(dir(fib)) # membres de l'objet fonction
# In[29]:
fib.__doc__ # pratique pour écrire des générateurs de doc ...
# In[30]:
import inspect # https://docs.python.org/3/library/inspect.html
inspect.getsourcelines(fib)
# In[31]:
fib.__code__ # intéressant ... on y reviendra !
# In[32]:
import math # un module est aussi un objet !
for name in math.__dict__: # traverse le dictionnaire de tout ce qui est défini dans le module
f=math.__dict__[name]
if callable(f): # si l'objet a une méthode __call__, on peut l'appeler
try:
try:
print(name+'()=',f())
continue
except TypeError:
pass
try:
print(name+'(1)=',f(1))
continue
except TypeError:
pass
print(name+'(1,2)=',f(1,2))
except:
print(name+'(???)')
#
# ## Les Décorateurs
#
#
# Un décorateur est une fonction qui modifie une fonction en l'enveloppant.
# C'est un [patron de conception](https://fr.wikipedia.org/wiki/Patron_de_conception)
#
# "Gang of four" (GOF) Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides
# “Design Patterns - Catalogue de modèles de conceptions réutilisables”
# Vuibert, 1999, 490 p
#
# ### Gestion du niveau de log pour debug
# In[33]:
import logging # librairie standard pour loguer
logger = logging.getLogger()
logger.setLevel(logging.INFO)
logging.error("un message d'erreur s'affiche")
logging.warning("un message d'avertissement s'affiche")
logging.info("un message d'info s'affiche")
logging.debug("mais un message de debug est inférieur au level INFO")
# In[34]:
def fonction():
"""cette fonction logue:
* son propre nom en INFO
* et sa doc en DEBUG
"""
logging.info(fonction.__name__)
def logdoc(doc):
for s in doc.split('\n'):
logging.debug(s)
logdoc(fonction.__doc__)
fonction() # ne logue que le nom puisqu'on est au level INFO
# In[35]:
@decorators.debug # passe en level DEBUG et ajoute des INFO à l'entrée et à la sortie
def fonction():
"""cette fonction logue:
* son propre nom
* et sa doc
"""
logging.debug(fonction.__name__)
def logdoc(doc):
for s in doc.split('\n'):
logging.debug(s)
logdoc(fonction.__doc__)
fonction()
# In[36]:
@decorators.debug # passe en level DEBUG et ajoute des INFO à l'entrée et à la sortie
def fonction():
"""cette fonction logue:
* son propre nom
* et sa doc
"""
logging.debug(fonction.__name__)
@decorators.nodebug # revenir en INFO pour réduire la taille du log
def logdoc(doc):
for s in doc.split('\n'):
logging.debug(s)
logdoc(fonction.__doc__)
fonction()
#
# ### Timeit
#
# écrivons un décorateur pour chronométrer une fonction
# In[37]:
import functools # librairie standard
def timeit(func):
@functools.wraps(func) # un décorateur qui simplifie l'écriture de décorateurs...
def wrapper(*args, **kwds):
import time
t=time.time()
f_result = func(*args, **kwds)
t=time.time()-t
params=', '.join(map(repr,args))
logger.info('%s(%s) took %f seconds'%(func.__name__,params,t))
return f_result
return wrapper
# In[38]:
def fib(n):
"""une très mauvaise implémentation de la suite de Fibonacci"""
if n < 2:
return 1
return fib(n-1) + fib(n-2)
@timeit
def tfib(n): return fib(n)
[tfib(i) for i in range(0,40,5)]
#
# ### La [Memoïsation](https://fr.wikipedia.org/wiki/M%C3%A9mo%C3%AFsation)
#
# Patron de conception qui accélère les appels répétés à une fonction en mémorisant les résultats en fonction des paramètres
# In[39]:
def memoize(obj): # disponible dans Goulib.decorators
cache = obj.cache = {} # simple dict. il existe des implémentations à mémoire limitée
@functools.wraps(obj)
def memoizer(*args, **kwargs):
key = str(args) + str(kwargs)
if key not in cache:
cache[key] = obj(*args, **kwargs)
return cache[key]
return memoizer
# In[40]:
@memoize
def mfib(n):
"""la même très mauvaise implémentation de la suite de Fibonacci"""
if n < 2:
return 1
return mfib(n-1) + mfib(n-2)
@timeit
def tfib(n): return mfib(n)
[tfib(i) for i in range(0,40,5)]
# In[41]:
tfib(500)
#
# ### Le Timeout
# In[42]:
t=3
i,n = 0,0
@decorators.timeout(t) # implémentation interrompant la thread
def fibloop():
global i,n
while True: # boucle infinie sans le décorateur ...
i=i+1
n=fib(i)
try:
fibloop()
except decorators.TimeoutError:
print('en %d secondes, on peut calculer %d termes'%(t,i))
print('le dernier est',n)
# In[43]:
import itertools
#implémentation au niveau de l'itérateur de boucle
# dommage, pas de @itimeout for ...
try:
for i in decorators.itimeout(itertools.count(),t):
n=mfib(i)
except decorators.TimeoutError:
print('en %d secondes, on peut calculer %d termes'%(t,i))
print('le dernier est',n)
#
# ### Hey mais alors ...
# In[44]:
class Valeur:
def __init__(self,v):
self.v=v
self.__name__=type(v).__name__
@property
def double(self):
return self.v*2
def type(self):
return self.__name__
@classmethod
def classe(self): # self désigne ici la classe !
return self.__name__
@staticmethod
def statique():
return 'statique'
v=Valeur('A')
print(v.classe(), v.type(), v.v, ", double =",v.double, v.statique())
# ... Oui ! certains décorateurs sont prédéfinis et étendent le langage !
#
# il n'y a pas (encore ?) de librairie standard de décorateurs, mais il y en a [pas mal ici](https://wiki.python.org/moin/PythonDecoratorLibrary)
#
# Guido van Rossum a par exemple proposé les "[multimethodes](https://www.artima.com/weblogs/viewpost.jsp?thread=101605)" :
# In[45]:
from fractions import Fraction
def f(a, b):
if isinstance(a, int) and isinstance(b, int):
return Fraction(a,b)
elif isinstance(a, float) and isinstance(b, float):
return Fraction(a/b)
elif isinstance(a, str) and isinstance(b, str):
return "%s / %s"%(a,b)
else:
raise TypeError("unsupported argument types (%s, %s)" % (type(a), type(b)))
print(f(2,3))
print(f(math.e,math.pi))
print(f('a','b'))
print(f('a',2))
# In[46]:
from Goulib.decorators import multimethod
@multimethod(int, int)
def foo(a, b):
return Fraction(a,b)
@multimethod(float, float)
def foo(a, b):
return Fraction(a/b)
@multimethod(str, str)
def foo(a, b):
return "%s / %s"%(a,b)
print(f(2,3))
print(f(math.e,math.pi))
print(f('a','b'))
print(f('a',2))