def trace(f):
def traced(*args, **kwargs):
print ('>>')
f(*args, **kwargs)
print ('<<')
return traced
@trace
def f1(truc):
print ('truc:', truc)
@trace
def f2(x, y):
print ('x:', x, 'y:', y)
f1((x, y))
f1(3)
>> truc: 3 <<
f2('bla',666)
>> x: bla y: 666 >> truc: ('bla', 666) << <<
Permet d'automatiser le procédé, déjà illustré sur l'exemple de la suite de Fibonacci.
Sans décorateur :
def f(n):
if n<2: return n
return f(n-1)+f(n-2)
[f(n) for n in range(15)]
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377]
cache = {0:0, 1:1}
def f(n):
try: return cache[n]
except KeyError:
cache[n] = f(n-1)+f(n-2)
return cache[n]
f(2000)
4224696333392304878706725602341482782579852840250681098010280137314308584370130707224123599639141511088446087538909603607640194711643596029271983312598737326253555802606991585915229492453904998722256795316982874482472992263901833716778060607011615497886719879858311468870876264597369086722884023654422295243347964480139515349562972087652656069529806499841977448720155612802665404554171717881930324025204312082516817125
Avec :
def memoize(f):
cache = {}
def memoized(*args):
try:
return cache[args]
except KeyError:
result = cache[args] = f(*args)
return result
return memoized
@memoize
def fib(n):
if n<2: return n
return fib(n-1)+fib(n-2)
fib(1000)
43466557686937456435688527675040625802564660517371780402481729089536555417949051890403879840079255169295922593080322634775209689623239873322471161642996440906533187938298969649928516003704476137795166849228875
for i in range(1,11): print (fib(i*100),'\n')
354224848179261915075 280571172992510140037611932413038677189525 222232244629420445529739893461909967206666939096499764990979600 176023680645013966468226945392411250770384383304492191886725992896575345044216019675 139423224561697880139724382870407283950070256587697307264108962948325571622863290691557658876222521294125 110433070572952242346432246767718285942590237357555606380008891875277701705731473925618404421867819924194229142447517901959200 87470814955752846203978413017571327342367240967697381074230432592527501911290377655628227150878427331693193369109193672330777527943718169105124275 69283081864224717136290077681328518273399124385204820718966040597691435587278383112277161967532530675374170857404743017623467220361778016172106855838975759985190398725 54877108839480000051413673948383714443800519309123592724494953427039811201064341234954387521525390615504949092187441218246679104731442473022013980160407007017175697317900483275246652938800 43466557686937456435688527675040625802564660517371780402481729089536555417949051890403879840079255169295922593080322634775209689623239873322471161642996440906533187938298969649928516003704476137795166849228875
En Python 3, le module functools
exporte un décorateur, @lru_cache
,
qui construit un cache LRU.
from functools import lru_cache
@lru_cache()
def fib2(n):
if n<2: return n
return fib2(n-1)+fib2(n-2)
for i in range(1,11): print (fib2(i*100),'\n')
354224848179261915075 280571172992510140037611932413038677189525 222232244629420445529739893461909967206666939096499764990979600 176023680645013966468226945392411250770384383304492191886725992896575345044216019675 139423224561697880139724382870407283950070256587697307264108962948325571622863290691557658876222521294125 110433070572952242346432246767718285942590237357555606380008891875277701705731473925618404421867819924194229142447517901959200 87470814955752846203978413017571327342367240967697381074230432592527501911290377655628227150878427331693193369109193672330777527943718169105124275 69283081864224717136290077681328518273399124385204820718966040597691435587278383112277161967532530675374170857404743017623467220361778016172106855838975759985190398725 54877108839480000051413673948383714443800519309123592724494953427039811201064341234954387521525390615504949092187441218246679104731442473022013980160407007017175697317900483275246652938800 43466557686937456435688527675040625802564660517371780402481729089536555417949051890403879840079255169295922593080322634775209689623239873322471161642996440906533187938298969649928516003704476137795166849228875
import time
def timeit(f):
def timed(*args, **kw):
ts = time.time()
result = f(*args, **kw)
te = time.time()
print ('%r (%r, %r) %2.2f sec' % (f.__name__, args, kw, te-ts))
return result
return timed
@timeit
def g(t):
print ('début')
time.sleep(t)
print ('fin')
g(3.145926535)
début fin 'g' ((3.145926535,), {}) 3.15 sec
class C(object):
@timeit
def __init__(self):
time.sleep(2.718281828)
print ('Fini !')
c=C()
Fini ! '__init__' ((<__main__.C object at 0x7f88e81ba710>,), {}) 2.72 sec
def require_int(f):
def wrapper (arg):
assert isinstance(arg, int)
return f(arg)
return wrapper
@require_int
def h(n):
print (n, " est un entier.")
h(42)
42 est un entier.
h(2.71828)
--------------------------------------------------------------------------- AssertionError Traceback (most recent call last) <ipython-input-33-aa7d89a8c751> in <module> ----> 1 h(2.71828) <ipython-input-31-e93f4cb037ac> in wrapper(arg) 1 def require_int(f): 2 def wrapper (arg): ----> 3 assert isinstance(arg, int) 4 return f(arg) 5 return wrapper AssertionError:
Les décorateurs peuvent être empilés :
@timeit
@require_int
def rien(n):
time.sleep(n)
print ('Fini')
rien(3)
Fini 'wrapper' ((3,), {}) 3.00 sec
rien(3.0)
--------------------------------------------------------------------------- AssertionError Traceback (most recent call last) <ipython-input-36-11fdd05a28be> in <module> ----> 1 rien(3.0) <ipython-input-28-a9c27c89792e> in timed(*args, **kw) 4 def timed(*args, **kw): 5 ts = time.time() ----> 6 result = f(*args, **kw) 7 te = time.time() 8 print ('%r (%r, %r) %2.2f sec' % (f.__name__, args, kw, te-ts)) <ipython-input-31-e93f4cb037ac> in wrapper(arg) 1 def require_int(f): 2 def wrapper (arg): ----> 3 assert isinstance(arg, int) 4 return f(arg) 5 return wrapper AssertionError:
On remarque que l'erreur est attribuée à la fonction wrapper
. Si on n'a pas sous les yeux le code des décorateurs utilisés,
on peut chercher longtemps l'origine du problème ...
pytyhon
@dec(argA, argB)
def f(arg1, arg2):
pass
est équivalent à
def f(arg1, arg2):
pass
f = dec(argA, argB)(f)
C'est donc équivalent à créer une fonction composée
f = dec(argA, argB)(f)
Autrement dit, dec(argA, argB)
doit être un décorateur.
def add_attr(val):
def decorated(f):
f.attribute = val
return f
return decorated
@add_attr('Nouvel attribut')
def f():
pass
f.attribute
'Nouvel attribut'
def return_bool(bool_value):
def wrapper(func):
def wrapped(*args):
result = func(*args)
if result != bool_value:
raise TypeError
return result
return wrapped
return wrapper
@return_bool(True)
def always_true():
return True
@return_bool(False)
def always_false():
return True
always_true()
True
always_false()
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-41-644ede37e897> in <module> ----> 1 always_false() <ipython-input-39-d96151da2fbc> in wrapped(*args) 4 result = func(*args) 5 if result != bool_value: ----> 6 raise TypeError 7 return result 8 return wrapped TypeError:
La seule contrainte sur l'objet retourné par un décorateur est qu'il se comporte comme une fonction (duck typing), autrement dit qu'il soit callable.
C'est le cas de toute classe possédant la méthode spéciale __call__
.
class MyDecorator(object):
def __init__(self, f):
# faire quelquechose avec f ...
def __call__(*args):
# faire autre chose avec args
class Memoized(object):
def __init__(self, f):
self.f = f
self.cache = {}
def __call__(self, *args):
if args in self.cache:
return self.cache[args]
else:
value = self.f(*args)
self.cache[args] = value
return value
def __repr__(self):
'''Return the function's docstring.'''
return self.f.__doc__
def __str__(self): return 'toto'
@Memoized
def lucas(n):
"Calcule la suite de Lucas"
if n==0: return 2
elif n==1: return 1
else: return lucas(n-1)+lucas(n-2)
print ([lucas(i) for i in range(50)])
[2, 1, 3, 4, 7, 11, 18, 29, 47, 76, 123, 199, 322, 521, 843, 1364, 2207, 3571, 5778, 9349, 15127, 24476, 39603, 64079, 103682, 167761, 271443, 439204, 710647, 1149851, 1860498, 3010349, 4870847, 7881196, 12752043, 20633239, 33385282, 54018521, 87403803, 141422324, 228826127, 370248451, 599074578, 969323029, 1568397607, 2537720636, 4106118243, 6643838879, 10749957122, 17393796001]
print(fib)
<function memoize.<locals>.memoized at 0x7f88e84b28c0>
lucas
Calcule la suite de Lucas
print(lucas)
toto
class countcalls(object):
def __init__(self, func):
self.__func = func
self.__numcalls = 0
def __call__(self, *args, **kwargs):
self.__numcalls += 1
return self.__func(*args, **kwargs)
def count(self):
return self.__numcalls
@countcalls
def p(): print ('*',end=' ')
@countcalls
def q(): print ('+')
for i in range(5):
p()
for i in range(4):
p();q()
* * * * * * + * + * + * +
p.count(), q.count()
(9, 4)
On a déjà vu la différence entre les variables des classes et celles des instances :
class A(object):
c = 0
def __init__(self):
self.c +=1
class B(object):
c = 0
def __init__(self):
B.c +=1
a=A(); b=A()
print (a.c, b.c, A.c)
1 1 0
x=B(); y=B()
print (x.c, y.c, B.c)
2 2 2
Les méthodes normales sont des méthodes d'instances.
Leur premier argument doit être l'instance elle-même, conventionnellement
appelée self
.
Il existe
aussi des méthodes de classes.
On les définit comme les méthodes d'instance,
leur premier argument est alors la classe
elle-même, conventionnellement appelée cls
, puis on les
passe à la fonction classmethod
:
class ASimpleClass(object):
description = 'a simple class'
def show_class(cls, msg):
print ('%s: %s' % (cls.description , msg, ))
show_class = classmethod(show_class)
C'est plus clair avec un décorateur :
class ASimpleClass(object):
description = 'a simple class'
@classmethod
def show_class(cls, msg):
print ('%s: %s' % (cls.description , msg, ))
Par exemple, la classe B
qui tient un compte de ses instances pourrait s'écrire
class B(object):
c = 0
def __init__(self):
B.c += 1
@classmethod
def compte_instances(cls):
print ('instances : %d' % (cls.c, ))
Une méthode statique ne prend ni une instance ni la classe comme premier paramètre.
Elle se définit à l'aide de la fonction staticmethod
ou du décorateur
@staticmethod
.
class B(object):
c = 0
def __init__(self):
B.c += 1
@staticmethod
def compte_instances():
print ('instances : %d' % (B.c, ))
a=B(); b=B(); c=B()
B.compte_instances()
instances : 3
itertools
¶Rappel :
for x in <quelquechose>
quelquechose
peut être une liste, un tuple, une chaîne, un dictionnaire, un fichier ouvert, un ensemble ...__iter__
iter
next
ll = [1, 2, 3, 4, 5]
it = ll.__iter__()
next(it)
1
next(it)
2
Normalement, on écrit plûtôt
ll = [1, 2, 3, 4, 5]
it = iter(ll)
it
<list_iterator at 0x7f88e85cebd0>
list(it)
[1, 2, 3, 4, 5]
list(it)
[]
On peut définir des classes qui supportent l'itération : il suffit
d'implémenter les méthodes __iter__
et __next__
.
Les boucles longues sont peu efficaces et sont une des principales causes de lenteur en Python.
Pour des boucles sur les entiers, en Python 2, on utilisera xrange
(un générateur
écrit directement en C)
plutôt que range
.
Pour des itérations plus compliquées, on pourra utiliser le module itertools
,
qui propose des version optimisées d'opérations courantes, et de nombreuses fonctionnalités
commodes.
from itertools import *
it=chain(range(5),range(5,-1,-1))
list(it)
[0, 1, 2, 3, 4, 5, 4, 3, 2, 1, 0]
it=zip(range(5),range(5,-1,-1))
list(it)
[(0, 5), (1, 4), (2, 3), (3, 2), (4, 1)]
count(start,step=1)
engendre les entiers à partir de start
avec les pas step
.
La fonction islice(iterable,[start],stop,[step])
remplace iterable[start:stop:step]
:
it=islice(count(5),7,17,2)
list(it)
[12, 14, 16, 18, 20]
Avec les mots binaires de 5 digits, on pourrait faire
['01' for i in range(5)]
['01', '01', '01', '01', '01']
list(product('01','01','01'))
[('0', '0', '0'), ('0', '0', '1'), ('0', '1', '0'), ('0', '1', '1'), ('1', '0', '0'), ('1', '0', '1'), ('1', '1', '0'), ('1', '1', '1')]
list(map(bin,range(16,24)))
['0b10000', '0b10001', '0b10010', '0b10011', '0b10100', '0b10101', '0b10110', '0b10111']
words = product(*['01' for i in range(5)])
ll=islice(words,16,24)
list(ll)
[('1', '0', '0', '0', '0'), ('1', '0', '0', '0', '1'), ('1', '0', '0', '1', '0'), ('1', '0', '0', '1', '1'), ('1', '0', '1', '0', '0'), ('1', '0', '1', '0', '1'), ('1', '0', '1', '1', '0'), ('1', '0', '1', '1', '1')]
La fonction product
renvoie
le produit cartésien (les tuples) d'un nombre arbitraire d'itérables.
On peut s'en servir pour construire les mots de longueur donnée sur un alphabet, comme dans ce craqueur de mots de passe basique :
from crypt import crypt
def words(alphabet,length):
return product(*[alphabet for i in range(length)])
def crack(hash_, salt, alphabet,length):
ww = words(alphabet,length)
for w in ww:
p = crypt(''.join(w),salt)
if p == hash_:
print ('Password found: ', ''.join(w))
return ''.join(w)
from string import ascii_lowercase
pw = 'gabu'
slt ='XY'
h = crypt(pw,slt)
h
'XYhc/C.XeLVM6'
crack(h,'XY',ascii_lowercase,4)
Password found: gabu
'gabu'
Ce n'est pas très efficace (!) mais c'est simple ...
La fonction tee(iterateur,n=2)
retourne $n$ copies
identiques de l'itérateur :
it = islice(count(), 5)
i1, i2, i3 = tee(it,3)
[(next(i1), next(i2),next(i3)) for i in range(5)]
[(0, 0, 0), (1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4)]
list(it) # entièrement consommé
[]
La fonction starmap
fonctionne comme map
, mais
calcule f(*i)
:
it = zip('abcd', range(1,5))
ff = starmap(lambda x,y: x*y,it)
list(ff)
['a', 'bb', 'ccc', 'dddd']
La fonction cycle
répète indéfiniment un itérateur fini :
it = cycle('bla')
[next(it) for i in range(12)]
['b', 'l', 'a', 'b', 'l', 'a', 'b', 'l', 'a', 'b', 'l', 'a']
La fonction repeat
fait ce qu'on imagine :
it=repeat('bla',4)
list(it)
['bla', 'bla', 'bla', 'bla']
On l'utilise en combinaison avec map
ou zip
:
it = zip(range(5), repeat(2), 'abcd')
list(it)
[(0, 2, 'a'), (1, 2, 'b'), (2, 2, 'c'), (3, 2, 'd')]
Le filtrage s'effectue au moyen des fonctions
dropwhile, takewhile, filter, filterfalse
:
it=takewhile(lambda x:x*x<100, count())
list(it)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
it=dropwhile(lambda x:x<0, range(-6,5))
list(it)
[0, 1, 2, 3, 4]
it=filter(lambda x:x%2, range(10))
list(it)
[1, 3, 5, 7, 9]
it=filterfalse(lambda x:x%2, range(10))
list(it)
[0, 2, 4, 6, 8]
Plus complexe : groupby
construit un itérateur qui renvoie
les clés et groupes consécutifs d'un itérable.La clé key
est une fonction qui calcule une valeur sur chaque élément. Par défaut,
c'est l'identité.
On pourra donc écrire
ll = [1,2,2,2,1,1,4,4,5,5,2,2,1,1,3,3,1,1]
it=groupby(ll)
[k for k,g in it]
[1, 2, 1, 4, 5, 2, 1, 3, 1]
Le résultat complet serait
it=groupby(ll)
[(k, list(g)) for k,g in it]
[(1, [1]), (2, [2, 2, 2]), (1, [1, 1]), (4, [4, 4]), (5, [5, 5]), (2, [2, 2]), (1, [1, 1]), (3, [3, 3]), (1, [1, 1])]
ll = [('a',1), ('b',2), ('c',2), ('d',1), ('e',2), ('f',1)]
it=groupby(ll, lambda x: x[1])
[(k, list(map(lambda y:y[0],g)) ) for k,g in it]
[(1, ['a']), (2, ['b', 'c']), (1, ['d']), (2, ['e']), (1, ['f'])]
Finalement, on dispose de quelques fonctions combinatoires basiques :
it=combinations('abcd',2)
list(it)
[('a', 'b'), ('a', 'c'), ('a', 'd'), ('b', 'c'), ('b', 'd'), ('c', 'd')]
it=combinations_with_replacement('abcd',2)
list(it)
[('a', 'a'), ('a', 'b'), ('a', 'c'), ('a', 'd'), ('b', 'b'), ('b', 'c'), ('b', 'd'), ('c', 'c'), ('c', 'd'), ('d', 'd')]
it=permutations(range(1,4))
list(it)
[(1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), (3, 2, 1)]
Le module operator
propose des fonctions optimisées
pour remplacer les opérateurs standards de Python (ex. add(x,y)
).
Le module collections
fournit des structures de données
hautes performances pour remplacer dict, list, set, tuple
:
namedtuple(), deque, Counter, OrderedDict, defaultdict
.
Le module array
fournit des tableaux optimisés pour
des types de données basiques (caractères, entiers, flottants ...)
ctypes
¶Il permet d'utiliser des bibliothèques partagées, avec des types de données compatibles au C.
from ctypes import *
libc = cdll.LoadLibrary("libc.so.6")
printf=libc.printf
printf(b"%s\n", b"Hello world!") # noter le b"" nécessaire pour python 3
13
...
[I 08:28:51.990 NotebookApp] Saving file at /python3_M1_2020-5.ipynb
Hello world!
[I 08:38:52.001 NotebookApp] Saving file at /python3_M1_2020-5.ipynb
...
Le résultat s'est affiché sur le terminal dans lequel on a lancé l'interface graphique ...
Dans un terminal, on obtiendrait
>>> from ctypes import *
>>> libc = cdll.LoadLibrary("libc.so.6")
>>> printf=libc.printf
>>> printf(b"%s\n", b"Hello world!")
Hello world!
13
>>>
print (libc.time(None))
1604481104
Pour de longues itérations, on peut écrire en C la fonction critique et la compiler sous forme dll/shared object
Un petit test de performances :
/* rien.c
compiler avec
gcc -Wall -fPIC -c rien.c
gcc -shared -Wl,-soname,librien.so.1 -o librien.so.1.0 *.o
*/
int rien(int n){
int i=0;
while (1==1) {
i++;
if(i>n){ return(i); }
}
}
from ctypes import *
cdll.LoadLibrary("./librien.so.1.0")
librien=CDLL("./librien.so.1.0")
def rien(n):
i=0
while 1==1:
i+=1
if i>n: return i
from time import time
print ("Avec le C :")
a=time()
librien.rien(10000000)
print (time()-a)
print ("En pur Python :")
a=time()
rien(10000000)
print (time()-a)
Avec le C : 0.021549701690673828 En pur Python : 0.5683567523956299
Pour étendre Python avec du code C ou C++, il vaut mieux utiliser Cython
Après avoir installé Cython, on peut reprendre l'exemple précédent. On crée un fichier rien.pyx
# rien.pyx`
def rien(int n):
cdef int i=0
while 1==1:
i+=1
if i>n: return i
Le code est le même, à ceci près que les types int
de n
et i
ont été déclarés.
Pour compiler, il faut, dans
le même répertoire, un fichier setup.py
structuré ainsi :
# setup.py
from distutils.core import setup
from Cython.Build import cythonize
setup( ext_modules = cythonize("rien.pyx") )
On compile avec la commande
python setup.py build_ext --inplace
On peut alors importer rien
comme un module ordinaire
from time import time
import rien
def pyrien(n):
i=0
while 1==1:
i+=1
if i>n: return i
print ("Avec le C :")
a=time()
rien.rien(10000000)
print (time()-a)
print ("En pur Python :")
a=time()
pyrien(10000000)
print (time()-a)
Avec le C : 0.0011870861053466797 En pur Python : 0.5563614368438721
0.5563614368438721/0.0011870861053466797
468.6782486443061
468 fois plus rapide ici, donc. Notons au passage que si on n'avait pas déclaré les types, l'effet aurait été beaucoup moins bon (un facteur 2).