XV. Compléments sur les classes▲
XV-1. L’héritage▲
L’héritage de classes est un mécanisme qui permet à une classe Y de bénéficier des attributs et méthodes d’une classe X tout en pouvant se rajouter ses propres attributs et méthodes. Cela permet de définir facilement un arbre d’objets tout en centralisant les éléments communs.
Un bon résumé pour définir l’héritage est de dire « est un ». Ainsi, si on admet qu’une voiture est un véhicule, alors on peut concevoir un héritage entre un objet véhicule et un objet voiture.
La syntaxe d'un héritage se fait à la définition de l'objet où l’on rajoute, après son nom, le nom de l'objet hérité entre parenthèses. Dans le cas d’un héritage multiple (un objet héritant de plusieurs), alors on met les différents noms des objets séparés par des virgules.
Exemple : une gestion de véhicules pourra commencera par définir un objet véhicule pour l’affiner ensuite avec un objet voiture et un autre avion.
On commence alors par créer l'objet de base vehicule…
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
class
vehicule:
def
__init__
(
self, vitesse, roues):
self._vitesse=
vitesse
self._roues=
roues
def
affich
(
self):
print
(
"véhicule (
%s
) - vitesse=
%d
, roues=
%d
"
%
(
self,
self._vitesse,
self._roues,
)
)
Ensuite, on crée les objets voiture et avion qui héritent de cet objet vehicule.
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.
class
voiture
(
vehicule):
def
__init__
(
self, motorisation, vitesse, roues):
vehicule.__init__
(
self, vitesse, roues)
self.__motorisation=
motorisation
def
affich
(
self):
vehicule.affich
(
self)
print
(
"voiture (
%s
) - motorisation=
%s
, vitesse=
%d
, roues=
%d
"
%
(
self,
self.__motorisation,
self._vitesse,
self._roues,
)
)
class
avion
(
vehicule):
def
__init__
(
self, altitude, vitesse, roues):
super(
avion, self).__init__
(
vitesse, roues)
self.__altitude=
altitude
def
affich
(
self):
super(
avion, self).affich
(
)
print
(
"avion (
%s
) - altitude=
%d
, vitesse=
%d
, roues=
%d
"
%
(
self,
self.__altitude,
self._vitesse,
self._roues,
)
)
>>>
v=
voiture
(
"diesel"
, 100
, 4
)
>>>
v.affich
(
)
véhicule (<
__main__
.voiture object at 0xb72308ac
>
) -
vitesse=
100
, roues=
4
voiture (<
__main__
.voiture object at 0xb72308ac
>
) -
motorisation=
diesel, vitesse=
100
, roues=
4
>>>
a=
avion
(
25000
, 200
, 2
)
>>>
a.affich
(
)
véhicule (<
__main__
.avion object at 0xb723090c
>
) -
vitesse=
200
, roues=
2
avion (<
__main__
.avion object at 0xb723090c
>
) -
altitude=
25000
, vitesse=
200
, roues=
2
Grâce à l’héritage, les objets voiture et avion récupèrent automatiquement les attributs de l’objet parent vehicule (vitesse et roues) qui peuvent alors être considérés et manipulés comme attributs au même titre que les autres.
L’héritage de classes s’étend bien entendu aussi aux éléments privés, mais comme ils sont privés, ils ne sont pas accessibles depuis la classe fille (sauf en donnant leur vrai nom obfusqué). D’où le simple underscore dans les attributs vitesse et roues de l’exemple pour que les classes filles puissent y accéder tout en indiquant aux autres développeurs que ces éléments doivent rester protégés).
De plus, l’héritage entraine une délégation des méthodes (la classe fille délègue certaines tâches à la classe mère ; telle la méthode __init__
(
) de cet exemple qui fait appel à vehicule.__init__
(
) pour initialiser les éléments de vehicule ou la méthode affich
(
) qui fait appel à vehicule.affich
(
) pour les afficher).
Dans la majorité des langages objets (C++, C#, php), cette délégation est implicite. En Python, elle sera implicite si la classe fille ne définit pas de méthode du même nom, mais devra être explicitement demandée dans le cas contraire (surcharge).
Celle-ci peut se faire de deux façons :
- appel explicite de l’objet et de la méthode dont on veut hériter. La syntaxe est alors la suivante : classe_mere.
methode
(
instance de classe, paramètres de la methode). Dans l'exemple, c'est la façon dont l'objet voiture délègue l'initialisation et l'affichage à l'objet vehicule ; utilisation de la fonction super
(
). La syntaxe est alors la suivante : super(
classe courante, instance de classe).methode
(
paramètres). Dans l'exemple, c'est la façon dont l'objet avion délègue l'initialisation et l'affichage à l'objet vehicule.
La différence entre les deux écritures est généralement minime. Elle ne devient réellement visible que dans les cas d’héritages complexes (tel un héritage en diamant où deux classes B et C héritent chacune de A ; et une quatrième classe D hérite de B et C en même temps) :
Sélectionnez 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.
|
Sélectionnez 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24.
|
Avec la syntaxe classique, la méthode __init__
(
) de la classe A est appelée deux fois (une fois depuis la classe B et une autre depuis la classe C). Avec la syntaxe passant par la fonction super(
), la méthode __init__
(
) de la classe A n’est appelée qu’une seule fois via le « MRO » (Method Resolution Order) de Python.
À noter : puisque les paramètres de la fonction super(
) respectent toujours le même schéma (classe courante, instance courante), elle a été simplifiée dans Python 3 pour pouvoir être appelée sans paramètres ; ceux-ci étant alors pris par défaut comme étant le nom et l’instance de la classe qui invoque la fonction.
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.
class
A:
def
__init__
(
self, mot):
print
(
"a"
, mot)
class
B
(
A):
def
__init__
(
self, mot):
print
(
"b"
)
super(
B, self).__init__
(
mot) # Ancienne syntaxe
class
C
(
A):
def
__init__
(
self, mot):
print
(
"c"
)
super(
C, self).__init__
(
mot) # Ancienne syntaxe
class
D
(
A):
def
__init__
(
self, mot):
print
(
"d"
)
super(
).__init__
(
mot) # Nouvelle syntaxe
class
E
(
B, C, D):
def
__init__
(
self, mot):
print
(
"e"
)
super(
).__init__
(
mot) # Nouvelle syntaxe
>>>
E
(
"xxx"
)
e
b
c
d
a xxx
Cela apporte un avantage non négligeable quand une classe privée veut hériter d'un objet public. La première syntaxe oblige à passer par le nom obfusqué…
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
class
A:
def
__init__
(
self): print
(
"A:
%s
"
%
self)
class
B:
class
__private
(
A):
def
__init__
(
self):
super(
B._B__private, self).__init__
(
)
print
(
"__private:
%s
"
%
self)
def
__init__
(
self):
B.__private
(
)
print
(
"B:
%s
"
%
self)
>>>
B
(
)
A: <
__main__
.B.__private object at 0x7f52803002e8
>
__private: <
__main__
.B.__private object at 0x7f52803002e8
>
B: <
__main__
.B object at 0x7f52802f6dd8
>
… tandis que la nouvelle syntaxe laisse Python gérer tout seul.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
class
A:
def
__init__
(
self): print
(
"A:
%s
"
%
self)
class
B:
class
__private
(
A):
def
__init__
(
self):
super(
).__init__
(
)
print
(
"__private:
%s
"
%
self)
def
__init__
(
self):
B.__private
(
)
print
(
"B:
%s
"
%
self self)
>>>
B
(
)
A: <
__main__
.B.__private object at 0x7f52803002e8
>
__private: <
__main__
.B.__private object at 0x7f52803002e8
>
B: <
__main__
.B object at 0x7f52802f6dd8
>
XV-2. L’héritage face à l’évolution▲
Il a été vu au chapitre précédent que la délégation des méthodes (la classe fille délègue certaines tâches à la classe mère) n’était pas implicite. Ce qui peut poser des soucis liés à l’évolution de la classe mère.
Reprenons notre objet vehicule contenant un attribut « vitesse maximale »…
2.
3.
class
vehicule:
def
__init__
(
self, vMax):
self.vMax=
vMax
Celui qui voudra en hériter, quoi qu’il mette dans son propre objet, devra quand même prendre en considération cette valeur vMax. Exemple avec un objet voiture contenant un attribut marque…
2.
3.
4.
5.
6.
7.
8.
9.
10.
class
voiture
(
vehicule):
def
__init__
(
self, marque, vMax):
super(
).__init__
(
vMax)
self.vmarque=
marque
>>>
v=
voiture
(
"Lada"
, 10
)
>>>
print
(
v.marque)
Lada
>>>
print
(
v.vMax)
10
Si demain la classe vehicule évolue pour s’enrichir d’un attribut vMin, tous ses héritiers devront évoluer de la même façon, et de même pour ceux qui hériteront des objets eux‑mêmes hérités. Très rapidement cette contrainte deviendra ingérable et l’héritage ne sera plus utilisé.
Une solution pourra être utilisée à base de valeurs par défaut, mais cette solution ne sera que bancale (tout le monde n’est pas obligé de vouloir mettre une valeur bidon dans son objet quand l’appelant ne met pas ce qu’il faut).
La bonne façon pour régler ce souci est d’utiliser les arguments spéciaux *
args et **
kwargs. L’utilisateur qui veut hériter d’un objet particulier pourra d’abord récupérer ses propres arguments puis n’aura ensuite qu’à passer ces arguments spéciaux à l’objet parent.
Reprenons notre héritage…
2.
3.
4.
5.
6.
7.
8.
9.
10.
class
voiture
(
vehicule):
def
__init__
(
self, marque, *
args, **
kwargs):
super(
).__init__
(*
args, **
kwargs)
self.vmarque=
marque
>>>
v=
voiture
(
"Lada"
, 10
)
>>>
print
(
v.marque)
Lada
>>>
print
(
v.vMax)
10
Cela ne change pas le fait que si demain l’objet vehicule évolue, l’utilisateur qui l’utilise par délégation devra évoluer avec, mais le programmeur de l’objet voiture, lui, n’a rien à faire, son objet s’adaptera automatiquement aux évolutions de son ancêtre.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
# Objet "vehicule" ayant évolué
class
vehicule:
def
__init__
(
self, vMin, vMax):
self.vMin=
vMin
self.vMax=
vMax
# Objet "voiture " restant inchangé
class
voiture
(
vehicule):
def
__init__
(
self, marque, *
args, **
kwargs):
super(
).__init__
(*
args, **
kwargs)
self.vmarque=
marque
# Seul l’appelant (l’utilisateur) doit s’adapter
>>>
v=
voiture
(
"Lada"
, 0
, 10
)
>>>
print
(
v.marque)
Lada
>>>
print
(
v.vMin)
0
>>>
print
(
v.vMax)
10
XV-3. Les attributs et méthodes statiques▲
Les attributs et méthodes vus jusqu’à présent s’appliquaient tous aux instances de classes. En effet, pour deux instances distinctes, les valeurs d’attributs ne sont pas forcément les mêmes (deux personnes distinctes n’ont pas forcément le même nom) et les méthodes (qui généralement utilisent les attributs) prennent alors fatalement en compte la valeur des attributs en question, valeur qui diffère alors selon les instances créées.
Toutefois, certains attributs et méthodes peuvent s'appliquer à la classe elle‑même et non à ses instances. Dans ce cas, il peut être alors judicieux de les définir en tant qu'attributs ou méthodes statiques (appelées aussi « attributs ou méthodes de classe »).
XV-3-a. Attribut statique▲
Pour l'attribut c'est implicite dès lors qu'il est défini en dehors de toute méthode. À partir de là, depuis l'extérieur, l’accès à cet attribut se fera depuis le nom de la classe elle‑même.
Exemple : création d’un compteur d’instances
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
class
personne:
cpt=
0
# Compteur de personnes (membre statique)
def
__init__
(
self, nom):
self.__nom=
nom
personne.cpt+=
1
# Une personne de plus
def
__del__
(
self):
personne.cpt-=
1
# Une personne de moins
>>>
p1=
personne
(
"Pierre"
)
>>>
p2=
personne
(
"Paul"
)
>>>
p3=
personne
(
"Jacques"
)
>>>
personne.cpt
3
>>>
del
p3
>>>
personne.cpt
2
Bien que l’accès à l’attribut se fasse depuis le nom de la classe elle‑même, il peut aussi se faire depuis une instance de la classe et se comporte alors comme un « attribut d’instance » tout en restant attribut de classe (donc unique dans la classe).
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
class
toto: item=
123
>>>
toto.item
123
>>>
p1=
toto
(
)
>>>
p1.item
123
>>>
p2=
toto
(
)
>>>
p2.item
123
>>>
toto.item=
456
>>>
toto.item
456
>>>
p2.item
456
>>>
p1.item
456
>>>
p3=
toto
(
)
>>>
p3.item
456
L’attribut de classe peut toutefois devenir un vrai attribut d’instance si on le modifie (recrée) dans l’instance. Tout comme pour les variables globales et locales, l’attribut d’instance prend alors le pas sur l’attribut de classe.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
>>>
class
toto: item=
123
...
>>>
p1=
toto
(
)
>>>
p2=
toto
(
)
>>>
toto.item
123
>>>
p1.item
123
>>>
p2.item
123
>>>
p1.item=
456
>>>
p1.item
456
>>>
p2.item
123
>>>
toto.item
123
XV-3-b. Méthode statique▲
Pour la méthode, cela se fait en l’encapsulant dans une surcouche staticmethod(
). À ce moment‑là, la méthode ne recevra plus d’instance de classe dans ses arguments et n’a donc plus à définir de self.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
class
point:
def
__milieu
(
x, y):
return
(
x.__v +
y.__v) /
2
milieu1=
staticmethod(
__milieu) # Méthode statique
milieu2=
staticmethod(
lambda
x, y : (
x.__v +
y.__v) /
2
) # Méthode statique
def
__init__
(
self, v):
self.__v=
v
>>>
p1=
point
(
4
)
>>>
p2=
point
(
8
)
>>>
point.milieu1
(
p1, p2)
6.0
>>>
point.milieu2
(
p1, p2)
6.0
Comme on le voit, les méthodes milieu1
(
) ou milieu2
(
), permettant de calculer le milieu barycentrique de deux points, ne peuvent pas s’appliquer à un point en particulier. Elles ne peuvent donc pas être utilisées comme des méthodes d’instance. Cependant, elles ne concernent que des points et s’appliquent donc à tous les points en général et peuvent par conséquent être intégrées dans l’objet point (ce qui renforce la modularité du code). La solution est d’en faire alors des « méthodes statiques ».
À noter : la méthode interne __milieu
(
) étant définie dans la classe, elle bénéficie quand même d’un accès à tous les attributs de la classe, y compris ses attributs privés (elle reçoit en paramètre deux instances de point x et y, elle a alors accès à x.__v et y.__v). Et les méthodes statiques milieu1
(
) et milieu2
(
) ont aussi cette possibilité.
Et de même que pour les getters/setters/deleters, il existe un sucre syntaxique @staticmethod
qui permet de raccourcir cette surcouche staticmethod(
).
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
class
point:
@staticmethod
# Préparation à la méthode de classe
def
milieu
(
x, y): # Définition de la méthode de classe
return
(
x.__v +
y.__v) /
2
def
__init__
(
self, v):
self.__v=
v
>>>
p1=
point
(
4
)
>>>
p2=
point
(
8
)
>>>
point.milieu
(
p1, p2)
6.0
XV-4. La fonction « classmethod() »▲
La fonction classmethod(
) permet de transformer une méthode pour lui faire recevoir en premier paramètre non plus l’instance, mais la classe qui l’évoque.
Prenons un exemple simple : un objet permettant de manipuler une date (jour, mois, année)…
2.
3.
4.
5.
6.
7.
8.
9.
class
date:
def
__init__
(
self, jj, mm, aa):
self.jj=
jj
self.mm=
mm
self.aa=
aa
>>>
a=
date
(
1
, 1
, 2000
)
>>>
print
(
a.jj, a.mm, a.aa)
1
1
2000
L’utilisateur qui veut utiliser cet objet doit alors l’instancier selon une convention bien définie en lui passant les trois caractéristiques attendues (jour, mois et année) en argument.
Si maintenant l’utilisateur reçoit une date sous forme de chaîne ("jj/mm/aaaa"
), il doit alors l’éclater pour pouvoir récupérer ses divers éléments avant de pouvoir créer son objet.
Si ce comportement se généralise, il peut alors avoir envie de modifier son objet pour qu’il puisse absorber ou une date classique ou une date chaîne. Petit travail facile de factorisation qui, en retour, lui simplifiera ensuite grandement l’écriture du code qui utilisera cet objet.
Une première solution sera alors de modifier __init__
(
) pour l’autoriser à absorber les deux types de paramètres. Faisable, mais si demain un 3° format différent se présente, et un 4° après-demain…?
Une seconde solution sera de créer une méthode statique renvoyant un nouvel objet date :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
class
date:
def
__init__
(
self, jj, mm, aa):
self.jj=
jj
self.mm=
mm
self.aa=
aa
@staticmethod
def
fromString
(
string):
(
jj, mm, aa)=
string.split
(
"/"
)
return
date
(
jj, mm, aa)
>>>
a=
date.fromString
(
"1/1/2000"
)
>>>
print
(
a.jj, a.mm, a.aa)
1
1
2000
L’avantage c’est que si un troisième format d’entrée arrive demain, on pourra toujours rajouter une autre méthode fromXXX, fromYYY et ainsi de suite.
L’inconvénient c'est que la méthode utilise le nom de l’objet de façon littérale. Déjà cela induit des contraintes d’évolutivité (si le nom change demain…) et surtout cela peut poser des soucis liés à l’héritage (la méthode renvoie un objet date alors que l’utilisateur pourrait avoir envie d’un objet hérité).
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
class
date:
def
__init__
(
self, jj, mm, aa):
self.jj=
jj
self.mm=
mm
self.aa=
aa
@staticmethod
def
fromString
(
string):
return
date
(*
string.split
(
"/"
)) # Remarquez l'utilisation de l'étoile (unpacking)…
class
subDate
(
date): pass
>>>
a=
date.fromString
(
"1/1/2000"
)
>>>
type(
a)
<
class
'__main__.date'
>
>>>
b=
subDate.fromString
(
"1/1/3000"
)
>>>
type(
b)
<
class
'__main__.date'
>
# Il voulait créer un subDate !!!
La bonne solution sera d’utiliser alors une méthode encapsulée dans une surcouche classmethod(
). Cette méthode recevra alors en premier paramètre le nom de la classe qui l’invoque (généralement on stocke ce nom dans un paramètre conventionnellement nommé cls). L’utilisateur pourra alors utiliser ce nom de classe pour générer l’objet qui en découle.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
class
date:
def
__init__
(
self, jj, mm, aa):
self.jj=
jj
self.mm=
mm
self.aa=
aa
def
__fromString
(
cls, string):
return
cls
(*
string.split
(
"/"
))
fromString=
classmethod(
__fromString)
class
subDate
(
date): pass
>>>
a=
date.fromString
(
"1/1/2000"
)
>>>
type(
a)
<
class
'__main__.date'
>
>>>
b=
subDate.fromString
(
"1/1/3000"
)
>>>
type(
b)
<
class
'__main__.subDate'
>
# Il a bien son objet subDate !!!
Cette solution répond aux mêmes contraintes que la solution précédente, mais offre l’avantage de suivre l’évolution du code (si le nom de l’objet change, cela se fera de façon transparente sans avoir besoin de revérifier tout son contenu), ainsi que les contraintes liées à l'héritage.
Et de même que pour les getters/setters/deleters ou pour la méthode statique, le sucre syntaxique @classmethod
est lui aussi autorisé.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
class
date:
def
__init__
(
self, jj, mm, aa):
self.jj=
jj
self.mm=
mm
self.aa=
aa
@classmethod
def
fromString
(
cls, string):
return
cls
(*
string.split
(
"/"
))
>>>
a=
date.fromString
(
"1/1/2000"
)
>>>
type(
a)
<
class
'__main__.date'
>
XV-5. Gestion interne des attributs▲
Dans ce chapitre, tous les attributs et méthodes décrits sont automatiquement créés par Python pour tout objet.
XV-5-a. L’attribut __dict__▲
L’attribut __dict__
référence tous les attributs créés dans l’instance.
2.
3.
4.
5.
6.
7.
8.
class
xxx:
def
__init__
(
self):
self.s=
"Hello"
self.i=
123
>>>
x=
xxx
(
)
>>>
print
(
x.__dict__
)
{'s'
: 'Hello'
, 'i'
: 123
}
Tout nouvel attribut viendra compléter cet attribut interne…
2.
3.
>>>
x.f=
3.1416
>>>
print
(
x.__dict__
)
{'s'
: 'Hello'
, 'i'
: 123
, 'f'
: 3.1416
}
De même, tout remplissage manuel de cet attribut équivaudra à la création (ou la modification) de l’attribut correspondant…
2.
3.
>>>
x.__dict__
["i"
]=
456
>>>
print
(
x.i)
456
XV-5-b. La méthode __delattr__()▲
La méthode __delattr__
(
) est automatiquement appelée à chaque fois qu’un attribut est supprimé de son instance.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
class
xxx:
def
__init__
(
self):
self.s=
"Hello"
self.i=
123
def
__delattr__
(
self, attr):
print
(
"deleting [
%s
]"
%
attr)
super(
).__delattr__
(
attr)
>>>
x=
xxx
(
)
>>>
print
(
x.i)
123
>>>
del
x.i
deleting [i]
>>>
print
(
x.i)
Traceback (
most recent call last):
File "<stdin>"
, line 1
, in
<
module>
AttributeError
: 'xxx'
object has no attribute 'i'
Toutefois, la méthode n’est pas invoquée si l’attribut est supprimé par le __dict__
.
2.
3.
4.
5.
6.
>>>
del
x.__dict__
["s"
]
>>>
print
(
x.s)
Traceback (
most recent call last):
Traceback (
most recent call last):
File "<stdin>"
, line 1
, in
<
module>
AttributeError
: 'xxx'
object has no attribute 's'
XV-5-c. La méthode __getattr__()▲
La méthode __getattr__
(
) est automatiquement appelée à chaque fois qu’une instance invoque un attribut inexistant.
En revanche, pour des raisons de performances, si l’attribut existe, alors la méthode n’est pas appelée.
class
xxx:
def
__init__
(
self):
self.s=
"Hello"
self.i=
123
def
__getattr__
(
self, attr):
print
(
"getting [
%s
]"
%
attr)
return
"perdu"
>>>
x=
xxx
(
)
>>>
print
(
x.s)
Hello
>>>
print
(
x.i)
123
>>>
print
(
x.j)
getting [j]
Perdu
XV-5-d. La méthode __getattribute__()▲
La méthode __getattribute__
(
) est automatiquement appelée à chaque fois qu’un attribut est invoqué par son instance.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
class
xxx:
def
__init__
(
self):
self.s=
"Hello"
self.i=
123
def
__getattribute__
(
self, attr):
print
(
"getting [
%s
]"
%
attr)
return
super(
).__getattribute__
(
attr)
>>>
x=
xxx
(
)
>>>
print
(
x.i)
getting [i]
123
Contrairement à ce qui se passe dans la méthode __delattr__
(
), ici, passer par le __dict__
pour tenter de contourner cette méthode en y cherchant directement l’attribut implique quand même un accès à l’attribut __dict__
ce qui provoque alors tout de même l’appel de la méthode.
2.
3.
>>>
print
(
x.__dict__
["i"
])
getting [__dict__
]
123
XV-5-e. La méthode __setattr__()▲
La méthode __setattr__
(
) est automatiquement appelée à chaque fois qu’un attribut est créé ou modifié par son instance.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
class
xxx:
def
__init__
(
self):
self.s=
"Hello"
self.i=
123
def
__setattr__
(
self, attr, value):
print
(
"setting [
%s
]: (
%d
)"
%
(
attr, value))
super(
).__setattr__
(
attr, value)
>>>
x=
xxx
(
)
setting [s]: (
Hello)
setting [i]: (
123
)
>>>
print
(
x.i)
123
>>>
x.i=
456
setting [i]: (
456
)
>>>
print
(
x.i)
456
Cependant, la méthode n’est pas invoquée si l’attribut est créé ou modifié par le __dict__
et la modification du __dict__
n’entraine pas non plus l’appel de cette méthode.
2.
3.
>>>
x.__dict__
["i"
]=
789
>>>
print
(
x.i)
789
XV-5-f. La méthode __new__()▲
En Python, la méthode spéciale __init__
(
) est souvent appelée constructeur de l’objet. Il s’agit en fait d’un abus de langage : la méthode __init__
(
) ne construit pas l’objet, elle intervient après la création de ce dernier pour l’initialiser.
Le vrai constructeur d’une classe est la méthode __new__
(
). Cette méthode est une méthode statique (un cas particulier qui n’a pas besoin d’être déclaré comme tel) qui prend une classe en premier paramètre (le paramètre self n’existe pas encore puisque l’objet n’est pas encore créé), et doit renvoyer l’objet nouvellement créé. Les autres paramètres sont identiques à ceux reçus par __init__
(
) puisque la méthode doit ensuite appeler cette dernière en lui passant les paramètres qu’elle attend (ce qui se fait alors de façon automatique).
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
class
cercle:
def
__new__
(
cls, rayon):
print
(
"new [
%s
], rayon=
%d
"
%
(
cls, rayon))
return
super(
).__new__
(
cls)
def
__init__
(
self, rayon):
print
(
"init [
%s
], rayon=
%d
"
%
(
self, rayon))
>>>
c=
cercle
(
5
)
new [<
class
'__main__.cercle'
>
], rayon=
5
init [<
__main__
.cercle object at 0x7feff77427b8
>
], rayon=
5
>>>
print
(
c)
<
__main__
.cercle object at 0x7feff77427b8
>
Cette méthode est particulièrement utile pour les objets immuables (str, tuple). En effet, il est impossible d’agir sur les objets dans la méthode __init__
(
), puisque celle-ci intervient après la création de l’objet, et que de fait, l’objet créé n’est alors plus modifiable.
Si l’on souhaite hériter d’un type immuable et agir sur l’initialisation de l’objet, il est donc nécessaire de redéfinir __new__
(
). Par exemple, pour créer une classe point qui hériterait de tuple…
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
class
point
(
tuple):
def
__new__
(
cls, x, y):
print
(
"new [
%s
], x=
%s
, y=
%s
"
%
(
cls, x, y))
return
super(
).__new__
(
cls, (
x, y))
x=
property(
lambda
self: self[0
])
y=
property(
lambda
self: self[1
])
>>>
t=
point
(
2
, 3
)
new [<
class
'__main__.point'
>
], x=
2
, y=
3
>>>
print
(
t[0
])
2
>>>
print
(
t.x)
2
>>>
print
(
t[1
])
3
>>>
print
(
t.y)
3
… tout en gardant le bénéfice de l’immuabilité du tuple.
2.
3.
4.
>>>
t.x=
123
Traceback (
most recent call last):
File "<stdin>"
, line 1
, in
<
module>
AttributeError
: can't set attribute
XV-5-g. La méthode __init_subclass__()▲
En Python, la méthode spéciale __init_subclass__
(
) est appelée par un objet quand il est instancié par l’un de ses héritiers.
C’est une méthode là aussi statique qui prend également une classe en premier paramètre.
Elle permet de créer si besoin des attributs statiques dans la classe fille en les créant « à la volée » dans la classe mère.
Prenons un exemple simple : une gestion de figures géométriques avec nombre de sommets.
Première solution : des classes simples…
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
class
carre:
sommets=
4
def
__init__
(
self, cote):
print
(
"carre init:
%s
(cote=
%d
)"
%
(
self, cote))
self.cote=
cote
class
pentagone:
sommets=
5
def
__init__
(
self, cote):
print
(
"pentagone init:
%s
(cote=
%d
)"
%
(
self, cote))
self.cote=
cote
>>>
c=
carre
(
10
)
carre init: <
__main__
.carre object at 0x7f02cc573470
>
(
cote=
10
)
>>>
c.cote
10
>>>
c.sommets
4
>>>
p=
pentagone
(
12
)
pentagone init: <
__main__
.pentagone object at 0x7f02cc573438
>
(
cote=
12
)
>>>
p.cote
12
>>>
p.sommets
5
C’est dommage qu’il n’y ait pas quelque part une espèce de centralisation des éléments redondants (ici le nombre de sommets).
Donc seconde solution : des classes qui héritent d’une classe mère en lui passant les sommets qu’elle a à gérer…
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.
class
figure:
def
__init__
(
self, sommets):
print
(
"figure init:
%s
(sommets=
%d
)"
%
(
self, sommets))
self.sommets=
sommets
class
carre
(
figure):
def
__init__
(
self, cote):
super(
).__init__
(
4
)
print
(
"carre init:
%s
(cote=
%d
)"
%
(
self, cote))
self.cote=
cote
class
pentagone
(
figure):
def
__init__
(
self, cote):
super(
).__init__
(
5
)
print
(
"pentagone init:
%s
(cote=
%d
)"
%
(
self, cote))
self.cote=
cote
>>>
c=
carre
(
10
)
figure init: <
__main__
.carre object at 0x7fcf87fea438
>
(
sommets=
4
)
carre init: <
__main__
.carre object at 0x7fcf87fea438
>
(
cote=
10
)
>>>
c.cote
10
>>>
c.sommets
4
>>>
p=
pentagone
(
12
)
figure init: <
__main__
.pentagone object at 0x7fcf87fbcb38
>
(
sommets=
5
)
pentagone init: <
__main__
.pentagone object at 0x7fcf87fbcb38
>
(
cote=
12
)
>>>
p.cote
12
>>>
p.sommets
5
C’est fonctionnel, mais les sommets d’une figure n’ayant pas vocation à changer au cours du temps, c’est dommage de ne pas les stocker de façon statique.
D’où la 3° méthode : passer par __init_subclass__
(
) qui aura pour rôle de paramétrer l’attribut statique de la classe mère en fonction de la configuration de la classe fille.
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.
class
figure:
def
__init__
(
self):
print
(
"figure init:
%s
"
%
self)
def
__init_subclass__
(
cls, sommets):
super(
).__init_subclass__
(
)
print
(
"figure init_subclass:
%s
(sommets=
%d
)"
%
(
cls, sommets))
cls.sommets=
sommets
>>>
class
carre
(
figure, sommets=
4
):
... def
__init__
(
self, cote):
... super(
).__init__
(
)
... print
(
"carre init:
%s
(cote=
%d
)"
%
(
self, cote))
... self.cote=
cote
...
figure init_subclass: <
class
'__main__.carre'
>
(
sommets=
4
)
>>>
class
pentagone
(
figure, sommets=
5
):
... def
__init__
(
self, cote):
... super(
).__init__
(
)
... print
(
"pentagone init:
%s
(cote=
%d
)"
%
(
self, cote))
... self.cote=
cote
...
figure init_subclass: <
class
'__main__.pentagone'
>
(
sommets=
5
)
>>>
c=
carre
(
10
)
figure init: <
__main__
.carre object at 0x7fd04a525b70
>
carre init: <
__main__
.carre object at 0x7fd04a525b70
>
(
cote=
10
)
>>>
print
(
c.cote)
10
>>>
print
(
c.sommets)
4
>>>
p=
pentagone
(
12
)
figure init: <
__main__
.pentagone object at 0x7fd04a525b00
>
pentagone init: <
__main__
.pentagone object at 0x7fd04a525b00
>
(
cote=
12
)
>>>
print
(
p.cote)
12
>>>
print
(
p.sommets)
5
C’est un peu le principe en allégé de la métaclasse que l’on verra un peu plus tard.
XV-5-h. L’attribut __slots__▲
Il a été vu dans le chapitre sur les objets qu’il est possible de leur créer de nouveaux attributs de façon manuelle et externe…
2.
3.
4.
5.
6.
7.
8.
9.
10.
class
personne:
def
__init__
(
self, nom):
self.nom=
nom
>>>
p=
personne
(
"LeRoi"
)
>>>
p.prenom=
"Arthur"
>>>
p.nom
'LeRoi'
>>>
p.prenom
'Arthur'
… ce qui est rarement une bonne chose…
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
>>>
p1=
personne
(
"LeRoi"
)
>>>
p1.prenom=
"Arthur"
>>>
p2=
personne
(
"Autre"
)
>>>
>>>
for
x in
(
p1, p2):
... print
(
x.nom, x.prenom)
...
LeRoi, Arthur
Traceback (
most recent call last):
File "<stdin>"
, line 1
, in
<
module>
AttributeError
: personne' object has no attribute '
prenom'
L’attribut __slots__
(attribut statique) permet de protéger son objet contre de telles manipulations.
Il suffit de le créer dans son objet en lui donnant l’attribut ou la liste des attributs permis pour l’objet. Toute création d’un attribut non présent dans cette liste provoquera une exception.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
class
personne:
__slots__
=(
"nom"
, "prenom"
)
def
__init__
(
self, nom, prenom):
self.nom=
nom
self.prenom=
prenom
>>>
p=
personne
(
"LeRoi"
, "Arthur"
)
>>>
p.age=
800
Traceback (
most recent call last):
File "<stdin>"
, line 1
, in
<
module>
AttributeError
: 'personne'
object has no attribute 'age'
Ce verrouillage se fait au travers de l’attribut __dict__
qui est alors purement supprimé de l’objet.
2.
3.
4.
>>>
print
(
p.__dict__
)
Traceback (
most recent call last):
File "<stdin>"
, line 1
, in
<
module>
AttributeError
: 'personne'
object has no attribute '__dict__'
XV-6. Trucs et astuces divers sur les classes▲
XV-6-a. Rendre une classe « callable »▲
Une classe peut devenir « callable » ; c’est-à-dire qu’une instance de cette classe peut être appelée en tant que fonction ; par le biais de la méthode spécifique __call__
(
).
2.
3.
4.
5.
6.
7.
class
ccc:
def
__call__
(
self, x, y):
print
(
"ici (
%s
,
%s
)"
%
(
x, y))
>>>
x=
ccc
(
) # Création d’un objet "ccc" instancié dans une variable "x"
>>>
x
(
2
, 3
) # Appel de "x" comme si c’était une fonction
ici (
2
, 3
)
XV-6-b. Rendre une classe « indexable »▲
Une classe peut devenir « indexable » ; c’est-à-dire être utilisée comme si elle était une séquence (tuple, liste ou dictionnaire) dans laquelle on accèderait à un élément indexé ou slicé ; par le biais des méthodes spécifiques __getitem__
(
) (pour récupérer un élément) et __setitem__
(
) (pour positionner un élément).
Cela se pratique généralement quand la classe contient un attribut de nature séquentiel et qu’on désire accéder aux éléments distincts de l’attribut sans passer par son nom (surtout si cet attribut séquentiel est un attribut privé).
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
class
maListe:
def
__init__
(
self, tab=
None
):
self.__tab=
list(
tab or
(
))
def
__getitem__
(
self, idx):
print
(
"getitem (
%d
)"
%
idx)
return
self.__tab[idx]
def
__setitem__
(
self, idx, n):
print
(
"setitem (
%d
,
%s
)"
%
(
idx, n))
self.__tab[idx]=
n
>>>
m=
maListe
((
"Pim"
, "Pam"
, "Poum"
))
>>>
print
(
m[2
])
getitem (
2
)
Poum
>>>
m[2
]=
"toto"
setitem (
2
, toto)
>>>
print
(
m[2
])
toto
XV-6-c. Rendre une classe « itérable »▲
Une classe peut devenir « itérable » ; c’est-à-dire être utilisable dans une boucle d’itération ; par le biais de la méthode spécifique __iter__
(
).
Cela se pratique généralement quand la classe contient un attribut de nature séquentielle et qu’on désire itérer directement sur les éléments distincts de l’attribut sans passer par son nom (surtout si cet attribut séquentiel est un attribut privé) ni avoir besoin de passer par son indexation.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
class
maListe:
def
__init__
(
self, tab=
None
):
self.__tab=
list(
tab or
(
))
def
__iter__
(
self):
yield
from
self.__tab # for x in self.__tab: yield x (en Python 2)
>>>
m=
maListe
((
"Pim"
, "Pam"
, "Poum"
))
>>>
for
x in
m : print
(
x)
Pim
Pam
Poum
>>>
print
(
tuple(
m))
(
'Pim'
, 'Pam'
, 'Poum'
)
Une telle classe qui se veut itérable peut aussi implémenter la méthode spécifique __next__
(
) permettant alors d’y appliquer la fonction next
(
).
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
class
maListe:
def
__init__
(
self, tab=
None
):
self.__tab=
list(
tab or
(
))
self.__idx=
0
def
__next__
(
self):
if
self.__idx ==
len(
self.__tab):
raise
StopIteration
t=
self.__tab[self.__idx]
self.__idx+=
1
return
t
>>>
m=
maListe
((
"Pim"
, "Pam"
, "Poum"
))
>>>
while
True
: print
(
next
(
m))
...
Pim
Pam,
Poum
Traceback (
most recent call last):
File "<stdin>"
, line 13
, in
<
module>
while
True
: print
(
next
(
m))
File "<stdin>"
, line 7
, in
__next__
raise
StopIteration
StopIteration
XV-6-d. Rendre une classe « mesurable »▲
Une classe peut devenir « mesurable » ; c’est-à-dire indiquer sa longueur ; par le biais de la méthode spécifique __len__
(
).
Cela se pratique généralement quand la classe contient un attribut de nature séquentielle et qu’on désire avoir la longueur de cet attribut sans passer par son nom (surtout si cet attribut séquentiel est un attribut privé) ni avoir besoin de passer par son indexation.
XV-6-e. Indiquer une contenance dans une classe▲
Une classe peut indiquer une contenance ; c’est-à-dire vérifier si un élément est présent ou pas ; par le biais de la méthode spécifique __contains__
(
).
Cela se pratique généralement quand la classe contient un attribut de nature séquentielle et qu’on désire simplement vérifier si cet attribut contient un élément particulier sans passer par son nom (surtout si cet attribut séquentiel est un attribut privé) ni avoir besoin de passer par son indexation ou son itération.
2.
3.
4.
5.
6.
7.
8.
9.
class
maListe:
def
__init__
(
self, tab=
None
):
self.__tab=
list(
tab or
(
))
def
__contains__
(
self, x):
return
x in
self.__tab
>>>
m=
maListe
((
"Pim"
, "Pam"
, "Poum"
))
>>>
print
(
"Poum"
in
m)
True
XV-6-f. Définir les opérateurs usuels▲
Une classe peut définir des opérateurs usuels (comme l’addition, soustraction, etc.) permettant ensuite de l’utiliser dans des opérations classiques.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
class
cPoint:
def
__init__
(
self, x=
0
, y=
0
):
self.__x=
x
self.__y=
y
def
__add__
(
self, n): # Redéfinit l’addition
return
cPoint
(
self.__x +
n.__x, self.__y +
n.__y)
def
__repr__
(
self): # Redéfinit la façon de représenter un objet
return
"x=
%d
, y=
%d
"
%
(
self.__x, self.__y)
>>>
a=
cPoint
(
2
, 3
)
>>>
b=
cPoint
(
4
, 5
)
>>>
a+
b
x=
6
, y=
8
Voici une liste non exhaustive des opérateurs les plus usuels :
-
mathématiques :
__add__
(addition) : permet d’appeler objet+
n__radd__
(addition renversée) : permet d’appeler n+
objet,__iadd__
(addition inplace) : permet d’appeler objet+=
n (si cette méthode n’est pas présente, ce sera__add__
qui sera utilisée à la place),__sub__
(soustraction) : permet d’appeler objet‑n__rsub__
(soustraction renversée) : permet d’appeler n‑objet,__isub__
(soustraction inplace) : permet d’appeler objet‑=
n (si cette méthode n’est pas présente, ce sera__sub__
qui sera utilisée à la place),__mul__
(multiplication) : permet d’appeler objet*
n,__rmul__
(multiplication renversée) : permet d’appeler n*
objet,__imul__
(multiplication inplace) : permet d’appeler objet*=
n (si cette méthode n’est pas présente, ce sera__mul__
qui sera utilisée à la place),__div__
(division dans Python 2, n’existe pas dans Python 3) : permet d’appeler objet/
n,__rdiv__
(division renversée dans Python 2, n’existe pas dans Python 3) : permet d’appeler n/
objet,__idiv__
(division inplace renversée dans Python 2, n’existe pas dans Python 3) : permet d’appeler objet/=
n (si cette méthode n’est pas présente, ce sera__div__
qui sera utilisée à la place),__floordiv__
(division entière) : permet d’appeler objet//
n,__rfloordiv__
(division entière renversée) : permet d’appeler n//
objet,__ifloordiv__
(division entière inplace) : permet d’appeler objet//=
n (si cette méthode n’est pas présente, ce sera__floordiv__
qui sera utilisée à la place),__truediv__
(division exacte dans Python 3, n’existe pas dans Python 2) : permet d’appeler objet/
n,__rtruediv__
(division exacte renversée dans Python 3, n’existe pas dans Python 2) : permet d’appeler n/
objet,__itruediv__
(division exacte inplace dans Python 3, n’existe pas dans Python 2) : permet d’appeler objet/=
n (si cette méthode n’est pas présente, ce sera__truediv__
qui sera utilisée à la place),__mod__
(modulo) : permet d’appeler objet%
n,__rmod__
(modulo renversé) : permet d’appeler n%
objet,__imod__
(modulo inplace) : permet d’appeler objet%=
n (si cette méthode n’est pas présente, ce sera__mod__
qui sera utilisée à la place),__pow__
(puissance) : permet d’appeler objet**
n,__rpow__
(puissance renversée) : permet d’appeler n**
objet,__ipow__
(puissance inplace) : permet d’appeler objet**=
n (si cette méthode n’est pas présente, ce sera__pow__
qui sera utilisée à la place),__and__
(« et » bit à bit) : permet d’appeler objet&
n,__rand__
(« et » bit à bit renversé) : permet d’appeler n&
objet,__iand__
(« et » bit à bit inplace) : permet d’appeler objet&=
n (si cette méthode n’est pas présente, ce sera__and__
qui sera utilisée à la place),__or__
(« ou » bit à bit) : permet d’appeler objet|
n,__ror__
(« ou » bit à bit renversé) : permet d’appeler n|
objet,__ior__
(« ou » bit à bit inplace) : permet d’appeler objet|=
n (si cette méthode n’est pas présente, ce sera__or__
qui sera utilisée à la place),__xor__
(« ou exclusif » bit à bit) : permet d’appeler objet^
n,__rxor__
(« ou » bit à bit renversé) : permet d’appeler n^
objet,__ixor__
(« ou » bit à bit inplace) : permet d’appeler objet^=
n (si cette méthode n’est pas présente, ce sera__xor__
qui sera utilisée à la place),__lshift__
(décalage vers la gauche) : permet d’appeler objet<<
n,__ilshift__
(décalage vers la gauche inplace) : permet d’appeler objet<<=
n (si cette méthode n’est pas présente, ce sera__lshift__
qui sera utilisée à la place),__rshift__
(décalage vers la droite) : permet d’appeler objet>>
n,__irshift__
(décalage vers la droite inplace) : permet d’appeler objet>>=
n (si cette méthode n’est pas présente, ce sera__rshift__
qui sera utilisée à la place),__neg__
(passage en négatif) : permet d’appeler ‑objet,__pos__
(identité neutre) : permet d’appeler+
objet,__inv__
(inversion bit à bit) : permet d’appeler~
objet,__abs__
(valeur absolue) ;
booléens :
__eq__
(égal à),__ne__
(différent de),__lt__
(inférieur à),__le__
(inférieur ou égal à),__gt__
(supérieur à),__ge__
(supérieur ou égal à), et . À noter sous Python 2 la présence de__cmp__
(comparaison riche devant renvoyer -1/0/1 si le premier élément est inférieur, égal ou supérieur au second élément et peut se substituer à tous les autres opérateurs booléens, mais ayant disparu dans Python 3).
XV-6-g. Redéfinir les casts▲
Une classe peut redéfinir des opérations de cast qui lui seront appliquées. Ainsi, elle peut redéfinir __bool__
(booléen), __float__
(flottant), __int__
(entier), __bytes__
(chaîne), __str__
(chaîne) : permettent de surcharger le cast d’un objet en bool, float, int, bytes, str. À noter sous Python 2 que __bool__
se nomme alors __nonzero__
ainsi que la présence supplémentaire de __long__
et __unicode__
qui permettent de surcharger le cast d’un objet en long et en unicode. De plus, __bytes__
n’existe pas dans cette version.
Elle peut aussi redéfinir __repr__
(représentation) utilisé par repr(
).
XV-6-h. Rendre une classe « hashable »▲
Une classe peut devenir « hashable » ; c’est-à-dire pouvoir faire l’objet d’un calcul d’empreinte numérique ; par le biais de la méthode spécifique __hash__
(
).
2.
3.
4.
5.
6.
7.
class
maClass:
def
__hash__
(
self):
return
123
>>>
m=
maClass
(
)
>>>
print
(
hash(
m))
123
XV-6-i. Gérer la liste des attributs listés▲
Il est possible de contrôler la liste des attributs donnés par la fonction dir(
) ; par le biais de la méthode spécifique __dir__
(
). Cette méthode, si elle est implémentée, doit impérativement renvoyer un itérable qui sera finalement transformé en liste.
2.
3.
4.
5.
6.
7.
class
maClass:
def
__dir__
(
self):
return
"123"
>>>
m=
maClass
(
)
>>>
dir(
m)
[1
, 2
, 3
]
XV-6-j. Gérer le formatage▲
Il est possible de contrôler l’affichage d’une classe grâce à la méthode format
(
) ou celle des « f-strings ».
class
maClass:
def
__init__
(
self, n):
self.n=
n
def
__format__
(
self, fmt):
print
(
"fmt: [
%s
]"
%
fmt)
return
"{0:
{1}
}"
.format
(
self.n, fmt)
>>>
m=
maClass
(
42
)
>>>
print
(
"
{:05d}
"
.format
(
m))
fmt: [05
d]
00042
>>>
print
(
f"
{m:04d}
"
)
fmt: [04
d]
0042
XV-7. Les fonctions sont aussi des objets▲
Toute fonction étant elle aussi un objet, cela permet alors d'y créer des attributs personnels, et ce, même en dehors du corps de la fonction…
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
def
toto
(
n):
toto.args.append
(
n)
print
(
"Historique des arguments:
%s
"
%
toto.args)
>>>
toto.args=
[]
>>>
toto
(
1
)
Historique des arguments: [1
]
>>>
toto
(
2
)
Historique des arguments: [1
, 2
]
>>>
toto
(
3
)
Historique des arguments: [1
, 2
, 3
]
De plus, elle possède aussi des attributs et méthodes internes qui permettent au programmeur Python de les inspecter ou de les manipuler directement dans son code.
Voici une liste non exhaustive des éléments pouvant être utiles :
fonction.
__name__
: nom de la fonction ;fonction.
__doc__
: documentation de la fonction (sera vu ultérieurement) ;fonction.
__qualname__
: nom complet (dans le cas d'une fonction incluse dans un objet) de la fonction (n'existe pas dans Python 2) ;fonction.
__module__
: nom du module de la fonction (sera vu ultérieurement) ;fonction.
__defaults__
: tuple contenant les valeurs par défaut des arguments placés avant *args ;fonction.
__kwdefaults__
: dictionnaire contenant les valeurs par défaut des arguments placés entre*
args et**
kwargs (n'existe pas dans Python 2 dans lequel*
args est collé à**
kwargs) ;fonction.
__code__
: code de la fonction ;fonction.
__code__
.co_varnames : liste des paramètres de la fonction (y compris*
args et**
kwargs s'ils y sont) ;fonction.
__code__
.co_argcount : nombre de paramètres placés avant*
args ;fonction.
__code__
.co_kwonlyargcount : nombre de paramètres placés entre*
args et**
kwargs (n'existe pas dans Python 2 dans lequel*
args est collé à**
kwargs).