Solution du problème sur les Ellipses¶
Vous trouverez ici la solution pour l'exercice suivant:
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