Python 5

Décorateurs

Un décorateur sert à envelopper un fonction dans une autre:

@dec
def f(arg1, arg2):
    pass

est équivalent à

def f(arg1, arg2):
    pass
f = dec(f)

dec prend une fonction comme argument et retourne une fonction.

Un petit test pour comprendre ce qui se passe :

In [1]:
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))
In [2]:
f1(3)
>>
truc: 3
<<

In [3]:
f2('bla',666)
>>
x: bla y: 666
>>
truc: ('bla', 666)
<<
<<

Exemple : mise en cache des valeurs d'une fonction récursive (memoization)

Permet d'automatiser le procédé, déjà illustré sur l'exemple de la suite de Fibonacci.

Sans décorateur :

In [4]:
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]
In [5]:
f(100)
Out[5]:
354224848179261915075L

Avec :

In [6]:
def memoize(f):
    cache = {}
    def memoized(*args):
        try:
            return cache[args]
        except KeyError:
            result = cache[args] = f(*args)
            return result
    return memoized
In [7]:
@memoize
def fib(n):
    if n<2: return n
    return fib(n-1)+fib(n-2)
In [8]:
fib(100)
Out[8]:
354224848179261915075L

En Python 3, le module functools exporte un décorateur, @lru_cache, qui construit un cache LRU.

Exemple : mesure du temps d'éxécution d'une fonction :

In [9]:
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
In [10]:
@timeit
def g(t):
    print 'début'
    time.sleep(t)
    print 'fin'
    
g(3.145926535)
début
fin
'g' ((3.145926535,), {}) 3.15 sec

In [11]:
class C(object):
    @timeit
    def __init__(self):
        time.sleep(2.718281828)
        print 'Fini !'

c=C()
Fini !
'__init__' ((<__main__.C object at 0x7f95eeb0de90>,), {}) 2.72 sec

Exemple : vérifier le type d'un argument :

In [12]:
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." 
In [13]:
h(42)
42  est un entier.

In [14]:
h(2.71828)
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-14-773bd444d310> in <module>()
----> 1 h(2.71828)

<ipython-input-12-860190342c41> 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 :

In [15]:
@timeit
@require_int
def rien(n):
    time.sleep(n)
    print 'Fini'
In [16]:
rien(3)
Fini
'wrapper' ((3,), {}) 3.00 sec

In [17]:
rien(3.0)
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-17-ce7123a5bd79> in <module>()
----> 1 rien(3.0)

<ipython-input-9-c0726b945d1e> 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-12-860190342c41> 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 ...

Décorateurs avec arguments

@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.

Exemple : ajouter un attribut à une fonction :

In [18]:
def add_attr(val):
    def decorated(f):
        f.attribute = val
        return f
    return decorated

@add_attr('Nouvel attribut')
def f():
    pass
In [19]:
f.attribute
Out[19]:
'Nouvel attribut'

Exemple : tester le type de la valeur retournée par une fonction :

In [20]:
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
In [21]:
always_true()
Out[21]:
True
In [22]:
always_false()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-22-230a9acc072d> in <module>()
----> 1 always_false()

<ipython-input-20-b1f407700193> in wrapped(*args)
      4             result = func(*args)
      5             if result != bool_value:
----> 6                 raise TypeError
      7             return result
      8         return wrapped

TypeError: 

Décorateurs définis par des classes

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
In [23]:
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__
In [24]:
@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)
In [25]:
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]

In [26]:
lucas
Out[26]:
Calcule la suite de Lucas

Exemple : permettre à une fonction de compter combien de fois elle a été appélée :

In [27]:
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
In [28]:
@countcalls
def p(): print '*',

@countcalls
def q(): print '+'

for i in range(5):
    p()

for i in range(4):
    p();q()
* * * * * * +
* +
* +
* +

In [29]:
p.count(), q.count()
Out[29]:
(9, 4)

Méthodes de classes et méthodes statiques

On a déjà vu la différence entre les variables des classes et celles des instances :

In [30]:
class A(object):
    c = 0
    def __init__(self):
        self.c +=1

class B(object):
    c = 0
    def __init__(self):
        B.c +=1
In [31]:
a=A(); b=A()
print a.c, b.c, A.c
1 1 0

In [32]:
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 :

In [33]:
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 :

In [34]:
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

