TD "Révision" : 25.05 ; 26.05.2016

Avertissement

Vous savez que l'examen portera sur les éléments du cours, la mise en oeuvre pratique des concepts OO comme des classes et méthodes, ensuite les itérateurs, les décorateurs, etc. On fait ces TD, je vous expliquerai des choses – dans la mesure du possible, et en fonction de vos questions –, mais je ne pourrai combler les lacunes dans votre connaissance du cours ; ces TD ne remplacent pas mes notes, ni votre révision. De plus, j'ai constaté que certains parmi vous ne maîtrisent toujours pas bien le langage Python niveau L1. Faites attention, car ici je serai particulièrement intransigeant. Du n'importe quoi syntaxique, la méconnaissance des boucles ou de l'indexation, ou des conditionnelles mal construites risquent d'invalider totalement l'exercice défaillant.

On travaillera surtout sur les exercices des examens passés.
En 4 heures probablement nous ne pourrons couvrir tous les exercices présents ici et éventuellent ajoutés. Alors essayez de les résoudre vous-mêmes, et si quelque chose vous bloque, posez des questions, j'inclurai la réponse sur cette page. Mais : pas de questions? alors pas de réponse...


Exercice 1.

[Arithmétique spéciale]. Le sujet "le plus standard" en POO est la construction d'une classe avec ses méthodes. Reprenons le sujet de décembre...

Un objet contenant plusieurs champs c'est comme un tuple, mais on peut faire beaucoup plus de choses avec. Par exemple, les nombres complexes : z = x + yi, avec \(i = \sqrt{-1}\) peuvent être implémentés comme paires de réels : C(x,y), avec des méthodes appropriées, sans que jamais cette racine de -1 se manifeste !

Clifford a inventé un autre corps numérique, les nombres duals, plus simples ; un nombre dual p est une paire de réels a et b, disons, D(a,b), écrit intuitivement comme p = a+bε avec les règles arithmétiques simples, par exemple on ajoute séparement les composantes réelle et duale. L’unité duale ε est irréductible, indépendante des réels (comme l’unité imaginaire dans le monde des nombres complexes), mais elle respecte l’identité : ε2 = 0. Donc, (a+bε)·(x+yε) = ax + (ay+bx)ε.
Attention, le symbole ε ne figure jamais dans le code Python, c'est une entité purement conceptuelle ; le code Python opère avec les paires (a,b), et c'est tout.

Écrire en Python une classe D qui représente de tels nombres, avec les méthodes standard : __init__ et __repr__ raisonnables, et les quatres opérations arithmétiques : + - * /, de manière la plus complète possible (et avec l’arithmétique mixte dual–réel). Pour la division, vérifier que (a+bε)·(a-bε) est un nombre réel (quelle est sa valeur?), et coder-la de manière similaire à la division des nombres complexes. La méthode __init__ doit pouvoir prendre 1 ou deux arguments, dans le cas d’un seul, le coefficient de ε est considéré nul.

Solution

