Objets

Vecteur

Créer une classe Vecteur3D. Chaque vecteur aura 3 attributs : x, y, z

  • Écrire une méthode norme qui renvoie la norme.

  • Écrire la méthode __add__ pour faire la somme entre deux veteurs

  • Écrire la méthode __mul__ pour faire soit le produit par un scalaire (\(2\vec{u}\)) ou le produit scalaire (\(\vec{u}\cdot\vec{v}\)).

Bibliographie

Un livre est décrit par son titre, auteur et année de publication (pour faire les choses simplements). Écrire une classe Livre qui enregistre ces informations. Ecrire la méthode __repr__ et __str__.

Une bibliographe est une liste de livre. Écrire la classe Bibliographie qui enregistre une liste de livre (on stockera la liste de livre sous forme d’une liste qui sera un attribut de la bibliographie).

L’objectif final est de pouvoir faire ceci ::

livre1 = Livre("A very nice book", "F. Dupont", 2014)
livre2 = Livre("A very smart book", "A. Einstein", 1923)
livre3 = Livre("A very stupid comic", "D. Duck", 1937)

bibliographie = Bibliographie([book1, book2, book3])

Maintenant que tout est fait sous forme d’objet, on peut imaginer écrire plusieurs méthode :

  • Écrire une méthode filter_by_year qui fait une nouvelle bibliographie ne contenant que les livres d’une année donée.

  • Écrire une méthode to_html qui formate correctement la bibliographie. La méthode de la classe Bibliographie devra appeler une méthode pour chaque Livre.

Et en HTML ::

<table>
    <thead>
        <tr> <th>Auteur</th><th>Titre</th><th>Année</th></tr>
    </thead>
    <tbody>
       <tr><td>F. Dupont</td><td>2014</td><td>A very nice book</td></tr>
       <tr><td>A. Einstein</td><td>1923</td><td>A very smart book</td></tr>
       <tr><td>D. Duck</td><td>1937</td><td>A very stupid comic</td></tr>
    </tbody>
</table>

Remarque : si un objet possède une méthode _repr_html_, alors le jupyter notebook utilsera automatiquement la représentation en HTML. Rajouter cette méthode (qui appelera to_html).

class Livre():
    def __init__(self, titre, auteur, annee):
        self.titre = titre
        self.auteur = auteur
        self.annee = annee
    
    def __repr__(self):
        return f"Livre({self.titre!r}, {self.auteur!r}, {self.annee!r})"

    def to_html_table_line(self):
        return f"<tr><td>{self.auteur}</td><td>{self.titre}</td><td>{self.annee}</td></tr>"
    
    
bibio_html_template="""<table>
<thead>
<tr><th>Auteur</th><th>Titre</th><th>Année</th></tr>
</thead>
<tbody>
{content}
</tbody>
</table>"""


class Bibliographie():
    def __init__(self, liste_des_livres):
        self._liste_livres = liste_des_livres
        
    def __getitem__(self, key):
        return self._liste_livres[key]
        
    def __repr__(self):
        return f"Bibliographie({self._liste_livres!r})"
        
    def to_html_table(self):
        content = '\n'.join([livre.to_html_table_line() for livre in self._liste_livres])
        return bibio_html_template.format(content=content)
        
    _repr_html_ = to_html_table
livre1 = Livre("A very nice book", "F. Dupont", 2014)
livre2 = Livre("A very smart book", "A. Einstein", 1923)
livre3 = Livre("A very stupid comic", "D. Duck", 1937)

bibliographie = Bibliographie([livre1, livre2, livre3])
bibliographie
AuteurTitreAnnée
F. DupontA very nice book2014
A. EinsteinA very smart book1923
D. DuckA very stupid comic1937

Système de calcul formel

Cet exercice est à but purement pédagogique. Pour utiliser un système de calcul formel sous Python, la librairie sympy existe et fonctionnera bien mieux que ce que l’on va faire !

L’objectif de ce TD est de réaliser un système de calcul formel qui permettra de manipuler des expressions algébriques simples et de réaliser des opérations simples. Par exemple, on souhaite pouvoir effectuer ::

x = Symbol('x')
y = Symbol('y')

s = 2*x*y + sin(x)*y

print(s.diff(x)) # Dérivée par rapport à x

Chaque expression sera représentée par un arbre. Les feuilles de l’arbre seront soit les symboles soit les constantes numériques. Les noeuds seront des fonctions à un ou plusieurs argument (sinus, somme, opposé, …). Le nom de la classe du noeud désignera la fonction. Les “enfants” du noeud seront les arguments de la fonction. Par exemple l’expression ci dessus correspondra à l’objet suivant ::

# sA : 2*x*y
sA = Prod(Prod(Number(2), Symbol('x')), Symbol('y'))
# sB : sin(x)*y
sB = Prod(Sin(Symbol('x')), Symbol('y'))

s = Sum(sA, sB)

Structure du programme

Voici la structure de base ::

class Expr(object):
    pass

class Node(Expr):
    pass

class Leave(Expr):
    pass

Pour les feuilles ::

class Symbol(Leave):
    pass

class Number(Leave):
    pass

Ensuite on définit les fonctions ::

class Function(Node):
    """ Function with an arbitrary number of arguments """
    pass

Les opérateurs sont des fonctions comme les autres, mais elle seront simplement affichées différemment ::

class BinaryOperator(Function):
    pass

class Sum(BinaryOperator):
    pass 
# Idem pour Sub, Div, Prod, Pow

class UnitaryOperator(Function):
    pass

class Neg(UnitaryOperateor):
    pass

Les fonction mathématiques, qui prennent un seul argument ::

class MathFunction(Function):
    pass

class Sin(MathFunction):
    pass

Questions

On va procéder étape par étape. Il sera plus facile de commencer par les feuilles avant d’écrire la structure globale.

  1. Ecrire le __init__ de la classe Symbol et Number

class Expr(object):
    def __add__(self, other):
        return Sum(self, other)
        
    def __mul__(self, other):
        return Prod(self, other)
    
    def __repr__(self):
        return self.display()

class Node(Expr):
    pass

class Leave(Expr):
    pass

class Symbol(Leave):
    pass

class Number(Leave):
    pass

class Function(Node):
    """ Function with an arbitrary number of arguments """
    pass

class BinaryOperator(Function):
    pass

class Sum(BinaryOperator):
    pass 
# Idem pour Sub, Div, Prod, Pow

class UnitaryOperator(Function):
    pass

class Neg(UnitaryOperator):
    pass

class MathFunction(Function):
    pass

class Sin(MathFunction):
    pass
import numbers

class Symbol(Leave):
    def __init__(self, symbole):
        self.symb = symbole
        
class Number(Leave):
    def __init__(self, nombre):
        if not isinstance(nombre, numbers.Number):
            raise ValueError
        self.nbre = nombre
        
  1. Ecrire une méthode display sur ces classes afin de renvoyer une chaîne de caractère contenant le symbole ou le nombre

class Symbol(Leave):
    def __init__(self, symbole):
        self.symb = symbole

    def display(self, *args):
        return self.symb

class Number(Leave):
    def __init__(self, nombre):
        if not isinstance(nombre, numbers.Number):
            raise ValueError
        self.nbre = nombre
        
    def display(self):
        return str(self.nbre)
        
  1. Ecrire le __init__ de la class Sin ainsi que le display. Le display devra appeler le display de l’argument. Par exemple ceci devra fonctionner ::

>>> x = Symbol('x')
>>> Sin(x).display()
sin(x)
>>> Sin(Sin(x)).display()
sin(sin(x))
class Sin(MathFunction):
    def __init__(self, arg):
        self.arg = arg
        
    def display(self):
        return f'Sin({self.arg.display()})'
    
x = Symbol('x')
Sin(Sin(x)).display()
'Sin(Sin(x))'
  1. Généraliser le init et le display de Sin afin de le mettre dans la class MathFunction. On rajoutera un attribut de classe à chaque sous classe de MathFunction ::

class Sin(MathFunction):
    funtion_name = 'sin'
class MathFunction(Function):
    def display(self):
        return f'{self.function_name}({self.arg.display()})'


class Sin(MathFunction):
    function_name = 'sin'
    def __init__(self, arg):
        self.arg = arg
        
    
x = Symbol('x')
Sin(Sin(x)).display()
'sin(sin(x))'
  1. Faire de même pour les opérateurs binaires. On pourra commencer par simplement le faire pour Sum, puis généraliser avec un attribut de classe ::

class Sum(BinaryOperator):
    operator_name = '+'
class Function(Node):
    """ Function with an arbitrary number of arguments """
    def __init__(self, *args):
        self.args = args

class BinaryOperator(Function):
    def __init__(self, arg1, arg2):
        self.args = (arg1, arg2)
    
    def display(self):
        return f'({self.args[0].display()}) {self.operator_name} ({self.args[1].display()})'
    
class Prod(BinaryOperator):
    operator_name = '*'

    
class Sum(BinaryOperator):
    operator_name = '+'

x = Symbol('x')
y = Symbol('y')
Sum(x, Sin(Prod(x, y))).display()    
'(x) + (sin((x) * (y)))'
  1. A ce stade quelque chose comme ceci devrait fonctionner ::

x = Symbol('x')
y = Symbol('y')
Sum(x, Sin(Prod(x, y)))

Rajouter les méthodes __add__, __mul__, etc à la classe Expr afin de pouvoir écrire :

>>> x + Sin(x*y)
x + Sin(x*y)
(x) + (sin((x) * (y)))
  1. Ecrire les méthodes evaluate afin de calculer la valeur numérique d’une expression. Cette méthode fonctionnera de la sorte :

>>> expr = x + Sin(x*y)
>>> expr.evaluate(x=1, y=3)

On aura donc le protocole suivant ::

def evaluate(self, **kwd):
    pass

Le dictionnaire kwd sera passé récursivement jusqu’aux feuilles et sera utilisé pour évaluer les symboles.

Les opérateurs binaires numériques sont définis dans le module operator et les fonctions dans le module math. Afin de factoriser le code, on rajoutera donc simplement un attribut de classe du type operator_function = operator.add pour les opérateurs binaires et math_function = math.sin pour les fonctions.

import math
import numbers 
import operator

class Expr(object):
    def __add__(self, other):
        return Sum(self, other)
        
    def __mul__(self, other):
        return Prod(self, other)
    
    def __repr__(self):
        return self.display()

class Node(Expr):
    pass

class Leave(Expr):
    pass


class UnitaryOperator(Function):
    pass

class Neg(UnitaryOperator):
    pass


class Symbol(Leave):
    def __init__(self, symbole):
        self.symb = symbole

    def display(self, *args):
        return self.symb
    
    def evaluate(self, **kwd):
        try:
            return kwd[self.symb]
        except KeyError:
            raise Exception("La valeur de {} n'est pas définie".format(self.symb))

class Number(Leave):
    def __init__(self, nombre):
        if not isinstance(nombre, numbers.Number):
            raise ValueError
        self.nbre = nombre
        
    def display(self):
        return str(self.nbre)

    def evaluate(self, **kwd):
        return self.nbre
            
class Function(Node):
    """ Function with an arbitrary number of arguments """
    def __init__(self, *args):
        self.args = args

    def evaluate(self, **kwd):
        evaluated_args = [elm.evaluate(**kwd) for elm in self.args]
        return self.math_function(*evaluated_args)
        
class BinaryOperator(Function):
    def __init__(self, arg1, arg2):
        self.args = (arg1, arg2)
    
    def display(self):
        return f'({self.args[0].display()}) {self.operator_name} ({self.args[1].display()})'
    
class Prod(BinaryOperator):
    operator_name = '*'
    math_function = operator.mul
    
class Sum(BinaryOperator):
    operator_name = '+'
    math_function = operator.add

class MathFunction(Function):
    def display(self):
        return f'{self.function_name}({self.args[0].display()})'


class Sin(MathFunction):
    function_name = 'sin'
    math_function = math.sin
    def __init__(self, arg):
        self.args = (arg,)
    
x = Symbol('x')
y = Symbol('y')
expr = x + Sin(x*y)
print(expr)
expr.evaluate(x=2, y=4.5)
(x) + (sin((x) * (y)))
2.4121184852417565
  1. Maintenant que vous avez compris le principe, il devrait être facile d’écrire une méthode diff qui effectue la dérivée par rapport à une variable !

  2. Reste à simplifier les expressions. Une technique consiste à créer des règles de simplifications sous forme de méthode que l’on regroupe ensuite dans une liste ::

class Sum(BinaryOperator):
    operator_name = '+'
    operator_function = operator.add

    def simplication_de_deux_nombres(self):
        if isinstance(self.arg1, Number) and 
                        isinstance(self.arg2, Number):
            return Number(self.arg1.value + self.arg2.value)

    def simplication_addition_avec_zero(self):
        pass

    liste_simplication = ['simplication_de_deux_nombres', 
                    'simplication_addition_avec_zero'] 

Ensuite, il faut réussir à appeler correctement et de façon recursive ces méthodes…

  1. Pour l’affichage des opérateurs binaires, les règles de priorité peuvent être utilisées pour éviter de mettre trop de parenthèses. Par exemple, dans le cas a*(b+c), la multiplication appelle le display de l’addition. Comme elle est prioritaire, l’addition va renvoyer le résulat avec des parenthèses. Dans le cas inverse a + b*c, c’est inutile. Il faut donc que le display d’un opérateur passe sa priorité à ses enfants lors de l’appel de display. Implémenter ce principe.

import numbers
import operator
import math

class Expr(object):

    def binary_operator(self, other, operator):
        if isinstance(other, numbers.Number):
            other = Number(other)
        if isinstance(other, Expr):
            return operator(self, other)
        return NotImplemented

    def reversed_binary_operator(self, other, operator):
        if isinstance(other, numbers.Number):
            other = Number(other)
        if isinstance(other, Expr):
            return operator(other, self)
        return NotImplemented

    def __add__(self, other):
        return self.binary_operator(other, Sum)
        
    def __mul__(self, other):
        return self.binary_operator(other, Prod)

    def __truediv__(self, other):
        return self.binary_operator(other, Div)

    def __sub__(self, other):
        return self.binary_operator(other, Sub)

    def __radd__(self, other):
        return self.reversed_binary_operator(other, Sum)
 
    def __rmul__(self, other):
        return self.reversed_binary_operator(other, Prod)

    def __rtruediv__(self, other):
        return self.reversed_binary_operator(other, Div)

    def __rsub__(self, other):
        return self.reversed_binary_operator(other, Sub)

    def __neg__(self):
        return Neg(self)
    
    def __repr__(self):
        return self.display()


    def diff(self, var):
        out = self._diff(var)
        return out.simplify()

class Node(Expr):
    pass

class Leave(Expr):
    def simplify(self):
        return self

class Symbol(Leave):
    def __init__(self, symbole):
        self.symb = symbole
        
    def display(self, *args):
        return self.symb
    
    def evaluate(self, **kwd):
        try:
            return kwd[self.symb]
        except KeyError:
            raise Exception("La valeur de {} n'est pas définie".format(self.symb))

    def __eq__(self, other):
        if not type(self)==type(other):
            return False
        return self.symb==other.symb

    def _diff(self, var):
        if self==var:
            return Number(1)
        return Number(0)

class Number(Leave):
    def __init__(self, nombre):
        if not isinstance(nombre, numbers.Number):
            raise ValueError
        self.nbre = nombre
        
    def display(self, *args):
        return str(self.nbre)
    
    def evaluate(self, **kwd):
        return self.nbre

    def __eq__(self, other):
        if isinstance(other, numbers.Number):
            other = Number(other)
        if isinstance(other, Number):
            return other.nbre==self.nbre
        return False

    def _diff(self, var):
        return Number(0)

class Function(Node):
    """ Function with an arbitrary number of arguments """
    def __init__(self, *args):
        self.args = args

    def evaluate(self, **kwd):
        evaluated_args = [elm.evaluate(**kwd) for elm in self.args]
        return self.math_function(*evaluated_args)

    def __eq__(self, other):
        if not type(self)==type(other):
            return False        
        return self.args==other.args

    def _diff(self, var):
        partial_derivative = getattr(self, 'partial_derivative', None)
        if partial_derivative is None:
            raise NotImplementedError('Cannot derivate function {self.__class__}.'.format(self=self))
        if len(self.args)==0:
            return Number(0)
        out = self.args[0].diff(var)*partial_derivative[0](*self.args)
        for deriv, arg in zip(partial_derivative[1:], self.args[1:]):
            out = out + arg.diff(var)*deriv(*self.args)
        return out

    liste_simplification = []
    def simplify(self):
        out = type(self)(*[elm.simplify() for elm in self.args])
        for elm in self.liste_simplification:
            tmp = getattr(out, elm)()
            if tmp is not None:
                return tmp.simplify()
        return out


class BinaryOperator(Function):
    commutative=False
    def display(self, parent_priority=0):
        if parent_priority>self.priority:
            fmt_str = '({} {} {})'
        else:
            fmt_str = '{} {} {}'
        return fmt_str.format(self.args[0].display(self.priority), 
                self.operator_name, self.args[1].display(self.priority))

    def __eq__(self, other):
        if not type(self)==type(other):
            return False
        if self.args==other.args:
            return True
        if self.commutative:
            return self.args==other.args[::-1]
        return False

class Sum(BinaryOperator):
    priority = 0
    operator_name = '+'
    math_function=operator.add
    commutative=True    
    partial_derivative = (lambda x, y:1, lambda x, y:1)


    def simplication_de_deux_nombres(self):
        if isinstance(self.args[0], Number) and isinstance(self.args[1], Number):
            return Number(self.args[0].nbre + self.args[1].nbre)

    def simplication_addition_avec_zero(self):
        if self.args[0]==0:
            return self.args[1]
        if self.args[1]==0:
            return self.args[0]

    def simplification_identique(self):
        if self.args[1]==self.args[0]:
            return 2*self.args[0]

    liste_simplification = ['simplication_de_deux_nombres',
        'simplication_addition_avec_zero', 'simplification_identique']

class Prod(BinaryOperator):
    priority = 1
    operator_name = '*'
    math_function=operator.mul
    commutative=True    
    partial_derivative = (lambda x, y:y, lambda x, y:x)

    def simplication_de_deux_nombres(self):
        if isinstance(self.args[0], Number) and isinstance(self.args[1], Number):
            return Number(self.args[0].nbre * self.args[1].nbre)


    def simplication_multiplication_par_un(self):
        if self.args[0]==1:
            return self.args[1]
        if self.args[1]==1:
            return self.args[0]

    def simplication_multiplication_par_zero(self):
        if self.args[0]==0 or self.args[1]==0:
            return Number(0)
        
    liste_simplification = ['simplication_de_deux_nombres',
        'simplication_multiplication_par_un', 'simplication_multiplication_par_zero']


class Div(BinaryOperator):
    priority = 2
    operator_name = '/'
    math_function=operator.truediv
    partial_derivative = (lambda x, y:1/y, lambda x, y:-x/(y*y))

class Sub(BinaryOperator):
    priority = 0.5
    operator_name = '-'
    math_function=operator.sub
    partial_derivative = (lambda x, y:1, lambda x, y:-1)


class UnitaryOperator(Function):    
    def display(self, parent_priority=0):
        if parent_priority>self.priority:
            fmt_str = '({}{})'
        else:
            fmt_str = '{}{}'
        return fmt_str.format(self.unitary_symbol, 
                              self.args[0].display(self.priority))
            
class Neg(UnitaryOperator):
    priority = 0.5
    unitary_symbol = "-"
    math_function = operator.neg
    partial_derivative = (lambda x:-1,)
    
class MathFunction(Function):
    def display(self, *args):
        return '{}({})'.format(self.function_name, self.args[0].display())


class Sin(MathFunction):     
    function_name = 'sin'
    math_function = math.sin
    partial_derivative = (lambda x:cos(x),)


class Cos(MathFunction):     
    function_name = 'cos'
    math_function = math.cos
    partial_derivative = (lambda x:-sin(x),)


sin = Sin
cos = Cos    
x = Symbol('x')
sin(2*x).diff(x).simplify()
2 * cos(2 * x)