In [35]:
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.

In [36]:
class B(object):
    c = 0
    def __init__(self):
        B.c += 1

    @staticmethod
    def compte_instances():
        print 'instances : %d' % (B.c, )
In [37]:
a=B(); b=B(); c=B()
B.compte_instances()
instances : 3

Itération, itérateurs et itertools

Rappel :

  • Syntaxe de l'itération : for x in <quelquechose>
  • quelquechose peut être une liste, un tuple, une chaîne, un dictionnaire, un fichier ouvert, un ensemble ...
  • Ces objets itérables possèdent une méthode spéciale __iter__
  • On l'appelle au moyen de la fonction iter
  • Elle retourne un itérateur qui possède une méthode next
In [38]:
ll = [1, 2, 3, 4, 5]
it = ll.__iter__()
it.next()
Out[38]:
1
In [39]:
it.next()
Out[39]:
2

Normalement, on écrit plûtôt

In [40]:
ll = [1, 2, 3, 4, 5]
it = iter(ll)
it
Out[40]:
<listiterator at 0x7f95eeacea10>
In [41]:
list(it)
Out[41]:
[1, 2, 3, 4, 5]

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.

On peut chainer des itérateurs :

In [42]:
from itertools import *

it=chain(range(5),range(5,-1,-1))
list(it)
Out[42]:
[0, 1, 2, 3, 4, 5, 4, 3, 2, 1, 0]

ou les tricoter ... (zip retourne une liste)

In [43]:
it=izip(range(5),range(5,-1,-1))
list(it)
Out[43]:
[(0, 5), (1, 4), (2, 3), (3, 2), (4, 1)]

Compteurs

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]:

In [44]:
it=islice(count(5),7,17,2)
list(it)
Out[44]:
[12, 14, 16, 18, 20]

Prooduit cartésien

Avec les mots binaires de 5 digits, on pourrait faire

In [45]:
words = product(*['01' for i in range(5)])
ll=islice(words,16,24)