(Avec l'arithmétique mixte : duals et réels).

class D(object):
    def __init__(self,x,y=0):
        self.x=x; self.y=y
    def __repr__(self):
        return "D"+str((self.x,self.y))
    def conj(self):
        return D(self.x,-self.y)
    def __add__(self,zz):
        if isinstance(zz,D):
            return D(self.x+zz.x,self.y+zz.y)
        else:
            return D(self.x+zz,self.y)
    def __radd__(self,zz):
        return D(self.x+zz,self.y)
        
    def __sub__(self,zz):
        if isinstance(zz,D):
            return D(self.x-zz.x,self.y-zz.y)
        else:
            return D(self.x-zz,self.y)
    def __rsub__(self,zz):
        return D(zz-self.x,-self.y)
    def __mul__(self,zz):
        if isinstance(zz,D):
            return D(self.x*zz.x,
                     self.x*zz.y+self.y*zz.x)
        else:
            return D(self.x*zz,self.y*zz)
    def __rmul__(self,zz):
        return D(self.x*zz,self.y*zz)
    def __truediv__(self,zz):
        if isinstance(zz,D):
            d=zz.x**2
            return (self*zz.conj())/d
        else:
            return D(self.x/zz,self.y/zz)
    def __rtruediv__(self,zz):
        return (zz/self.x**2)*self.conj()


Exercice 2.

[Contrôle de la création des instances]. Construire une simple classe, sans trop de fonctionnalités, par ex. une classe qui stocke une chaîne dans ses instances. Voici une définition incomplète :

class Cl(object):
    def __init__(self,s):
        self.s=s
    def __repr__(self):
        return "<<"+self.s+">>"
Le sens de cet exercice ne concerne pas le fonctionnement des instances, mais leur création. Cette classe doit sauvegarder le nombre d'instances engendrées. Chaque création de Cl("quelque chose") doit augmenter un compteur "caché" dans la classe, dont la valeur peut être récupérée par une méthode de classe, disons compteur().

Est-il possible / facile d'organiser la décrémentation de ce compteur quand une instance de Cl cesse d'exister [d'être accessible]?

Solution

class Cl(object):
    __cpt=0
    def __new__(cls,ss):  # création des instances
        ob=object.__new__(Cl)
        Cl.__cpt+=1
        print("param: ",ss)
        return ob
    def __init__(self,s):
        self.s=s
    def __repr__(self):
        return "<<"+self.s+">>"
    @classmethod
    def comp(cls):  # Récupération du compteur
        return cls.__cpt
    # Finalement une méthode spéciale qui PEUT se déclencher par le système 
    # quand un objet devient inaccessible
    def __del__(self):
        print("destruction")
        Cl.__cpt-=1
        
zz = Cl('first')


Commentaires

Les classes sont assemblées "statiquement", quand Python lit votre programme, mais elles sont des objets, comme les autres, et leur fonctionnement est orienté-objet. Afin qu'une classe puisse "faire" quelque chose, Python doit déclencher une de ses méthodes. Si l'action en question est la création d'une instance, aucune méthode normale (comptant self parmi ses paramètres) ne peut être utilisée, car le destinataire, l'instance, n'existe pas encore.

Cependant, Python dispose d'autres catégories de méthodes : statiques, et de classe. en particulier __new__ est une méthode statique, qui n'a pas d'arguments implicits (comme self). Le paramètre cls est explicit, manifeste, il sera là lors de l'appel de cette méthode. Mais ce n'est pas l'utilisateur qui l'appelle...

La méthode est appelée automatiquement lors de la création d'une instance. Si on définit cette méthode dans une classe, disons MClass(...), la machine Python lancera

MClass.__new__(MClass,...)
et on peut se poser la question, pourquoi ce paramètre cls, affectée à la classe, si elle est déjà présente comme le "propriétaire" de la méthode __new__?...

Ceci est le résultat de certains choix historiques de Van Rossum, parfois un peu chaotiques, et j'ai préféré de ne pas en parler trop en cours, car pour les débutants ceci n'est pas clair. La réponse la plus courte est :
La méthode __new__ dans la classe MClass grâce à ce paramètre, peut engendrer l'instance d'une autre classe, notamment la souclasse de MClass, et ainsi les sous-classes peuvent partager cette méthode. La réponse définitive à la question "qu'est-ce que cls" est : c'est la classe qui sera instanciée, et qui est d'habitude mais pas toujours, identique à la classe qui exécute __new__.

D'autres paramètres, qui suivent cls, sont passés à __init__. La machine Python qui exécute __new__ de la classe instanciée, récupère son résultat, qui est l'instance, une zone mémoire structurée comme une instance, mais vide. C'est __init__ qui la remplit, construit son __dict__, etc., et puisque __new__(MClass,params) est appelé quand la forme MClass(params) est interprétée, la façon la plus naturelle de procéder après la création de l'instance est d'appeler automatiquement son __init__ et lui passer params.


Exercice 3.

[Code de César]. Une technique primitive de chiffrage consiste à remplacer chaque lettre dans le texte codé par la lettre décalée dans l'alphabet d'un certain nombre n de positions. Par ex., si n=8, le codage de 'cesar' donne 'km{iz'. (On peut modifier l'algorithme en utilisant le décalage cyclique qui ne sort pas de l'alphabet de lettres).

Construire une classe Cesar, dont les instances sont parametrées par n, et qui "simule" un dictionnaire qui effectue le codage.
Par ex. b=Cesar(4); res=b['vous obtiendrez'] donne : 'zsyw$sfxmirhvi~' .

Est-ce que la construction de cette classe par la surcharge des dictionnaires apporte quelque chose d'intéressant par rapport à sa construction primitive (surcharge de object)?

Solution

class Cesar(object):
    def __init__(self,n):
        self.n=n
    def __getitem__(self,s):
        x=list(s)
        y=[chr(ord(a)+self.n) for a in x]
        return ''.join(y)

La seule chose importante ici est la surcharge de __getitem__. Rappelons que ord convertit un caractère en code, et chr effectue la conversion inverse. La méthode présentée est peu fiable, et peut déborder (pas de réduction cyclique) l'alphabet.


Exercice 4.

[Décorateur]. Soit f(x) une fonction numérique réelle d'un argument x, qui peut être définie normalement en Python avec des expressions arithmétiques standard. Définir un simple décorateur dfd, tel que

@dfd
def f(x):
    ...
    return ...
produit une fonction qui calcule l'approximation de la dérivée de la fonction-argument, utilisant le quotient différentiel classique (centré) \(f'(x) \approx (f(x+\epsilon/2) - f(x-\epsilon/2))/\epsilon\).

Solution

def f(x):
    return sin(3*x)*exp(-x/3)

x=linspace(0,10,300)
#y=f(x)
# ou : 
y=array([f(z) for z in x])
plot(x,y,"g",lw=2)

def dfd(fun):
    epsd=0.0001; epsh=epsd/2
    def wrap(x):
        y=(fun(x+epsh)-fun(x-epsh))/epsd
        return y
    return wrap

def g(x):
    return sin(3*x)*exp(-x/3)
g=dfd(g)

z=300*[0.0]
y1=g(x)
figure()
plot(x,y,'k',x,y1,'r',x,z,'b',lw=1.5)


Exercice 5.

[Décorateur, un cas simple, mais non-trivial : parametré !]. Écrire une fonction-décorateur, disons flim(ymin,ymax), parametrée par deux nombres réels, et agissant sur une fonction numérique d'un argument :

@flim(a,b)
def f(x):
   ...
   return ...
telle, que la fonction f transformée retourne les valeurs qui ne dépassent pas l'intervalle a   :   b. Toute valeur de sortie inférieure à a devient a, toute supérieure à b, est limitée à b.

Solution

def flim(a,b):
    def decor(fun):
        def aux(x):
            y=fun(x)
            if y<a: return a
            if y>b: return b
            return y
        return aux
    return decor
                
@flim(-0.3,0.7)
def fu(x):
    return sin(4*x)

# ou :
#fu=flim(-0.3,0.7)(fu)

x=linspace(0,5,300)
y=[fu(z) for z in x]
plot(x,y)


Exercice 6.

[Surcharge des dictionnaires]. Construire une classe qui surcharge les dictionnaires standard Python.

class Mdict(dict):
   ...
On suppose que toutes les clés de ce dictionnaire sont des simples chaînes de caractères. Construire la méthode __init__ permettant d'initialiser les instances de cette classe avec un dictionnaire normal : d = Mdict({'x':87,'MaiSoN':'House'}), etc. Cependant, cette classe transforme toutes les clés lors de l'initialisation, ou de l'accès, ou de l'insertion : r=d['X']; d['BoN'] = "Good" etc. en minuscules. Le dictionnaire n'aura jamais des clés comme 'MaiSoN' ou 'BoN', elles seront transformées en 'maison' et 'bon'. Tout accès convertira aussi les clés. On pourra utiliser la méthode des chaînes : chn.lower() pour effectuer la conversion.

Solution

class Mdict(dict):
    def __init__(self,dct):
        d1=dict([(c.lower(),v) for c,v in dct.items()])
        dict.__init__(self,d1)
    def __getitem__(self,cl):
        return dict.__getitem__(self,cl.lower())
    def __setitem__(self,cl,v):
        dict.__setitem__(self,cl.lower(),v)

dd={'MaiSoN':'House', 'X':42, "BoN":'Good'}
md=Mdict(dd)


Exercice 7.

[Flots]. Vous aurez sans doute un exercice sur les flots (itérateurs / générateurs). Ce sont des objets qui fournissent des éléments d'une séquence à la demande, et non pas des listes ! Si gen est un tel itérateur, x=next(gen) est le moyen typique d'engendrer cet élément.

Écrire un générateur Python qui prend comme entrée un flot (un autre générateur, une séquence) numérique,sans longueur spécifiée [finie ou non]. La séquence engendrée montre la « tendance » des données source : si les valeurs des données augmentent, le résultat est +1, si diminuent : -1, si égaux : zéro. Ainsi, si la séquence – source est : 3, 3, 6, -2, 7, 8, 4, 2, 7, 7, 2, 6, 1, 2, 2, 6, ..., le résultat engendré par le générateur doit être : 0, 1, -1, 1, 1, -1, -1, 1, 0, -1, 1, -1, 1, 0, 1, ...

Solution

from numpy import *
l=[3, 3, 6, -2, 7, 8, 4, 2, 7, 7, 2, 6, 1, 2, 2, 6]  # test
def g(l):
    for x in l:
        yield x
p=g(l)
# On peut plus facilement définir :  p = iter(l)

def tend(gen):
    f=next(gen)
    for x in gen:
        r=sign(x-f)
        f=x  # le "présent" devient le "précédent"
        yield r
pp=tend(p)


Exercice 8.

[Encore, itérateurs]. Écrire une fonction/générateur ousinon(g1,g2) qui prend deux itérateurs / générateurs, g1, et g2, et si le premier est effectif (au moins un item disponible), le résultat se réduit à lui, l'autre devient inutilisable. Si, par contre, le premier échoue tout de suite, le résultat est équivalent au second générateur.

Pensez à vérifier le premier élément dans try ..., mais ne pas oublier sa valeur, il sera retourné (par yield) de notre fonction génératrice avant la boucle for qui engendre les autres. Dans ce cas l'autre générateur n'est pas touché. Si try échoue, on reprend le second argument.

Solution

import itertools as it
g1=iter([1,3,5,7])
g2=iter([12,14,16,18,20]) # Pour tester

def ousinon(g1,g2):
    try:
        x=next(g1)
        yield x
        for y in g1 : yield y
        return
    except StopIteration:
        for y in g2 : yield y

gn = ousinon(g1,g2)


Exercice 9.

[Encore, arithmétique spéciale]. Concevoir et coder la classe Hor des objets qui représentent le temps horloge, avec trois champs : (heure, minute, seconde). Coder l’arithmétique pour ces objets :

Pas de solution de ma part. Travaillez vous-mêmes sur cet exercice.


Exercice 10.

[Méthodes spéciales d'accès]. Construire une simple classe, dont les instances disposent d'un champ spécial, nommé par ex. val (parmi d'autres champs, au moins un), mais qui ne peut être changé à volonté. Utiliser la méthode spéciale d'affectation des champs, de manière à obtenir le résultat suivant :

Après l'initialisation de l'instance, on ne pourra que diminuer la valeur de ce champ. Toute tentative de l'augmenter imprime un message diagnostic, et la valeur reste inchangée. Les autres champs n'ont aucune restriction.

Solution

# Piège : setattr est appelé par __init__
class Dimi(object):
    def __init__(self,val,x):
        self.__dict__['val']=val
        # Surtout pas self.val = val
        self.x = x
    def __setattr__(self,nom,valeur):
        if nom=='val':
            if self.val<valeur:
                print("Affectation illégale")
            else: self.__dict__['val']=valeur
        else:
            self.__dict__[nom]=valeur
    def __repr__(self):
        return str((self.val,self.x))
        
dd=Dimi(76,111)


Exercice 11.

[Interpolateur linéaire]. Concevoir et coder une classe, disont Interp(object), dont les instances prennent en paramètre une liste numérique (nommons-la l), par ex. p=Interp([1,-1,3.5,2.2,0.2,1.0]). Ensuite un appel p(x)x est un nombre entre 0 et la longueur de la liste moins un [le dernier indice], donne pour résultat la valeur interpolée entre les deux valeurs voisines de la liste. Par exemple, p(2.8) identifie les éléments voisins l[2] = 3.5, et l[3] = 2.2, et cherche la valeur interpolée à la distance 0.8 depuis le premier ; la distance totale vaut 1, bien sûr. Le résultat doit être 2.46.

Solution

class Interp(object):
    def __init__(self,ll):
        self.ll=ll
        self.n=len(ll)-1
    def __call__(self,x):
        m=int(x); z=x-m
        if m<=0: return self.ll[0]
        if m>=self.n: return self.ll[self.n]
        d=self.ll[m+1]-self.ll[m]
        f=self.ll[m]+z*d
        return f

p=Interp([1,-1,3.5,2.2,0.2,1.0])

a=linspace(-2,8,300)
ff=[p(y) for y in a]
#ff=p(a)
plot(a,ff,"k",lw=1.5)


Exercice 12.

[Encore un sujet d'examen : "pseudo-listes" chaînées]. On définit une classe qui émule les tuples (paires):

class P(object):
    def __init__(self,hd=None,tl=None):
        self.hd=hd; self.tl=tl
    def __repr__(self): return str((self.hd,self.tl)) # Pour l'affichage
Écrire la fonction ltop(lst) qui convertit une liste en suite d'objets, par ex. mp=ltop([1,2,5]) s'affiche : (1,(2,(5,None))).

Écrire la méthode ptol(self) qui effectue l'opération inverse et retourne une liste depuis une "cascade" de paires. Écrire deux autres méthodes : 1. concat(self,pp) qui ajoute la suite pp à la fin de self ; 2. reverse(self), qui renverse la suite (en la copiant, sans modifier l'original). mp après mp.concat(mp.reverse()) donne (1,(2,(5,(5,(2,(1,None)))))).

Solution

class P(object):
    def __init__(self,hd=None,tl=None):
        self.hd=hd; self.tl=tl
    def __repr__(self): return str((self.hd,self.tl))
    def  ptol(self):
        v=self
        r=[]
        while isinstance(v,P):
            r.append(v.hd)
            v=v.tl
        return r
    def reverse(self):
        v=self
        r=None
        while v:
            r=P(v.hd,r)
            v=v.tl
        return r
    def concat(self,pp):
        if self.tl==None:
            tp=pp
        else:
            tp=self.tl.concat(pp)
        return P(self.hd,tp)       
   
def ltop(l):
    if l: return P(l[0],ltop(l[1:]))
    return None
    

mp=ltop([1,2,5])
rs=mp.concat(mp.reverse())   
# ==========================================   


Exercice 13.

[Reprise d'un itérateur]. Un de vous m'a demandé si on peut faire "ressusciter" un itérateur déjà "consommé". La réponse est : en général ceci n'est pas possible, il faut que l'itérateur puisse mémoriser la séquence engendrée, afin de la répéter. Il y a des objets comme ça, par exemple range(...), mais c'est un objet spécial, non pas un itérateur normal.
Nous avons vu que p=iter(lst)lst est une liste, disons [1,2,5,8]. Construire une classe dont une instance parametrée et contenant une liste, par ex. p=IterL([1,2,5,8]). Cette classe sera équipée de la méthode __iter__, ce qui permet son usage dans une boucle for, disons :

for x in p: print(x)
Cependant l'objet p doit se comporter comme range, c'est-à dire, si on lance la boucle pour la seconde fois, la séquence est reprise, et affichée. (Observez que si la liste est là, accessible par l'objet, la séquence EST déjà mémorisée).

Solution

class IterL(object):
    def __init__(self,lst):
        self.l=lst
    def __iter__(self):
        return iter(self.l)

ll=[2,3,5,8,13,4,0,1]
p=IterL(ll)

# tester 2 fois
for x in p: print(x)


Exercice 14.

[Méthodes d'accès, encore]. (Surcharge des listes ; cet exercice est difficile). Écrire une classe, disons class Cl(list) qui surcharge les listes standard (et donc hérite ses propriétés). La méthode __init__ doit permettre la création des instances en passant un nombre arbitraire d'arguments, par ex. a=Cl(1,"chaine"); b=Cl((),2,4,Cl(7)), etc. La représentation textuelle peut rester standard, [1, 'chaine'], etc.

Implémenter ces deux modifications par rapport aux listes normales :

Solution

class Cl(list):
    def __init__(self,*l):
        list.__init__(self,l)
        self.n=len(l)
    def __getitem__(self,k):
        return list.__getitem__(self,k % self.n)
    def __setitem__(self,ind,v):
        raise TypeError("Ces listes sont immuables")
    # Bonus : longueur bidon
    def __len__(self):
        return 42 

a=Cl(1,"chaine")
b=Cl((),2,4,Cl(7))
print(len(b))



Les solutions ne sont pas commentées, et c'est volontaire. Cette page est destinée aux personnes présentes en séances de rattrapage. On a discuté TOUT (ou presque), toute question a eu sa réponse, et j'ai justifié tout élément douteux du code. Si quelqu'un (absent) pense qu'il suffit de lire les solutions sans passer un certain temps dessus, bon courage.

Retour