Dans ce chapitre, nous allons nous intéresser à des concepts plus avancés de programmation orientée objet disponibles en Python, tels que les attributs/méthodes de classe ou les propriétés.
- Les attributs entrent en classe
- La méthode pour avoir la classe
- Le statique c'est fantastique
- Attribut es-tu là ?
- La dynamique des propriétés
- L'art des classes abstraites
- TP : Base de données
Les attributs entrent en classe
Nous avons déjà rencontré un attribut de classe, quand nous nous intéressions aux parents d’une classe.
Souvenez-vous de __bases__
, nous ne l’utilisions pas sur des instances mais sur notre classe directement.
En Python, les classes sont des objets comme les autres, et peuvent donc posséder leurs propres attributs.
1 2 3 4 5 6 | >>> class User: ... pass ... >>> User.type = 'simple_user' >>> User.type 'simple_user' |
Les attributs de classe peuvent aussi se définir dans le corps de la classe, de la même manière que les méthodes.
1 2 | class User: type = 'simple_user' |
On notera à l’inverse qu’il est aussi possible de définir une méthode de la classe depuis l’extérieur :
1 2 3 4 5 6 | >>> def User_repr(self): ... return '<User>' ... >>> User.__repr__ = User_repr >>> User() <User> |
L’avantage des attributs de classe, c’est qu’ils sont aussi disponibles pour les instances de cette classe. Ils sont partagés par toutes les instances.
1 2 3 4 5 6 | >>> john = User() >>> john.type 'simple_user' >>> User.type = 'admin' >>> john.type 'admin' |
C’est le fonctionnement du MRO de Python, il cherche d’abord si l’attribut existe dans l’objet, puis si ce n’est pas le cas, le cherche dans les classes parentes.
Attention donc, quand l’attribut est redéfini dans l’objet, il sera trouvé en premier, et n’affectera pas la classe.
1 2 3 4 5 6 7 8 9 10 11 | >>> john = User() >>> john.type 'admin' >>> john.type = 'superadmin' >>> john.type 'superadmin' >>> User.type 'admin' >>> joe = User() >>> joe.type 'admin' |
Attention aussi, quand l’attribut de classe est un objet mutable1, il peut être modifié par n’importe quelle instance de la classe.
1 2 3 4 5 6 7 8 | >>> class User: ... users = [] ... >>> john, joe = User(), User() >>> john.users.append(john) >>> joe.users.append(joe) >>> john.users [<__main__.User object at 0x7f3b7acf8b70>, <__main__.User object at 0x7f3b7acf8ba8>] |
L’attribut de classe est aussi conservé lors de l’héritage, et partagé avec les classes filles (sauf lorsque les classes filles redéfinissent l’attribut, de la même manière que pour les instances).
1 2 3 4 5 6 7 8 9 10 | >>> class Guest(User): ... pass ... >>> Guest.users [<__main__.User object at 0x7f3b7acf8b70>, <__main__.User object at 0x7f3b7acf8ba8>] >>> class Admin(User): ... users = [] ... >>> Admin.users [] |
-
Un objet mutable est un objet que l’on peut modifier (liste, dictionnaire) par opposition à un objet immutable (nombre, chaîne de caractères, tuple). ↩
La méthode pour avoir la classe
Comme pour les attributs, des méthodes peuvent être définies au niveau de la classe. C’est par exemple le cas de la méthode mro
.
1 | int.mro() |
Les méthodes de classe constituent des opérations relatives à la classe mais à aucune instance.
Elles recevront la classe courante en premier paramètre (nommé cls
, correspondant au self
des méthodes d’instance), et auront donc accès aux autres attributs et méthodes de classe.
Reprenons notre classe User
, à laquelle nous voudrions ajouter le stockage de tous les utilisateurs, et la génération automatique de l’id
.
Il nous suffirait d’une même méthode de classe pour stocker l’utilisateur dans un attribut de classe users
, et qui lui attribuerait un id
en fonction du nombre d’utilisateurs déjà enregistrés.
1 2 3 4 5 6 7 | >>> root = Admin('root', 'toor') >>> root <User: 1, root> >>> User('john', '12345') <User: 2, john> >>> guest = Guest('guest') <User: 3, guest> |
Les méthodes de classe se définissent comme les méthodes habituelles, à la différence près qu’elles sont précédées du décorateur classmethod
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | import crypt class User: users = [] def __init__(self, name, password): self.name = name self._salt = crypt.mksalt() self._password = self._crypt_pwd(password) self.register(self) @classmethod def register(cls, user): cls.users.append(user) user.id = len(cls.users) def _crypt_pwd(self, password): return crypt.crypt(password, self._salt) def check_pwd(self, password): return self._password == self._crypt_pwd(password) def __repr__(self): return '<User: {}, {}>'.format(self.id, self.name) class Guest(User): def __init__(self, name): super().__init__(name, '') def check_pwd(self, password): return True class Admin(User): def manage(self): print('I am an über administrator!') |
Vous pouvez constater le résultat en réessayant le code donné plus haut.
Le statique c'est fantastique
Les méthodes statiques sont très proches des méthodes de classe, mais sont plus à considérer comme des fonctions au sein d’une classe.
Contrairement aux méthodes de classe, elles ne recevront pas le paramètre cls
, et n’auront donc pas accès aux attributs de classe, méthodes de classe ou méthodes statiques.
Les méthodes statiques sont plutôt dédiées à des comportements annexes en rapport avec la classe, par exemple on pourrait remplacer notre attribut id
par un uuid aléatoire, dont la génération ne dépendrait de rien d’autre dans la classe.
Elles se définissent avec le décorateur staticmethod
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import uuid class User: def __init__(self, name, password): self.id = self._gen_uuid() self.name = name self._salt = crypt.mksalt() self._password = self._crypt_pwd(password) @staticmethod def _gen_uuid(): return str(uuid.uuid4()) def _crypt_pwd(self, password): return crypt.crypt(password, self._salt) def check_pwd(self, password): return self._password == self._crypt_pwd(password) |
1 2 3 | >>> john = User('john', '12345') >>> john.id '69ef1327-3d96-42a9-94e6-622619fbf666' |
Attribut es-tu là ?
Nous savons récupérer et assigner un attribut dont le nom est fixé, cela se fait facilement à l’aide des instructions obj.foo
et obj.foo = value
.
Mais nous est-il possible d’accéder à des attributs dont le nom est variable ?
Prenons une instance john
de notre classe User
, et le nom d’un attribut que nous voudrions extraire :
1 2 | >>> john = User('john', '12345') >>> attr = 'name' |
La fonction getattr
nous permet alors de récupérer cet attribut.
1 2 | >>> getattr(john, attr) 'john' |
Ainsi, getattr(obj, 'foo')
est équivalent à obj.foo
.
On trouve aussi une fonction hasattr
pour tester la présence d’un attribut dans un objet.
Elle est construite comme getattr
mais retourne un booléen pour savoir si l’attribut est présent ou non.
1 2 3 4 5 6 | >>> hasattr(john, 'name') True >>> hasattr(john, 'last_name') False >>> hasattr(john, 'id') True |
De la même manière, les fonctions setattr
et delattr
servent respectivement à modifier et supprimer un attribut.
1 2 3 4 5 6 7 8 | >>> setattr(john, 'name', 'peter') # équivalent à `john.name = 'peter'` >>> john.name 'peter' >>> delattr(john, 'name') # équivalent à `del john.name` >>> john.name Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'User' object has no attribute 'name' |
La dynamique des propriétés
Les propriétés sont une manière en Python de « dynamiser » les attributs d’un objet. Ils permettent de générer des attributs à la volée à partir de méthodes de l’objet.
Un exemple valant mieux qu’un long discours :
1 2 3 4 5 6 7 | class ProfilePicture: @property def picture(self): return '{}-{}.png'.format(self.id, self.name) class UserPicture(ProfilePicture, User): pass |
On définit donc une propriété picture
, qui s’utilise comme un attribut. Chaque fois qu’on appelle picture
, la méthode correspondante est appelée et le résultat est calculé.
1 2 3 4 5 6 | >>> john = UserPicture('john', '12345') >>> john.picture '1-john.png' >>> john.name = 'John' >>> john.picture '1-John.png' |
Il s’agit là d’une propriété en lecture seule, il nous est en effet impossible de modifier la valeur de l’attribut picture
.
1 2 3 4 | >>> john.picture = 'toto.png' Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: can't set attribute |
Pour le rendre modifiable, il faut ajouter à notre classe la méthode permettant de gérer la modification, à l’aide du décorateur @picture.setter
(le décorateur setter
de notre propriété picture
, donc).
On utilisera ici un attribut _picture
, qui pourra contenir l’adresse de l’image si elle a été définie, et None
le cas échéant.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class ProfilePicture: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._picture = None @property def picture(self): if self._picture is not None: return self._picture return '{}-{}.png'.format(self.id, self.name) @picture.setter def picture(self, value): self._picture = value class UserPicture(ProfilePicture, User): pass |
1 2 3 4 5 6 | >>> john = UserPicture('john', '12345') >>> john.picture '1-john.png' >>> john.picture = 'toto.png' >>> john.picture 'toto.png' |
Enfin, on peut aussi coder la suppression de l’attribut à l’aide de @picture.deleter
, ce qui revient à réaffecter None
à l’attribut _picture
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | class ProfilePicture: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._picture = None @property def picture(self): if self._picture is not None: return self._picture return '{}-{}.png'.format(self.id, self.name) @picture.setter def picture(self, value): self._picture = value @picture.deleter def picture(self): self._picture = None class UserPicture(ProfilePicture, User): pass |
1 2 3 4 5 6 7 8 9 | >>> john = UserPicture('john', '12345') >>> john.picture '1-john.png' >>> john.picture = 'toto.png' >>> john.picture 'toto.png' >>> del john.picture >>> john.picture '1-john.png' |
L'art des classes abstraites
La notion de classes abstraites est utilisée lors de l’héritage pour forcer les classes filles à implémenter certaines méthodes (dites méthodes abstraites) et donc respecter une interface.
Les classes abstraites ne font pas partie du cœur même de Python, mais sont disponibles via un module de la bibliothèque standard, abc
(Abstract Base Classes).
Ce module contient notamment la classe ABC
et le décorateur abstractmethod
, pour définir respectivement une classe abstraite et une méthode abstraite de cette classe.
Une classe abstraite doit donc hériter d’ABC
, et utiliser le décorateur cité pour définir ses méthodes abstraites.
1 2 3 4 5 6 | import abc class MyABC(abc.ABC): @abc.abstractmethod def foo(self): pass |
Il nous est impossible d’instancier des objets de type MyABC
, puisqu’une méthode abstraite n’est pas implémentée :
1 2 3 4 | >>> MyABC() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: Can't instantiate abstract class MyABC with abstract methods foo |
Il en est de même pour une classe héritant de MyABC
sans redéfinir la méthode.
1 2 3 4 5 6 7 | >>> class A(MyABC): ... pass ... >>> A() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: Can't instantiate abstract class A with abstract methods foo |
Aucun problème par contre avec une autre classe qui redéfinit bien la méthode.
1 2 3 4 5 6 7 8 | >>> class B(MyABC): ... def foo(self): ... return 7 ... >>> B() <__main__.B object at 0x7f33065316a0> >>> B().foo() 7 |
TP : Base de données
Pour ce dernier TP, nous aborderons les méthodes de classe et les propriétés.
Reprenons notre forum, auquel nous souhaiterions ajouter la gestion d’une base de données.
Notre base de données sera une classe avec deux méthodes, insert
et select
. Son implémentation est libre, elle doit juste respecter l’interface suivante :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | >>> class A: pass ... >>> class B: pass ... >>> >>> db = Database() >>> obj = A() >>> obj.value = 42 >>> db.insert(obj) >>> obj = A() >>> obj.value = 5 >>> db.insert(obj) >>> obj = B() >>> obj.value = 42 >>> obj.name = 'foo' >>> db.insert(obj) >>> >>> db.select(A) <__main__.A object at 0x7f033697f358> >>> db.select(A, value=5) <__main__.A object at 0x7f033697f3c8> >>> db.select(B, value=42) <__main__.B object at 0x7f033697f438> >>> db.select(B, value=42, name='foo') <__main__.B object at 0x7f033697f438> >>> db.select(B, value=5) ValueError: item not found |
Nous ajouterons ensuite une classe Model
, qui se chargera de stocker dans la base toutes les instances créées.
Model
comprendra une méthode de classe get(**kwargs)
chargée de réaliser une requête select
sur la base de données et de retourner l’objet correspondant.
Les objets de type Model
disposeront aussi d’une propriété id
, retournant un identifiant unique de l’objet.
On pourra alors faire hériter nos classes User
et Post
de Model
, afin que les utilisateurs et messages soient stockés en base de données.
Dans un second temps, on pourra faire de Model
une classe abstraite, par exemple en rendant abstraite la méthode __init__
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | import abc import datetime class Database: data = [] def insert(self, obj): self.data.append(obj) def select(self, cls, **kwargs): items = (item for item in self.data if isinstance(item, cls) and all(hasattr(item, k) and getattr(item, k) == v for (k, v) in kwargs.items())) try: return next(items) except StopIteration: raise ValueError('item not found') class Model(abc.ABC): db = Database() @abc.abstractmethod def __init__(self): self.db.insert(self) @classmethod def get(cls, **kwargs): return cls.db.select(cls, **kwargs) @property def id(self): return id(self) class User(Model): def __init__(self, name): super().__init__() self.name = name class Post(Model): def __init__(self, author, message): super().__init__() self.author = author self.message = message self.date = datetime.datetime.now() def format(self): date = self.date.strftime('le %d/%m/%Y à %H:%M:%S') return '<div><span>Par {} {}</span><p>{}</p></div>'.format(self.author.name, date, self.message) if __name__ == '__main__': john = User('john') peter = User('peter') Post(john, 'salut') Post(peter, 'coucou') print(Post.get(author=User.get(name='peter')).format()) print(Post.get(author=User.get(id=john.id)).format()) |
Ces dernières notions ont dû compléter vos connaissances du modèle objet de Python, et vous devriez maintenant être prêts à vous lancer dans un projet exploitant ces concepts.