list(ll)
Out[45]:
[('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 :

In [46]:
from crypt import crypt

def words(alphabet,length):
    return product(*[alphabet for i in range(length)])

def crack(password, salt, alphabet,length):
    ww = words(alphabet,length)
    for w in ww:
        p = crypt(''.join(w),salt)
        if p == password:
            print 'Password found: ', ''.join(w)
            return ''.join(w)
In [47]:
from string import lowercase
pw = 'toto'
slt ='XY'
h = crypt(pw,slt)
h
Out[47]:
'XYwNfZo28h1a6'
In [48]:
crack(h,'XY',lowercase,4)
Password found:  toto

Out[48]:
'toto'

Ce n'est pas très efficace (!) mais c'est simple ...

tee

La fonction tee(iterateur,n=2) retourne \(n\) copies identiques de l'itérateur :

In [49]:
it = islice(count(), 5)
i1, i2, i3 = tee(it,3)
[(i1.next(), i2.next(),i3.next()) for i in range(5)]
Out[49]:
[(0, 0, 0), (1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4)]
In [50]:
list(it) # entièrement consommé
Out[50]:
[]

imap

La fonction imap fonctionne comme map, mais s'arrête lorsque l'un des itérateurs est entièrement consommé (au lieu d'insérer des None):

In [51]:
map(lambda x,y: x*y, range(4), range(8))
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-51-95fc17660084> in <module>()
----> 1 map(lambda x,y: x*y, range(4), range(8))

<ipython-input-51-95fc17660084> in <lambda>(x, y)
----> 1 map(lambda x,y: x*y, range(4), range(8))

TypeError: unsupported operand type(s) for *: 'NoneType' and 'int'
In [52]:
map(lambda x,y: (x,y), range(4), range(8))
Out[52]:
[(0, 0), (1, 1), (2, 2), (3, 3), (None, 4), (None, 5), (None, 6), (None, 7)]
In [53]:
it=imap(lambda x,y: x*y, range(4), range(8))
list(it)
Out[53]:
[0, 1, 4, 9]

starmap

La fonction starmap fonctionne comme imap, mais calcule f(*i) :

In [54]:
it = izip('abcd', xrange(1,5))
ff = starmap(lambda x,y: x*y,it)
list(ff)
Out[54]:
['a', 'bb', 'ccc', 'dddd']

cycle

La fonction cycle répète indéfiniment un itérateur fini :

In [55]:
it = cycle('bla')
[it.next() for i in range(12)]
Out[55]:
['b', 'l', 'a', 'b', 'l', 'a', 'b', 'l', 'a', 'b', 'l', 'a']

repeat

La fonction repeat fait ce qu'on imagine :

In [56]:
it=repeat('bla',4)
list(it)
Out[56]:
['bla', 'bla', 'bla', 'bla']

On l'utilise en combinaison avec imap ou izip :

In [57]:
it = izip(xrange(5), repeat(2), 'abcd')
list(it)
Out[57]:
[(0, 2, 'a'), (1, 2, 'b'), (2, 2, 'c'), (3, 2, 'd')]

Filtrage

Le filtrage s'effectue au moyen des fonctions dropwhile, takewhile, ifilter, ifilterfalse :

In [58]:
it=takewhile(lambda x:x*x<100, count())
list(it)
Out[58]:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
In [59]:
it=dropwhile(lambda x:x<0, range(-6,5))
list(it)
Out[59]:
[0, 1, 2, 3, 4]
In [60]:
it=ifilter(lambda x:x%2, xrange(10))
list(it)
Out[60]:
[1, 3, 5, 7, 9]
In [61]:
it=ifilterfalse(lambda x:x%2, xrange(10))
list(it)
Out[61]:
[0, 2, 4, 6, 8]

groupby

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

In [62]:
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]
Out[62]:
[1, 2, 1, 4, 5, 2, 1, 3, 1]

Le résultat complet serait

In [63]:
it=groupby(ll)
[(k, list(g))  for k,g in it]
Out[63]:
[(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])]
In [64]:
ll = [('a',1), ('b',2), ('c',2), ('d',1), ('e',2), ('f',1)]
it=groupby(ll, lambda x: x[1])
[(k, list(g))  for k,g in it]
Out[64]:
[(1, [('a', 1)]),
 (2, [('b', 2), ('c', 2)]),
 (1, [('d', 1)]),
 (2, [('e', 2)]),
 (1, [('f', 1)])]

Finalement, on dispose de quelques fonctions combinatoires basiques :

In [65]:
it=combinations('abcd',2)
list(it)
Out[65]:
[('a', 'b'), ('a', 'c'), ('a', 'd'), ('b', 'c'), ('b', 'd'), ('c', 'd')]
In [66]:
it=combinations_with_replacement('abcd',2)
list(it)
Out[66]:
[('a', 'a'),
 ('a', 'b'),
 ('a', 'c'),
 ('a', 'd'),
 ('b', 'b'),
 ('b', 'c'),
 ('b', 'd'),
 ('c', 'c'),
 ('c', 'd'),
 ('d', 'd')]
In [67]:
it=permutations(range(1,4))
list(it)
Out[67]:
[(1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), (3, 2, 1)]

Autres optimisations

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 ...)

Le module ctypes

Il permet d'utiliser des bibliothèques partagées, avec des types de données compatibles au C.

In [68]:
from ctypes import *
libc =  cdll.LoadLibrary("libc.so.6")
In [69]:
printf=libc.printf
printf("%s\n", "Hello world:")
Out[69]:
13
Screenshot_20171020_171302.png

Screenshot_20171020_171302.png

Le résultat s'est affiché sur le terminal dans lequel on a lancé l'interface graphique ...

In [70]:
 print libc.time(None)
1510902284

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); }
    }
}
In [71]:
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.0564420223236
En pur Python :
0.981117010117

Cython

Pour étendre Python avec du code C ou C++, il vaut mieux utiliser Cython

  • Cython est un sur-langage de Python (basé sur Pyrex) avec les types de données du C
  • Il possède un compilateur optimisé
  • Presque tout code Python est aussi du code Cython valide
  • En déclarant les types, on obtient du code très efficace

Après avoir installé Cython, on peut reprendre l'exemple précédent. On crée un fichier ien.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

In [72]:
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.00692081451416
En pur Python :
0.996628046036

In [73]:
1.01101112366/0.00668811798096
Out[73]:
151.16526450911698

150 fois plus rapide, donc. Notons au passage que si on n'avait pas déclaré les types, l'effet aurait été beaucoup moins bon (un facteur 2).

In [ ]: