Skip to content

Solution du problème sur les Ellipses

Vous trouverez ici la solution pour l'exercice suivant:

Ellipse

Exercice de base

Il s'agit de créer une classe avec des attributs. En python cela se fait de la façon suivante:

class MyClass:
    def __init__(self):
        self.my_attribute = "string_value"
        self.my_other_attribute = 123

>>> item = MyClass()
>>> item.my_attribute
"string_value"

Si on veut passer des informations lors de la construction d'un objet, il faut rajouter des parametres à la fonction __init__:

class MyClass:
    def __init__(self, param1):
        self.my_attribute = param1
        self.my_other_attribute = 123

>>> item = MyClass(123)
>>> item.my_attribute
123

Donc si on veut résoudre l'exercice de base:

import math

class Ellipse:
    def __init__(self, big_base, small_base):
        self.big_base = big_base
        self.half_big_base = big_base / 2
        self.small_base = small_base
        self.half_small_base = small_base / 2
        self.area = math.pi * big_base * small_base / 4
        self.perimeter = math.pi * \
            math.sqrt(
                2 * (self.half_big_base**2 + self.half_small_base**2)
            )

Valeurs par defaut

Si on veut affecter une valeur par defaut a un parametre, rien de plus simple: on le fait en ajoutant le signe = ainsi que la valeur par defaut que l'on souhaite.

Pour notre ca, il faut changer la definition de __init__:

(...)
    def __init__(self, big_base=1, small_base=1):
(...)

Un joli string

Pour le fait d'avoir une representation en string jolie, il nous faut redefinir la fonction spéciale __repr__ qui sert lorsque l'on appelle repr() sur un objet.

La fonction __str__ qui est appelée lorsque l'on fait str() sur un objet n'a pas à être redéfinie ici; en effet, l'implementation par défaut de __str__ appelle repr().

N.B.: l'inverse n'est pas vrai, la version par défaut de __repr__ n'appelle pas str().

Donc nous n'avons qu'a rajouter ces lignes à notre classe:

(...)
    def __repr__(self):
        return "Ellipse(%s, %s)" % (self.big_base, self.small_base)

Bonus 1

Pour réussir ici, il faut pouvoir changer les valeurs des demi bases, d'aire et le perimetre lorsque l'utilisateur change une base.

>>> e = Ellipse(2, 1)
>>> e.big_base
2
>>> e.big_base = 5
>>> e.big_base
5

Un moyen simple est de calculer ces valeurs lorsque le code client les demande, en se basant sur les small_base et big_base uniquement.

Mais, comment faire en sorte de pouvoir calculer dynamiquement une valeur lorsqu'on accede à un attribut?

Pour cela il faut utiliser un décorateur.

Le decorateur à utiliser ici est @property. Celui-ci permet de declarer un attribut en utilisant une methode qui sera son getter.

Voila comment on fait:

(...)
    @property
    def half_big_base(self):
        # this method is called when client code access half_big_base attribute for read
        return self.big_base / 2

Pour réussir le bonus 1, il suffit donc d'utiliser ce decorateur sur half_big_base, half_small_base, perimeter et area.

Bonus 2

Ici il s'agit de pouvoir changer half_big_base et half_small_base afin de changer les autres attributs. Pour y arriver nous allons de nouveau utiliser le decorateur @property mais cette fois pour definir un setter pour ces attributs.

(...)
    @half_big_base.setter
    def half_big_base(self, new_value):
        """ This is the setter for half_small_base attribute"""
        self.big_base = new_value * 2

Il suffit de faire cela pour half_small_base aussi et le tour est joué.

Pour ce qui concerne l'exception à lever, @property s'en charge pour nous: si l'attribut n'a pas de setter alors l'exception est levée automatiquement. Donc, ne definission pas de setter ni pour area ni pour perimeter et voila.

N.B. : Il est aussi possible d'ajouter un deleter afin de faire des actions suplémentaires quand un code client detruit un attribut (avec la commande del), mais ici cela ne sera d'aucune utilité.

Bonus 3

Une fois de plus il va falloir mettre en place un attribut avec le decorateur @property, cette fois pour big_base et small_base afin de pouvoir s'assurer que l'on ne donne pas une valeur négative à ces attributs.

Afin que tout continue a fonctionner il va falloir stocker les valeurs de grande base et petite base dans d'autres attributs, si possible privés ou protégés. Pour rendre un item protégé, il faut ajouter un _ en prefix de nom de variable. Si on veut rendre l'item privé, on en met deux.

    def __init__(self, big_base=1, small_base=1):
        # keep big_base value in a dedicated internal attribute
        self._big_base = big_base
(...)
    @property
    def big_base(self):
        # this method is called when client code access big_base attribute for read
        # internal attribute cannot have the same name as the property
        return self._big_base

    @big_base.setter
    def big_base(self, new_value):
        # this method is called when client code changes value of big base
        if new_value < 0:
            raise ValueError("Base cannot be negative")
        self._big_base = new_value

En faisant le test sur la négativité de la nouvelle valeur, on ne change pas l'objet Ellipse. Si on avait testé après, l'objet aurait été changé et serait devenu inutilisable.

Une solution.

Voila mon code, il n'est pas parfait, et manque de beaucoup de choses (comme des commentaires et de la doc entre autres), mais cela peut vous donner des idées:

#!/usr/bin/env python3

from __future__ import division


import math


class Ellipse(object):
    """
    Training class for discovering how to use smart attributes
    """
    def __init__(self, big_base=1, small_base=1):
        self._big_base = big_base
        self._small_base = small_base

    def __repr__(self):
        return "Ellipse(%s, %s)" % (self.big_base, self.small_base)

    @property
    def big_base(self):
        return self._big_base

    @big_base.setter
    def big_base(self, new_value):
        if new_value < 0:
            raise ValueError("Base cannot be negative")
        self._big_base = new_value

    @property
    def half_big_base(self):
        return self._big_base / 2

    @half_big_base.setter
    def half_big_base(self, new_value):
        self.big_base = new_value * 2

    @property
    def small_base(self):
        return self._small_base

    @small_base.setter
    def small_base(self, new_value):
        if new_value < 0:
            raise ValueError("Base cannot be negative")
        self._small_base = new_value

    @property
    def half_small_base(self):
        return self._small_base / 2

    @half_small_base.setter
    def half_small_base(self, new_value):
        self.small_base = new_value * 2

    @property
    def perimeter(self):
        return \
            math.pi * \
            math.sqrt(
                2 * (self.half_big_base**2 + self.half_small_base**2)
            )

    @property
    def area(self):
        return math.pi * self.half_big_base * self.half_small_base